3 # Copyright (C) 2014 Cumulus Networks, Inc.
5 # This file is part of Frr.
7 # Frr is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by the
9 # Free Software Foundation; either version 2, or (at your option) any
12 # Frr is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Frr; see the file COPYING. If not, write to the Free
19 # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
24 - reads a frr configuration text file
25 - reads frr's current running configuration via "vtysh -c 'show running'"
26 - compares the two configs and determines what commands to execute to
27 synchronize frr's running configuration with the configuation in the
31 from __future__
import print_function
, unicode_literals
40 from collections
import OrderedDict
41 from ipaddress
import IPv6Address
, ip_network
42 from pprint
import pformat
46 return iter(d
.items())
49 log
= logging
.getLogger(__name__
)
52 class VtyshException(Exception):
57 def __init__(self
, bindir
=None, confdir
=None, sockdir
=None, pathspace
=None):
59 self
.confdir
= confdir
60 self
.pathspace
= pathspace
61 self
.common_args
= [os
.path
.join(bindir
or "", "vtysh")]
63 self
.common_args
.extend(["--config_dir", confdir
])
65 self
.common_args
.extend(["--vty_socket", sockdir
])
67 self
.common_args
.extend(["-N", pathspace
])
69 def _call(self
, args
, stdin
=None, stdout
=None, stderr
=None):
72 kwargs
["stdin"] = stdin
73 if stdout
is not None:
74 kwargs
["stdout"] = stdout
75 if stderr
is not None:
76 kwargs
["stderr"] = stderr
77 return subprocess
.Popen(self
.common_args
+ args
, **kwargs
)
79 def _call_cmd(self
, command
, stdin
=None, stdout
=None, stderr
=None):
80 if isinstance(command
, list):
81 args
= [item
for sub
in command
for item
in ["-c", sub
]]
83 args
= ["-c", command
]
84 return self
._call
(args
, stdin
, stdout
, stderr
)
86 def __call__(self
, command
, stdouts
=None):
88 Call a CLI command (e.g. "show running-config")
90 Output text is automatically redirected, decoded and returned.
91 Multiple commands may be passed as list.
93 proc
= self
._call
_cmd
(command
, stdout
=subprocess
.PIPE
)
94 stdout
, stderr
= proc
.communicate()
96 if stdouts
is not None:
97 stdouts
.append(stdout
.decode("UTF-8"))
99 'vtysh returned status %d for command "%s"' % (proc
.returncode
, command
)
101 return stdout
.decode("UTF-8")
103 def is_config_available(self
):
105 Return False if no frr daemon is running or some other vtysh session is
106 in 'configuration terminal' mode which will prevent us from making any
107 configuration changes.
110 output
= self("configure")
112 if "VTY configuration is locked by other VTY" in output
:
113 log
.error("vtysh 'configure' returned\n%s\n" % (output
))
118 def exec_file(self
, filename
):
119 child
= self
._call
(["-f", filename
])
120 if child
.wait() != 0:
121 raise VtyshException(
122 "vtysh (exec file) exited with status %d" % (child
.returncode
)
125 def mark_file(self
, filename
, stdin
=None):
127 ["-m", "-f", filename
],
128 stdout
=subprocess
.PIPE
,
129 stdin
=subprocess
.PIPE
,
130 stderr
=subprocess
.PIPE
,
133 stdout
, stderr
= child
.communicate()
134 except subprocess
.TimeoutExpired
:
136 stdout
, stderr
= child
.communicate()
137 raise VtyshException("vtysh call timed out!")
139 if child
.wait() != 0:
140 raise VtyshException(
141 "vtysh (mark file) exited with status %d:\n%s"
142 % (child
.returncode
, stderr
)
145 return stdout
.decode("UTF-8")
147 def mark_show_run(self
, daemon
=None):
148 cmd
= "show running-config"
150 cmd
+= " %s" % daemon
152 show_run
= self
._call
_cmd
(cmd
, stdout
=subprocess
.PIPE
)
154 ["-m", "-f", "-"], stdin
=show_run
.stdout
, stdout
=subprocess
.PIPE
158 stdout
, stderr
= mark
.communicate()
161 if show_run
.returncode
!= 0:
162 raise VtyshException(
163 "vtysh (show running-config) exited with status %d:"
164 % (show_run
.returncode
)
166 if mark
.returncode
!= 0:
167 raise VtyshException(
168 "vtysh (mark running-config) exited with status %d" % (mark
.returncode
)
171 return stdout
.decode("UTF-8")
174 class Context(object):
176 A Context object represents a section of frr configuration such as:
179 description swp3 -> r8's swp1
184 or a single line context object such as this:
190 def __init__(self
, keys
, lines
):
194 # Keep a dictionary of the lines, this is to make it easy to tell if a
195 # line exists in this Context
196 self
.dlines
= OrderedDict()
199 self
.dlines
[ligne
] = True
202 return str(self
.keys
) + " : " + str(self
.lines
)
204 def add_lines(self
, lines
):
206 Add lines to specified context
209 self
.lines
.extend(lines
)
212 self
.dlines
[ligne
] = True
215 def get_normalized_es_id(line
):
217 The es-id or es-sys-mac need to be converted to lower case
219 sub_strs
= ["evpn mh es-id", "evpn mh es-sys-mac"]
220 for sub_str
in sub_strs
:
221 obj
= re
.match(sub_str
+ " (?P<esi>\S*)", line
)
223 line
= "%s %s" % (sub_str
, obj
.group("esi").lower())
228 def get_normalized_mac_ip_line(line
):
229 if line
.startswith("evpn mh es"):
230 return get_normalized_es_id(line
)
232 if not "ipv6 add" in line
:
233 return get_normalized_ipv6_line(line
)
238 class Config(object):
240 A frr configuration is stored in a Config object. A Config object
241 contains a dictionary of Context objects where the Context keys
242 ('router ospf' for example) are our dictionary key.
245 def __init__(self
, vtysh
):
247 self
.contexts
= OrderedDict()
250 def load_from_file(self
, filename
):
252 Read configuration from specified file and slurp it into internal memory
253 The internal representation has been marked appropriately by passing it
254 through vtysh with the -m parameter
256 log
.info("Loading Config object from file %s", filename
)
258 file_output
= self
.vtysh
.mark_file(filename
)
260 for line
in file_output
.split("\n"):
263 # Compress duplicate whitespaces
264 line
= " ".join(line
.split())
267 line
= get_normalized_mac_ip_line(line
)
269 # vrf static routes can be added in two ways. The old way is:
271 # "ip route x.x.x.x/x y.y.y.y vrf <vrfname>"
273 # but it's rendered in the configuration as the new way::
276 # ip route x.x.x.x/x y.y.y.y
279 # this difference causes frr-reload to not consider them a
280 # match and delete vrf static routes incorrectly.
281 # fix the old way to match new "show running" output so a
282 # proper match is found.
284 line
.startswith("ip route ") or line
.startswith("ipv6 route ")
285 ) and " vrf " in line
:
286 newline
= line
.split(" ")
287 vrf_index
= newline
.index("vrf")
288 vrf_ctx
= newline
[vrf_index
] + " " + newline
[vrf_index
+ 1]
289 del newline
[vrf_index
: vrf_index
+ 2]
290 newline
= " ".join(newline
)
291 self
.lines
.append(vrf_ctx
)
292 self
.lines
.append(newline
)
293 self
.lines
.append("exit-vrf")
296 self
.lines
.append(line
)
300 def load_from_show_running(self
, daemon
):
302 Read running configuration and slurp it into internal memory
303 The internal representation has been marked appropriately by passing it
304 through vtysh with the -m parameter
306 log
.info("Loading Config object from vtysh show running")
308 config_text
= self
.vtysh
.mark_show_run(daemon
)
310 for line
in config_text
.split("\n"):
314 line
== "Building configuration..."
315 or line
== "Current configuration:"
320 self
.lines
.append(line
)
326 Return the lines read in from the configuration
328 return "\n".join(self
.lines
)
330 def get_contexts(self
):
332 Return the parsed context as strings for display, log etc.
334 for (_
, ctx
) in sorted(iteritems(self
.contexts
)):
337 def save_contexts(self
, key
, lines
):
339 Save the provided key and lines as a context
344 # IP addresses specified in "network" statements, "ip prefix-lists"
345 # etc. can differ in the host part of the specification the user
346 # provides and what the running config displays. For example, user can
347 # specify 11.1.1.1/24, and the running config displays this as
348 # 11.1.1.0/24. Ensure we don't do a needless operation for such lines.
349 # IS-IS & OSPFv3 have no "network" support.
350 re_key_rt
= re
.match(r
"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0])
352 addr
= re_key_rt
.group(2)
355 newaddr
= ip_network(addr
, strict
=False)
356 key
[0] = "%s route %s/%s%s" % (
358 str(newaddr
.network_address
),
365 re_key_rt
= re
.match(
366 r
"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0]
369 addr
= re_key_rt
.group(4)
372 network_addr
= ip_network(addr
, strict
=False)
373 newaddr
= "%s/%s" % (
374 str(network_addr
.network_address
),
375 network_addr
.prefixlen
,
382 legestr
= re_key_rt
.group(5)
383 re_lege
= re
.search(r
"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr
)
385 legestr
= "%sge %s le %s%s" % (
392 key
[0] = "%s prefix-list%s%s %s%s" % (
400 if lines
and key
[0].startswith("router bgp"):
403 re_net
= re
.match(r
"network\s+([A-Fa-f:.0-9/]+)(.*)$", line
)
405 addr
= re_net
.group(1)
406 if "/" not in addr
and key
[0].startswith("router bgp"):
407 # This is most likely an error because with no
408 # prefixlen, BGP treats the prefixlen as 8
412 network_addr
= ip_network(addr
, strict
=False)
413 line
= "network %s/%s %s" % (
414 str(network_addr
.network_address
),
415 network_addr
.prefixlen
,
418 newlines
.append(line
)
420 # Really this should be an error. Whats a network
421 # without an IP Address following it ?
422 newlines
.append(line
)
424 newlines
.append(line
)
427 # More fixups in user specification and what running config shows.
428 # "null0" in routes must be replaced by Null0.
430 key
[0].startswith("ip route")
431 or key
[0].startswith("ipv6 route")
432 and "null0" in key
[0]
434 key
[0] = re
.sub(r
"\s+null0(\s*$)", " Null0", key
[0])
436 if lines
and key
[0].startswith("vrf "):
439 if line
.startswith("ip route ") or line
.startswith("ipv6 route "):
441 line
= re
.sub(r
"\s+null0(\s*$)", " Null0", line
)
442 newlines
.append(line
)
444 newlines
.append(line
)
448 if tuple(key
) not in self
.contexts
:
449 ctx
= Context(tuple(key
), lines
)
450 self
.contexts
[tuple(key
)] = ctx
452 ctx
= self
.contexts
[tuple(key
)]
456 if tuple(key
) not in self
.contexts
:
457 ctx
= Context(tuple(key
), [])
458 self
.contexts
[tuple(key
)] = ctx
460 def load_contexts(self
):
462 Parse the configuration and create contexts for each appropriate block
464 The end of a context is flagged via the 'end' keyword:
473 bgp router-id 10.0.0.1
474 bgp log-neighbor-changes
475 no bgp default ipv4-unicast
476 neighbor EBGP peer-group
477 neighbor EBGP advertisement-interval 1
478 neighbor EBGP timers connect 10
479 neighbor 2001:40:1:4::6 remote-as 40
480 neighbor 2001:40:1:8::a remote-as 40
484 neighbor IBGPv6 activate
485 neighbor 2001:10::2 peer-group IBGPv6
486 neighbor 2001:10::3 peer-group IBGPv6
491 ospf router-id 10.0.0.1
492 log-adjacency-changes detail
493 timers throttle spf 0 50 5000
497 The code assumes that its working on the output from the "vtysh -m"
498 command. That provides the appropriate markers to signify end of
499 a context. This routine uses that to build the contexts for the
502 There are single line contexts such as "log file /media/node/zebra.log"
503 and multi-line contexts such as "router ospf" and subcontexts
504 within a context such as "address-family" within "router bgp"
505 In each of these cases, the first line of the context becomes the
506 key of the context. So "router bgp 10" is the key for the non-address
507 family part of bgp, "router bgp 10, address-family ipv6 unicast" is
508 the key for the subcontext and so on.
510 This dictionary contains a tree of all commands that we know start a
511 new multi-line context. All other commands are treated either as
512 commands inside a multi-line context or as single-line contexts. This
513 dictionary should be updated whenever a new node is added to FRR.
521 "vnc nve-group ": {},
525 "segment-routing srv6": {},
530 "router openfabric ": {},
535 "mpls ldp": {"address-family ": {"interface ": {}}},
536 "l2vpn ": {"member pseudowire ": {}},
537 "key chain ": {"key ": {}},
539 "interface ": {"link-params": {}},
544 "policy ": {"candidate-path ": {}},
545 "pcep": {"pcc": {}, "pce ": {}, "pce-config ": {}},
547 "srv6": {"locators": {"locator ": {}}},
549 "nexthop-group ": {},
553 "bfd": {"peer ": {}, "profile ": {}},
557 # stack of context keys
559 # stack of context keywords
560 cur_ctx_keywords
= [ctx_keywords
]
561 # list of stored commands
564 for line
in self
.lines
:
569 if line
.startswith("!") or line
.startswith("#"):
572 if line
.startswith("exit"):
573 # ignore on top level
574 if len(ctx_keys
) == 0:
577 # save current context
578 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
580 # exit current context
581 log
.debug("LINE %-50s: exit context %-50s", line
, ctx_keys
)
584 cur_ctx_keywords
.pop()
589 if line
.startswith("end"):
591 while len(ctx_keys
) > 0:
592 # save current context
593 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
595 # exit current context
596 log
.debug("LINE %-50s: exit context %-50s", line
, ctx_keys
)
599 cur_ctx_keywords
.pop()
606 # check if the line is a context-entering keyword
607 for k
, v
in cur_ctx_keywords
[-1].items():
608 if line
.startswith(k
):
609 # candidate-path is a special case. It may be a node and
610 # may be a single-line command. The distinguisher is the
611 # word "dynamic" or "explicit" at the middle of the line.
612 # It was perhaps not the best choice by the pathd authors
613 # but we have what we have.
614 if k
== "candidate-path " and "explicit" in line
:
615 # this is a single-line command
618 # save current context
619 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
623 ctx_keys
.append(line
)
624 cur_ctx_keywords
.append(v
)
627 log
.debug("LINE %-50s: enter context %-50s", line
, ctx_keys
)
633 if len(ctx_keys
) == 0:
634 log
.debug("LINE %-50s: single-line context", line
)
635 self
.save_contexts([line
], [])
637 log
.debug("LINE %-50s: add to current context %-50s", line
, ctx_keys
)
638 cur_ctx_lines
.append(line
)
640 # Save the context of the last one
641 if len(ctx_keys
) > 0:
642 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
645 def lines_to_config(ctx_keys
, line
, delete
):
647 Return the command as it would appear in frr.conf
652 for (i
, ctx_key
) in enumerate(ctx_keys
):
653 cmd
.append(" " * i
+ ctx_key
)
656 indent
= len(ctx_keys
) * " "
658 # There are some commands that are on by default so their "no" form will be
659 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
660 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
661 # not by doing a "no no bgp default ipv4-unicast"
663 if line
.startswith("no "):
664 cmd
.append("%s%s" % (indent
, line
[3:]))
666 cmd
.append("%sno %s" % (indent
, line
))
669 cmd
.append(indent
+ line
)
671 # If line is None then we are typically deleting an entire
672 # context ('no router ospf' for example)
674 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
675 cmd
.append("%s%s" % (" " * i
, ctx_key
))
677 # Only put the 'no' on the last sub-context
679 if ctx_keys
[-1].startswith("no "):
680 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
682 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
684 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
689 def get_normalized_ipv6_line(line
):
691 Return a normalized IPv6 line as produced by frr,
692 with all letters in lower case and trailing and leading
693 zeros removed, and only the network portion present if
694 the IPv6 word is a network
697 words
= line
.split(" ")
703 v6word
= ip_network(word
, strict
=False)
704 norm_word
= "%s/%s" % (
705 str(v6word
.network_address
),
712 norm_word
= "%s" % IPv6Address(word
)
717 norm_line
= norm_line
+ " " + norm_word
719 return norm_line
.strip()
722 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
723 for (ctx_keys
, line
) in lines
:
724 if ctx_keys
== target_ctx_keys
:
726 if line
== target_line
:
729 if line
.startswith(target_line
):
734 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
736 # exit-vrf is a bit tricky. If the new config is missing it but we
737 # have configs under a vrf, we need to add it at the end to do the
738 # right context changes. If exit-vrf exists in both the running and
739 # new config, we cannot delete it or it will break context changes.
743 for (ctx_keys
, line
) in lines_to_add
:
744 if add_exit_vrf
== True:
745 if ctx_keys
[0] != prior_ctx_key
:
746 insert_key
= ((prior_ctx_key
),)
747 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
750 if ctx_keys
[0].startswith("vrf") and line
:
751 if line
!= "exit-vrf":
753 prior_ctx_key
= ctx_keys
[0]
758 for (ctx_keys
, line
) in lines_to_del
:
759 if line
== "exit-vrf":
760 if line_exist(lines_to_add
, ctx_keys
, line
):
761 lines_to_del
.remove((ctx_keys
, line
))
763 return (lines_to_add
, lines_to_del
)
766 def bgp_delete_inst_move_line(lines_to_del
):
767 # Deletion of bgp default inst followed by
768 # bgp vrf inst leads to issue of default
769 # instance can not be removed.
770 # Move the bgp default instance line to end.
771 bgp_defult_inst
= False
774 for (ctx_keys
, line
) in lines_to_del
:
775 # Find bgp default inst
777 ctx_keys
[0].startswith("router bgp")
779 and "vrf" not in ctx_keys
[0]
781 bgp_defult_inst
= True
783 if ctx_keys
[0].startswith("router bgp") and not line
and "vrf" in ctx_keys
[0]:
786 if bgp_defult_inst
and bgp_vrf_inst
:
787 for (ctx_keys
, line
) in lines_to_del
:
788 # move bgp default inst to end
790 ctx_keys
[0].startswith("router bgp")
792 and "vrf" not in ctx_keys
[0]
794 lines_to_del
.remove((ctx_keys
, line
))
795 lines_to_del
.append((ctx_keys
, line
))
798 def bgp_delete_nbr_remote_as_line(lines_to_add
):
799 # Handle deletion of neighbor <nbr> remote-as line from
800 # lines_to_add if the nbr is configured with peer-group and
801 # peer-group has remote-as config present.
802 # 'neighbor <nbr> remote-as change on peer is not allowed
803 # if the peer is part of peer-group and peer-group has
807 # Find all peer-group commands; create dict of each peer-group
808 # to store assoicated neighbor as value
809 for ctx_keys
, line
in lines_to_add
:
811 ctx_keys
[0].startswith("router bgp")
813 and line
.startswith("neighbor ")
815 # {'router bgp 65001': {'PG': [], 'PG1': []},
816 # 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}}
817 if ctx_keys
[0] not in pg_dict
:
818 pg_dict
[ctx_keys
[0]] = dict()
819 # find 'neighbor <pg_name> peer-group'
820 re_pg
= re
.match("neighbor (\S+) peer-group$", line
)
821 if re_pg
and re_pg
.group(1) not in pg_dict
[ctx_keys
[0]]:
822 pg_dict
[ctx_keys
[0]][re_pg
.group(1)] = {
828 # Find peer-group with remote-as command, also search neighbor
829 # associated to peer-group and store into peer-group dict
830 for ctx_keys
, line
in lines_to_add
:
832 ctx_keys
[0].startswith("router bgp")
834 and line
.startswith("neighbor ")
836 if ctx_keys
[0] in pg_dict
:
837 for pg_key
in pg_dict
[ctx_keys
[0]]:
838 # Find 'neighbor <pg_name> remote-as'
839 pg_rmtas
= "neighbor %s remote-as (\S+)" % pg_key
840 re_pg_rmtas
= re
.search(pg_rmtas
, line
)
842 pg_dict
[ctx_keys
[0]][pg_key
]["remoteas"] = True
844 # Find 'neighbor <peer> [interface] peer-group <pg_name>'
845 nb_pg
= "neighbor (\S+) peer-group %s$" % pg_key
846 re_nbr_pg
= re
.search(nb_pg
, line
)
849 and re_nbr_pg
.group(1) not in pg_dict
[ctx_keys
[0]][pg_key
]
851 pg_dict
[ctx_keys
[0]][pg_key
]["nbr"].append(re_nbr_pg
.group(1))
853 # Find any neighbor <nbr> remote-as config line check if the nbr
854 # is in the peer group's list of nbrs. Remove 'neighbor <nbr> remote-as <>'
856 lines_to_del_from_add
= []
857 for ctx_keys
, line
in lines_to_add
:
859 ctx_keys
[0].startswith("router bgp")
861 and line
.startswith("neighbor ")
863 nbr_rmtas
= "neighbor (\S+) remote-as.*"
864 re_nbr_rmtas
= re
.search(nbr_rmtas
, line
)
865 if re_nbr_rmtas
and ctx_keys
[0] in pg_dict
:
866 for pg
in pg_dict
[ctx_keys
[0]]:
867 if pg_dict
[ctx_keys
[0]][pg
]["remoteas"] == True:
868 for nbr
in pg_dict
[ctx_keys
[0]][pg
]["nbr"]:
869 if re_nbr_rmtas
.group(1) in nbr
:
870 lines_to_del_from_add
.append((ctx_keys
, line
))
872 for ctx_keys
, line
in lines_to_del_from_add
:
873 lines_to_add
.remove((ctx_keys
, line
))
876 def bgp_remove_neighbor_cfg(lines_to_del
, del_nbr_dict
):
878 # This method handles deletion of bgp neighbor configs,
879 # if there is neighbor to peer-group cmd is in delete list.
880 # As 'no neighbor .* peer-group' deletes the neighbor,
881 # subsequent neighbor speciic config line deletion results
883 lines_to_del_to_del
= []
885 for (ctx_keys
, line
) in lines_to_del
:
887 ctx_keys
[0].startswith("router bgp")
889 and line
.startswith("neighbor ")
891 if ctx_keys
[0] in del_nbr_dict
:
892 for nbr
in del_nbr_dict
[ctx_keys
[0]]:
893 re_nbr_pg
= re
.search('neighbor (\S+) .*peer-group (\S+)', line
)
894 nb_exp
= "neighbor %s .*" % nbr
896 re_nb
= re
.search(nb_exp
, line
)
898 lines_to_del_to_del
.append((ctx_keys
, line
))
900 for (ctx_keys
, line
) in lines_to_del_to_del
:
901 lines_to_del
.remove((ctx_keys
, line
))
904 def delete_move_lines(lines_to_add
, lines_to_del
):
905 # This method handles deletion of bgp peer group config.
906 # The objective is to delete config lines related to peers
907 # associated with the peer-group and move the peer-group
908 # config line to the end of the lines_to_del list.
910 bgp_delete_nbr_remote_as_line(lines_to_add
)
913 del_nbr_dict
= dict()
914 # Stores the lines to move to the end of the pending list.
915 lines_to_del_to_del
= []
916 # Stores the lines to move to end of the pending list.
917 lines_to_del_to_app
= []
918 found_pg_del_cmd
= False
920 # When "neighbor <pg_name> peer-group" under a bgp instance is removed,
921 # it also deletes the associated peer config. Any config line below no form of
922 # peer-group related to a peer are errored out as the peer no longer exists.
923 # To cleanup peer-group and associated peer(s) configs:
924 # - Remove all the peers config lines from the pending list (lines_to_del list).
925 # - Move peer-group deletion line to the end of the pending list, to allow
926 # removal of any of the peer-group specific configs.
928 # Create a dictionary of config context (i.e. router bgp vrf x).
929 # Under each context node, create a dictionary of a peer-group name.
930 # Append a peer associated to the peer-group into a list under a peer-group node.
931 # Remove all of the peer associated config lines from the pending list.
932 # Append peer-group deletion line to end of the pending list.
935 # neighbor underlay peer-group
936 # neighbor underlay remote-as external
937 # neighbor underlay advertisement-interval 0
938 # neighbor underlay timers 3 9
939 # neighbor underlay timers connect 10
940 # neighbor swp1 interface peer-group underlay
941 # neighbor swp1 advertisement-interval 0
942 # neighbor swp1 timers 3 9
943 # neighbor swp1 timers connect 10
944 # neighbor swp2 interface peer-group underlay
945 # neighbor swp2 advertisement-interval 0
946 # neighbor swp2 timers 3 9
947 # neighbor swp2 timers connect 10
948 # neighbor swp3 interface peer-group underlay
949 # neighbor uplink1 interface remote-as internal
950 # neighbor uplink1 advertisement-interval 0
951 # neighbor uplink1 timers 3 9
952 # neighbor uplink1 timers connect 10
955 # "router bgp 200 no bgp bestpath as-path multipath-relax"
956 # "router bgp 200 no neighbor underlay advertisement-interval 0"
957 # "router bgp 200 no neighbor underlay timers 3 9"
958 # "router bgp 200 no neighbor underlay timers connect 10"
959 # "router bgp 200 no neighbor uplink1 advertisement-interval 0"
960 # "router bgp 200 no neighbor uplink1 timers 3 9"
961 # "router bgp 200 no neighbor uplink1 timers connect 10"
962 # "router bgp 200 no neighbor underlay remote-as external"
963 # "router bgp 200 no neighbor uplink1 interface remote-as internal"
964 # "router bgp 200 no neighbor underlay peer-group"
966 for (ctx_keys
, line
) in lines_to_del
:
968 ctx_keys
[0].startswith("router bgp")
970 and line
.startswith("neighbor ")
972 # When 'neighbor <peer> remote-as <>' is removed it deletes the peer,
973 # there might be a peer associated config which also needs to be removed
975 # Append the 'neighbor <peer> remote-as <>' to the lines_to_del.
978 # neighbor uplink1 interface remote-as internal
979 # neighbor uplink1 advertisement-interval 0
980 # neighbor uplink1 timers 3 9
981 # neighbor uplink1 timers connect 10
984 # neighbor uplink1 advertisement-interval 0
985 # neighbor uplink1 timers 3 9
986 # neighbor uplink1 timers connect 10
989 # neighbor uplink1 interface remote-as internal
991 # 'no neighbor peer [interface] remote-as <>'
992 nb_remoteas
= "neighbor (\S+) .*remote-as (\S+)"
993 re_nb_remoteas
= re
.search(nb_remoteas
, line
)
995 lines_to_del_to_app
.append((ctx_keys
, line
))
997 # 'no neighbor peer [interface] peer-group <>' is in lines_to_del
998 # copy the neighbor and look for all config removal lines associated
999 # to neighbor and delete them from the lines_to_del
1000 re_nbr_pg
= re
.search('neighbor (\S+) .*peer-group (\S+)', line
)
1002 if ctx_keys
[0] not in del_nbr_dict
:
1003 del_nbr_dict
[ctx_keys
[0]] = list()
1004 if re_nbr_pg
.group(1) not in del_nbr_dict
[ctx_keys
[0]]:
1005 del_nbr_dict
[ctx_keys
[0]].append(re_nbr_pg
.group(1))
1007 # {'router bgp 65001': {'PG': [], 'PG1': []},
1008 # 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}}
1009 if ctx_keys
[0] not in del_dict
:
1010 del_dict
[ctx_keys
[0]] = dict()
1011 # find 'no neighbor <pg_name> peer-group'
1012 re_pg
= re
.match("neighbor (\S+) peer-group$", line
)
1013 if re_pg
and re_pg
.group(1) not in del_dict
[ctx_keys
[0]]:
1014 del_dict
[ctx_keys
[0]][re_pg
.group(1)] = list()
1015 found_pg_del_cmd
= True
1017 if found_pg_del_cmd
== False:
1018 bgp_delete_inst_move_line(lines_to_del
)
1020 bgp_remove_neighbor_cfg(lines_to_del
, del_nbr_dict
)
1021 return (lines_to_add
, lines_to_del
)
1023 for (ctx_keys
, line
) in lines_to_del_to_app
:
1024 lines_to_del
.remove((ctx_keys
, line
))
1025 lines_to_del
.append((ctx_keys
, line
))
1027 # {'router bgp 65001': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']},
1028 # 'router bgp 65001 vrf vrf1': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}}
1029 for (ctx_keys
, line
) in lines_to_del
:
1031 ctx_keys
[0].startswith("router bgp")
1033 and line
.startswith("neighbor ")
1035 if ctx_keys
[0] in del_dict
:
1036 for pg_key
in del_dict
[ctx_keys
[0]]:
1037 # 'neighbor <peer> [interface] peer-group <pg_name>'
1038 nb_pg
= "neighbor (\S+) .*peer-group %s$" % pg_key
1039 re_nbr_pg
= re
.search(nb_pg
, line
)
1042 and re_nbr_pg
.group(1) not in del_dict
[ctx_keys
[0]][pg_key
]
1044 del_dict
[ctx_keys
[0]][pg_key
].append(re_nbr_pg
.group(1))
1046 lines_to_del_to_app
= []
1047 for (ctx_keys
, line
) in lines_to_del
:
1049 ctx_keys
[0].startswith("router bgp")
1051 and line
.startswith("neighbor ")
1053 if ctx_keys
[0] in del_dict
:
1054 for pg
in del_dict
[ctx_keys
[0]]:
1055 for nbr
in del_dict
[ctx_keys
[0]][pg
]:
1056 nb_exp
= "neighbor %s .*" % nbr
1057 re_nb
= re
.search(nb_exp
, line
)
1058 # add peer configs to delete list.
1059 if re_nb
and line
not in lines_to_del_to_del
:
1060 lines_to_del_to_del
.append((ctx_keys
, line
))
1062 pg_exp
= "neighbor %s peer-group$" % pg
1063 re_pg
= re
.match(pg_exp
, line
)
1065 lines_to_del_to_app
.append((ctx_keys
, line
))
1067 for (ctx_keys
, line
) in lines_to_del_to_del
:
1068 lines_to_del
.remove((ctx_keys
, line
))
1070 for (ctx_keys
, line
) in lines_to_del_to_app
:
1071 lines_to_del
.remove((ctx_keys
, line
))
1072 lines_to_del
.append((ctx_keys
, line
))
1074 bgp_delete_inst_move_line(lines_to_del
)
1076 return (lines_to_add
, lines_to_del
)
1079 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1081 # Quite possibly the most confusing (while accurate) variable names in history
1082 lines_to_add_to_del
= []
1083 lines_to_del_to_del
= []
1085 for (ctx_keys
, line
) in lines_to_del
:
1088 # If there is a change in the segment routing block ranges, do it
1089 # in-place, to avoid requesting spurious label chunks which might fail
1090 if line
and "segment-routing global-block" in line
:
1091 for (add_key
, add_line
) in lines_to_add
:
1093 ctx_keys
[0] == add_key
[0]
1095 and "segment-routing global-block" in add_line
1097 lines_to_del_to_del
.append((ctx_keys
, line
))
1101 if ctx_keys
[0].startswith("router bgp") and line
:
1103 if line
.startswith("neighbor "):
1104 # BGP changed how it displays swpX peers that are part of peer-group. Older
1105 # versions of frr would display these on separate lines:
1106 # neighbor swp1 interface
1107 # neighbor swp1 peer-group FOO
1109 # but today we display via a single line
1110 # neighbor swp1 interface peer-group FOO
1112 # This change confuses frr-reload.py so check to see if we are deleting
1113 # neighbor swp1 interface peer-group FOO
1116 # neighbor swp1 interface
1117 # neighbor swp1 peer-group FOO
1119 # If so then chop the del line and the corresponding add lines
1120 re_swpx_int_peergroup
= re
.search(
1121 "neighbor (\S+) interface peer-group (\S+)", line
1123 re_swpx_int_v6only_peergroup
= re
.search(
1124 "neighbor (\S+) interface v6only peer-group (\S+)", line
1127 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1128 swpx_interface
= None
1129 swpx_peergroup
= None
1131 if re_swpx_int_peergroup
:
1132 swpx
= re_swpx_int_peergroup
.group(1)
1133 peergroup
= re_swpx_int_peergroup
.group(2)
1134 swpx_interface
= "neighbor %s interface" % swpx
1135 elif re_swpx_int_v6only_peergroup
:
1136 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1137 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1138 swpx_interface
= "neighbor %s interface v6only" % swpx
1140 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1141 found_add_swpx_interface
= line_exist(
1142 lines_to_add
, ctx_keys
, swpx_interface
1144 found_add_swpx_peergroup
= line_exist(
1145 lines_to_add
, ctx_keys
, swpx_peergroup
1147 tmp_ctx_keys
= tuple(list(ctx_keys
))
1149 if not found_add_swpx_peergroup
:
1150 tmp_ctx_keys
= list(ctx_keys
)
1151 tmp_ctx_keys
.append("address-family ipv4 unicast")
1152 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1153 found_add_swpx_peergroup
= line_exist(
1154 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1157 if not found_add_swpx_peergroup
:
1158 tmp_ctx_keys
= list(ctx_keys
)
1159 tmp_ctx_keys
.append("address-family ipv6 unicast")
1160 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1161 found_add_swpx_peergroup
= line_exist(
1162 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1165 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1167 lines_to_del_to_del
.append((ctx_keys
, line
))
1168 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1169 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1171 # Changing the bfd timers on neighbors is allowed without doing
1172 # a delete/add process. Since doing a "no neighbor blah bfd
1173 # ..." will cause the peer to bounce unnecessarily, just skip
1174 # the delete and just do the add.
1175 re_nbr_bfd_timers
= re
.search(
1176 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1179 if re_nbr_bfd_timers
:
1180 nbr
= re_nbr_bfd_timers
.group(1)
1181 bfd_nbr
= "neighbor %s" % nbr
1182 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1184 for (ctx_keys
, add_line
) in lines_to_add
:
1185 if ctx_keys
[0].startswith("router bgp"):
1186 re_add_nbr_bfd_timers
= re
.search(
1187 bfd_search_string
, add_line
1190 if re_add_nbr_bfd_timers
:
1191 found_add_bfd_nbr
= line_exist(
1192 lines_to_add
, ctx_keys
, bfd_nbr
, False
1195 if found_add_bfd_nbr
:
1196 lines_to_del_to_del
.append((ctx_keys
, line
))
1198 # Neighbor changes of route-maps need to be accounted for in
1199 # that we do not want to do a `no route-map...` `route-map
1200 # ....` when changing a route-map. This is bad mojo as that we
1201 # will send/receive data we don't want. Additionally we need
1202 # to ensure that if we have different afi/safi variants that
1203 # they actually match and if we are going from a very old style
1204 # command such that the neighbor command is under the `router
1205 # bgp ..` node that we need to handle that appropriately
1206 re_nbr_rm
= re
.search("neighbor(.*)route-map(.*)(in|out)$", line
)
1208 adjust_for_bgp_node
= 0
1209 neighbor_name
= re_nbr_rm
.group(1)
1210 rm_name_del
= re_nbr_rm
.group(2)
1211 dir = re_nbr_rm
.group(3)
1212 search
= "neighbor%sroute-map(.*)%s" % (neighbor_name
, dir)
1214 for (ctx_keys_al
, add_line
) in lines_to_add
:
1215 if ctx_keys_al
[0].startswith("router bgp"):
1217 rm_match
= re
.search(search
, add_line
)
1219 rm_name_add
= rm_match
.group(1)
1220 if rm_name_add
== rm_name_del
:
1222 if len(ctx_keys_al
) == 1:
1224 adjust_for_bgp_node
= 1
1228 and len(ctx_keys_al
) > 1
1229 and ctx_keys
[1] == ctx_keys_al
[1]
1231 lines_to_del_to_del
.append((ctx_keys_al
, line
))
1233 if adjust_for_bgp_node
== 1:
1234 for (ctx_keys_dl
, dl_line
) in lines_to_del
:
1236 ctx_keys_dl
[0].startswith("router bgp")
1237 and len(ctx_keys_dl
) > 1
1238 and ctx_keys_dl
[1] == "address-family ipv4 unicast"
1240 if save_line
== dl_line
:
1241 lines_to_del_to_del
.append((ctx_keys_dl
, save_line
))
1243 # We changed how we display the neighbor interface command. Older
1244 # versions of frr would display the following:
1245 # neighbor swp1 interface
1246 # neighbor swp1 remote-as external
1247 # neighbor swp1 capability extended-nexthop
1249 # but today we display via a single line
1250 # neighbor swp1 interface remote-as external
1252 # and capability extended-nexthop is no longer needed because we
1253 # automatically enable it when the neighbor is of type interface.
1255 # This change confuses frr-reload.py so check to see if we are deleting
1256 # neighbor swp1 interface remote-as (external|internal|ASNUM)
1259 # neighbor swp1 interface
1260 # neighbor swp1 remote-as (external|internal|ASNUM)
1261 # neighbor swp1 capability extended-nexthop
1263 # If so then chop the del line and the corresponding add lines
1264 re_swpx_int_remoteas
= re
.search(
1265 "neighbor (\S+) interface remote-as (\S+)", line
1267 re_swpx_int_v6only_remoteas
= re
.search(
1268 "neighbor (\S+) interface v6only remote-as (\S+)", line
1271 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1272 swpx_interface
= None
1273 swpx_remoteas
= None
1275 if re_swpx_int_remoteas
:
1276 swpx
= re_swpx_int_remoteas
.group(1)
1277 remoteas
= re_swpx_int_remoteas
.group(2)
1278 swpx_interface
= "neighbor %s interface" % swpx
1279 elif re_swpx_int_v6only_remoteas
:
1280 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1281 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1282 swpx_interface
= "neighbor %s interface v6only" % swpx
1284 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1285 found_add_swpx_interface
= line_exist(
1286 lines_to_add
, ctx_keys
, swpx_interface
1288 found_add_swpx_remoteas
= line_exist(
1289 lines_to_add
, ctx_keys
, swpx_remoteas
1291 tmp_ctx_keys
= tuple(list(ctx_keys
))
1293 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1295 lines_to_del_to_del
.append((ctx_keys
, line
))
1296 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1297 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1299 # We made the 'bgp bestpath as-path multipath-relax' command
1300 # automatically assume 'no-as-set' since the lack of this option
1301 # caused weird routing problems. When the running config is shown
1302 # in releases with this change, the no-as-set keyword is not shown
1303 # as it is the default. This causes frr-reload to unnecessarily
1304 # unapply this option only to apply it back again, causing
1305 # unnecessary session resets.
1306 if "multipath-relax" in line
:
1307 re_asrelax_new
= re
.search(
1308 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1310 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1311 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1313 if re_asrelax_new
and found_asrelax_old
:
1315 lines_to_del_to_del
.append((ctx_keys
, line
))
1316 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1318 # If we are modifying the BGP table-map we need to avoid a del/add
1319 # and instead modify the table-map in place via an add. This is
1320 # needed to avoid installing all routes in the RIB the second the
1321 # 'no table-map' is issued.
1322 if line
.startswith("table-map"):
1323 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1326 lines_to_del_to_del
.append((ctx_keys
, line
))
1328 # More old-to-new config handling. ip import-table no longer accepts
1329 # distance, but we honor the old syntax. But 'show running' shows only
1330 # the new syntax. This causes an unnecessary 'no import-table' followed
1331 # by the same old 'ip import-table' which causes perturbations in
1332 # announced routes leading to traffic blackholes. Fix this issue.
1333 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1335 table_num
= re_importtbl
.group(1)
1336 for ctx
in lines_to_add
:
1337 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1338 lines_to_del_to_del
.append(
1339 (("ip import-table %s" % table_num
,), None)
1341 lines_to_add_to_del
.append((ctx
[0], None))
1343 # ip/ipv6 prefix-lists and access-lists can be specified without a seq
1344 # number. However, the running config always adds 'seq x', where x is
1345 # a number incremented by 5 for every element of the prefix/access
1346 # list. So, ignore such lines as well. Sample prefix-list and
1348 # ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1349 # ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1350 # ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1351 # access-list FOO seq 5 permit 2.2.2.2/32
1352 # ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1353 re_acl_pfxlst
= re
.search(
1354 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1360 re_acl_pfxlst
.group(1)
1361 + re_acl_pfxlst
.group(2)
1362 + re_acl_pfxlst
.group(3)
1363 + re_acl_pfxlst
.group(5)
1364 + re_acl_pfxlst
.group(6)
1366 for ctx
in lines_to_add
:
1367 if ctx
[0][0] == tmpline
:
1368 lines_to_del_to_del
.append((ctx_keys
, None))
1369 lines_to_add_to_del
.append(((tmpline
,), None))
1371 # If prefix-lists or access-lists are being deleted and not added
1372 # (see comment above), add command with 'no' to lines_to_add and
1373 # remove from lines_to_del to improve scaling performance.
1375 add_cmd
= ("no " + ctx_keys
[0],)
1376 lines_to_add
.append((add_cmd
, None))
1377 lines_to_del_to_del
.append((ctx_keys
, None))
1381 and ctx_keys
[0].startswith("router bgp")
1382 and ctx_keys
[1] == "address-family l2vpn evpn"
1383 and ctx_keys
[2].startswith("vni")
1387 re
.search("^route-target import (.*)$", line
)
1393 rt
= re_route_target
.group(1).strip()
1394 route_target_import_line
= line
1395 route_target_export_line
= "route-target export %s" % rt
1396 route_target_both_line
= "route-target both %s" % rt
1398 found_route_target_export_line
= line_exist(
1399 lines_to_del
, ctx_keys
, route_target_export_line
1401 found_route_target_both_line
= line_exist(
1402 lines_to_add
, ctx_keys
, route_target_both_line
1405 # If the running configs has
1406 # route-target import 1:1
1407 # route-target export 1:1
1408 # and the config we are reloading against has
1409 # route-target both 1:1
1410 # then we can ignore deleting the import/export and ignore adding the 'both'
1411 if found_route_target_export_line
and found_route_target_both_line
:
1412 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1413 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1414 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1416 # Deleting static routes under a vrf can lead to time-outs if each is sent
1417 # as separate vtysh -c commands. Change them from being in lines_to_del and
1418 # put the "no" form in lines_to_add
1419 if ctx_keys
[0].startswith("vrf ") and line
:
1420 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1421 add_cmd
= "no " + line
1422 lines_to_add
.append((ctx_keys
, add_cmd
))
1423 lines_to_del_to_del
.append((ctx_keys
, line
))
1426 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1429 lines_to_del_to_del
.append((ctx_keys
, line
))
1430 lines_to_add_to_del
.append((ctx_keys
, line
))
1432 # We have commands that used to be displayed in the global part
1433 # of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1437 # neighbor ISL advertisement-interval 0
1443 # address-family ipv4 unicast
1444 # neighbor ISL advertisement-interval 0
1446 # Look to see if we are deleting it in one format just to add it back in the other
1448 ctx_keys
[0].startswith("router bgp")
1449 and len(ctx_keys
) > 1
1450 and ctx_keys
[1] == "address-family ipv4 unicast"
1452 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1453 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1455 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1458 lines_to_del_to_del
.append((ctx_keys
, line
))
1459 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1461 for (ctx_keys
, line
) in lines_to_del_to_del
:
1462 lines_to_del
.remove((ctx_keys
, line
))
1464 for (ctx_keys
, line
) in lines_to_add_to_del
:
1465 lines_to_add
.remove((ctx_keys
, line
))
1467 return (lines_to_add
, lines_to_del
)
1470 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1472 There are certain commands that cannot be removed. Remove
1473 those commands from lines_to_del.
1475 lines_to_del_to_del
= []
1477 for (ctx_keys
, line
) in lines_to_del
:
1479 # The integrated-vtysh-config one is technically "no"able but if we did
1480 # so frr-reload would stop working so do not let the user shoot
1481 # themselves in the foot by removing this.
1484 ctx_keys
[0].startswith(x
)
1491 "service integrated-vtysh-config",
1495 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1496 lines_to_del_to_del
.append((ctx_keys
, line
))
1498 for (ctx_keys
, line
) in lines_to_del_to_del
:
1499 lines_to_del
.remove((ctx_keys
, line
))
1501 return (lines_to_add
, lines_to_del
)
1504 def compare_context_objects(newconf
, running
):
1506 Create a context diff for the two specified contexts
1509 # Compare the two Config objects to find the lines that we need to add/del
1516 candidates_to_add
= []
1519 # Find contexts that are in newconf but not in running
1520 # Find contexts that are in running but not in newconf
1521 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1523 if running_ctx_keys
not in newconf
.contexts
:
1525 # We check that the len is 1 here so that we only look at ('router bgp 10')
1526 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1527 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1528 # running but not in newconf.
1529 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1531 lines_to_del
.append((running_ctx_keys
, None))
1533 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1534 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1536 ].startswith("vrf"):
1537 for line
in running_ctx
.lines
:
1538 lines_to_del
.append((running_ctx_keys
, line
))
1540 # If this is an address-family under 'router bgp' and we are already deleting the
1541 # entire 'router bgp' context then ignore this sub-context
1543 "router bgp" in running_ctx_keys
[0]
1544 and len(running_ctx_keys
) > 1
1549 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1551 "router bgp" in running_ctx_keys
[0]
1552 and len(running_ctx_keys
) > 2
1553 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1554 and running_ctx_keys
[2].startswith("vni ")
1556 lines_to_del
.append((running_ctx_keys
, None))
1559 "router bgp" in running_ctx_keys
[0]
1560 and len(running_ctx_keys
) > 1
1561 and running_ctx_keys
[1].startswith("address-family")
1563 # There's no 'no address-family' support and so we have to
1564 # delete each line individually again
1565 for line
in running_ctx
.lines
:
1566 lines_to_del
.append((running_ctx_keys
, line
))
1568 # Some commands can happen at higher counts that make
1569 # doing vtysh -c inefficient (and can time out.) For
1570 # these commands, instead of adding them to lines_to_del,
1571 # add the "no " version to lines_to_add.
1572 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1574 ].startswith("ipv6 route"):
1575 add_cmd
= ("no " + running_ctx_keys
[0],)
1576 lines_to_add
.append((add_cmd
, None))
1578 # if this an interface sub-subcontext in an address-family block in ldpd and
1579 # we are already deleting the whole context, then ignore this
1581 len(running_ctx_keys
) > 2
1582 and running_ctx_keys
[0].startswith("mpls ldp")
1583 and running_ctx_keys
[1].startswith("address-family")
1584 and (running_ctx_keys
[:2], None) in lines_to_del
1588 # same thing for a pseudowire sub-context inside an l2vpn context
1590 len(running_ctx_keys
) > 1
1591 and running_ctx_keys
[0].startswith("l2vpn")
1592 and running_ctx_keys
[1].startswith("member pseudowire")
1593 and (running_ctx_keys
[:1], None) in lines_to_del
1597 # Segment routing and traffic engineering never need to be deleted
1599 running_ctx_keys
[0].startswith("segment-routing")
1600 and len(running_ctx_keys
) < 3
1604 # Neither the pcep command
1606 len(running_ctx_keys
) == 3
1607 and running_ctx_keys
[0].startswith("segment-routing")
1608 and running_ctx_keys
[2].startswith("pcep")
1612 # Segment lists can only be deleted after we removed all the candidate paths that
1613 # use them, so add them to a separate array that is going to be appended at the end
1615 len(running_ctx_keys
) == 3
1616 and running_ctx_keys
[0].startswith("segment-routing")
1617 and running_ctx_keys
[2].startswith("segment-list")
1619 seglist_to_del
.append((running_ctx_keys
, None))
1621 # Policies must be deleted after there candidate path, to be sure
1622 # we add them to a separate array that is going to be appended at the end
1624 len(running_ctx_keys
) == 3
1625 and running_ctx_keys
[0].startswith("segment-routing")
1626 and running_ctx_keys
[2].startswith("policy")
1628 pollist_to_del
.append((running_ctx_keys
, None))
1630 # pce-config must be deleted after the pce, to be sure we add them
1631 # to a separate array that is going to be appended at the end
1633 len(running_ctx_keys
) >= 4
1634 and running_ctx_keys
[0].startswith("segment-routing")
1635 and running_ctx_keys
[3].startswith("pce-config")
1637 pceconf_to_del
.append((running_ctx_keys
, None))
1639 # pcc must be deleted after the pce and pce-config too
1641 len(running_ctx_keys
) >= 4
1642 and running_ctx_keys
[0].startswith("segment-routing")
1643 and running_ctx_keys
[3].startswith("pcc")
1645 pcclist_to_del
.append((running_ctx_keys
, None))
1647 # Non-global context
1648 elif running_ctx_keys
and not any(
1649 "address-family" in key
for key
in running_ctx_keys
1651 lines_to_del
.append((running_ctx_keys
, None))
1653 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1654 lines_to_del
.append((running_ctx_keys
, None))
1658 for line
in running_ctx
.lines
:
1659 lines_to_del
.append((running_ctx_keys
, line
))
1661 # if we have some policies commands to delete, append them to lines_to_del
1662 if len(pollist_to_del
) > 0:
1663 lines_to_del
.extend(pollist_to_del
)
1665 # if we have some segment list commands to delete, append them to lines_to_del
1666 if len(seglist_to_del
) > 0:
1667 lines_to_del
.extend(seglist_to_del
)
1669 # if we have some pce list commands to delete, append them to lines_to_del
1670 if len(pceconf_to_del
) > 0:
1671 lines_to_del
.extend(pceconf_to_del
)
1673 # if we have some pcc list commands to delete, append them to lines_to_del
1674 if len(pcclist_to_del
) > 0:
1675 lines_to_del
.extend(pcclist_to_del
)
1677 # Find the lines within each context to add
1678 # Find the lines within each context to del
1679 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1681 if newconf_ctx_keys
in running
.contexts
:
1682 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1684 for line
in newconf_ctx
.lines
:
1685 if line
not in running_ctx
.dlines
:
1687 # candidate paths can only be added after the policy and segment list,
1688 # so add them to a separate array that is going to be appended at the end
1690 len(newconf_ctx_keys
) == 3
1691 and newconf_ctx_keys
[0].startswith("segment-routing")
1692 and newconf_ctx_keys
[2].startswith("policy ")
1693 and line
.startswith("candidate-path ")
1695 candidates_to_add
.append((newconf_ctx_keys
, line
))
1698 lines_to_add
.append((newconf_ctx_keys
, line
))
1700 for line
in running_ctx
.lines
:
1701 if line
not in newconf_ctx
.dlines
:
1702 lines_to_del
.append((newconf_ctx_keys
, line
))
1704 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1706 if newconf_ctx_keys
not in running
.contexts
:
1708 # candidate paths can only be added after the policy and segment list,
1709 # so add them to a separate array that is going to be appended at the end
1711 len(newconf_ctx_keys
) == 4
1712 and newconf_ctx_keys
[0].startswith("segment-routing")
1713 and newconf_ctx_keys
[3].startswith("candidate-path")
1715 candidates_to_add
.append((newconf_ctx_keys
, None))
1716 for line
in newconf_ctx
.lines
:
1717 candidates_to_add
.append((newconf_ctx_keys
, line
))
1720 lines_to_add
.append((newconf_ctx_keys
, None))
1722 for line
in newconf_ctx
.lines
:
1723 lines_to_add
.append((newconf_ctx_keys
, line
))
1725 # if we have some candidate paths commands to add, append them to lines_to_add
1726 if len(candidates_to_add
) > 0:
1727 lines_to_add
.extend(candidates_to_add
)
1729 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1730 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1731 lines_to_add
, lines_to_del
1733 (lines_to_add
, lines_to_del
) = delete_move_lines(lines_to_add
, lines_to_del
)
1734 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1735 lines_to_add
, lines_to_del
1738 return (lines_to_add
, lines_to_del
)
1741 if __name__
== "__main__":
1742 # Command line options
1743 parser
= argparse
.ArgumentParser(
1744 description
="Dynamically apply diff in frr configs"
1746 parser
.add_argument(
1747 "--input", help='Read running config from file instead of "show running"'
1749 group
= parser
.add_mutually_exclusive_group(required
=True)
1751 "--reload", action
="store_true", help="Apply the deltas", default
=False
1754 "--test", action
="store_true", help="Show the deltas", default
=False
1756 level_group
= parser
.add_mutually_exclusive_group()
1757 level_group
.add_argument(
1759 action
="store_true",
1760 help="Enable debugs (synonym for --log-level=debug)",
1763 level_group
.add_argument(
1767 choices
=("critical", "error", "warning", "info", "debug"),
1769 parser
.add_argument(
1770 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1772 parser
.add_argument(
1776 help="Reload specified path/namespace",
1779 parser
.add_argument("filename", help="Location of new frr config file")
1780 parser
.add_argument(
1782 action
="store_true",
1783 help="Overwrite frr.conf with running config output",
1786 parser
.add_argument(
1787 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1789 parser
.add_argument(
1790 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1792 parser
.add_argument(
1793 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1795 parser
.add_argument(
1797 help="socket to be used by vtysh to connect to the daemons",
1800 parser
.add_argument(
1801 "--daemon", help="daemon for which want to replace the config", default
=""
1803 parser
.add_argument(
1805 action
="store_true",
1806 help="Used by topotest to not delete debug or log file commands",
1809 args
= parser
.parse_args()
1812 # For --test log to stdout
1813 # For --reload log to /var/log/frr/frr-reload.log
1814 if args
.test
or args
.stdout
:
1815 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1817 # Color the errors and warnings in red
1818 logging
.addLevelName(
1819 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1821 logging
.addLevelName(
1822 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1826 if not os
.path
.isdir("/var/log/frr/"):
1827 os
.makedirs("/var/log/frr/", mode
=0o0755)
1829 logging
.basicConfig(
1830 filename
="/var/log/frr/frr-reload.log",
1831 format
="%(asctime)s %(levelname)5s: %(message)s",
1834 # argparse should prevent this from happening but just to be safe...
1836 raise Exception("Must specify --reload or --test")
1837 log
= logging
.getLogger(__name__
)
1840 log
.setLevel(logging
.DEBUG
)
1842 log
.setLevel(args
.log_level
.upper())
1844 if args
.reload and not args
.stdout
:
1845 # Additionally send errors and above to STDOUT, with no metadata,
1846 # when we are logging to a file. This specifically does not follow
1847 # args.log_level, and is analagous to behaviour in earlier versions
1848 # which additionally logged most errors using print().
1850 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
1851 stdout_hdlr
.setLevel(logging
.ERROR
)
1852 stdout_hdlr
.setFormatter(logging
.Formatter())
1853 log
.addHandler(stdout_hdlr
)
1855 # Verify the new config file is valid
1856 if not os
.path
.isfile(args
.filename
):
1857 log
.error("Filename %s does not exist" % args
.filename
)
1860 if not os
.path
.getsize(args
.filename
):
1861 log
.error("Filename %s is an empty file" % args
.filename
)
1864 # Verify that confdir is correct
1865 if not os
.path
.isdir(args
.confdir
):
1866 log
.error("Confdir %s is not a valid path" % args
.confdir
)
1869 # Verify that bindir is correct
1870 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
1871 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
1874 # verify that the vty_socket, if specified, is valid
1875 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
1876 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
1879 # verify that the daemon, if specified, is valid
1880 if args
.daemon
and args
.daemon
not in [
1900 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1905 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
1907 # Verify that 'service integrated-vtysh-config' is configured
1909 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
1911 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
1912 service_integrated_vtysh_config
= True
1914 if os
.path
.isfile(vtysh_filename
):
1915 with
open(vtysh_filename
, "r") as fh
:
1916 for line
in fh
.readlines():
1919 if line
== "no service integrated-vtysh-config":
1920 service_integrated_vtysh_config
= False
1923 if not args
.test
and not service_integrated_vtysh_config
and not args
.daemon
:
1925 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1929 log
.info('Called via "%s"', str(args
))
1931 # Create a Config object from the config generated by newconf
1932 newconf
= Config(vtysh
)
1934 newconf
.load_from_file(args
.filename
)
1936 except VtyshException
as ve
:
1937 log
.error("vtysh failed to process new configuration: {}".format(ve
))
1942 # Create a Config object from the running config
1943 running
= Config(vtysh
)
1946 running
.load_from_file(args
.input)
1948 running
.load_from_show_running(args
.daemon
)
1950 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1953 if not args
.test_reset
:
1954 print("\nLines To Delete")
1955 print("===============")
1957 for (ctx_keys
, line
) in lines_to_del
:
1962 nolines
= lines_to_config(ctx_keys
, line
, True)
1965 # For topotests the original code stripped the lines, and ommitted blank lines
1966 # after, do that here
1967 nolines
= [x
.strip() for x
in nolines
]
1968 # For topotests leave these lines in (don't delete them)
1969 # [chopps: why is "log file" more special than other "log" commands?]
1971 x
for x
in nolines
if "debug" not in x
and "log file" not in x
1976 cmd
= "\n".join(nolines
)
1980 if not args
.test_reset
:
1981 print("\nLines To Add")
1982 print("============")
1984 for (ctx_keys
, line
) in lines_to_add
:
1989 lines
= lines_to_config(ctx_keys
, line
, False)
1992 # For topotests the original code stripped the lines, and ommitted blank lines
1993 # after, do that here
1994 lines
= [x
.strip() for x
in lines
if x
.strip()]
1998 cmd
= "\n".join(lines
)
2002 lines_to_configure
= []
2004 # We will not be able to do anything, go ahead and exit(1)
2005 if not vtysh
.is_config_available() or not reload_ok
:
2008 log
.debug("New Frr Config\n%s", newconf
.get_lines())
2010 # This looks a little odd but we have to do this twice...here is why
2011 # If the user had this running bgp config:
2014 # neighbor 1.1.1.1 remote-as 50
2015 # neighbor 1.1.1.1 route-map FOO out
2017 # and this config in the newconf config file
2020 # neighbor 1.1.1.1 remote-as 999
2021 # neighbor 1.1.1.1 route-map FOO out
2024 # Then the script will do
2025 # - no neighbor 1.1.1.1 remote-as 50
2026 # - neighbor 1.1.1.1 remote-as 999
2028 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
2029 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
2030 # configs again to put this line back.
2032 # There are many keywords in FRR that can only appear one time under
2033 # a context, take "bgp router-id" for example. If the config that we are
2034 # reloading against has the following:
2037 # bgp router-id 1.1.1.1
2038 # bgp router-id 2.2.2.2
2040 # The final config needs to contain "bgp router-id 2.2.2.2". On the
2041 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
2042 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
2043 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
2044 # second pass to include all of the "adds" from the first pass.
2045 lines_to_add_first_pass
= []
2048 running
= Config(vtysh
)
2049 running
.load_from_show_running(args
.daemon
)
2050 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
2052 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
2055 lines_to_add_first_pass
= lines_to_add
2057 lines_to_add
.extend(lines_to_add_first_pass
)
2059 # Only do deletes on the first pass. The reason being if we
2060 # configure a bgp neighbor via "neighbor swp1 interface" FRR
2061 # will automatically add:
2064 # ipv6 nd ra-interval 10
2065 # no ipv6 nd suppress-ra
2068 # but those lines aren't in the config we are reloading against so
2069 # on the 2nd pass they will show up in lines_to_del. This could
2070 # apply to other scenarios as well where configuring FOO adds BAR
2072 if lines_to_del
and x
== 0:
2073 for (ctx_keys
, line
) in lines_to_del
:
2078 # 'no' commands are tricky, we can't just put them in a file and
2079 # vtysh -f that file. See the next comment for an explanation
2081 cmd
= lines_to_config(ctx_keys
, line
, True)
2084 # Some commands in frr are picky about taking a "no" of the entire line.
2085 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
2086 # only the beginning. If we hit one of these command an exception will be
2087 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2090 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2091 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2092 # % Unknown command.
2093 # frr(config-if)# no ip ospf authentication message-digest
2094 # % Unknown command.
2095 # frr(config-if)# no ip ospf authentication
2101 vtysh(["configure"] + cmd
, stdouts
)
2103 except VtyshException
:
2105 # - Pull the last entry from cmd (this would be
2106 # 'no ip ospf authentication message-digest 1.1.1.1' in
2108 # - Split that last entry by whitespace and drop the last word
2109 log
.info("Failed to execute %s", " ".join(cmd
))
2110 last_arg
= cmd
[-1].split(" ")
2112 if len(last_arg
) <= 2:
2114 '"%s" we failed to remove this command',
2115 " -- ".join(original_cmd
),
2117 # Log first error msg for original_cmd
2119 log
.error(stdouts
[0])
2123 new_last_arg
= last_arg
[0:-1]
2124 cmd
[-1] = " ".join(new_last_arg
)
2126 log
.info('Executed "%s"', " ".join(cmd
))
2130 lines_to_configure
= []
2132 for (ctx_keys
, line
) in lines_to_add
:
2137 # Don't run "no" commands twice since they can error
2138 # out the second time due to first deletion
2139 if x
== 1 and ctx_keys
[0].startswith("no "):
2142 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2143 lines_to_configure
.append(cmd
)
2145 if lines_to_configure
:
2146 random_string
= "".join(
2147 random
.SystemRandom().choice(
2148 string
.ascii_uppercase
+ string
.digits
2153 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2154 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2156 with
open(filename
, "w") as fh
:
2157 for line
in lines_to_configure
:
2158 fh
.write(line
+ "\n")
2161 vtysh
.exec_file(filename
)
2162 except VtyshException
as e
:
2163 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2167 # Make these changes persistent
2168 target
= str(args
.confdir
+ "/frr.conf")
2169 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):