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
41 from collections
import OrderedDict
44 from ipaddress
import IPv6Address
, ip_network
46 from ipaddr
import IPv6Address
, IPNetwork
47 from pprint
import pformat
51 except AttributeError:
54 return iter(d
.items())
63 log
= logging
.getLogger(__name__
)
66 class VtyshException(Exception):
71 def __init__(self
, bindir
=None, confdir
=None, sockdir
=None, pathspace
=None):
73 self
.confdir
= confdir
74 self
.pathspace
= pathspace
75 self
.common_args
= [os
.path
.join(bindir
or "", "vtysh")]
77 self
.common_args
.extend(["--config_dir", confdir
])
79 self
.common_args
.extend(["--vty_socket", sockdir
])
81 self
.common_args
.extend(["-N", pathspace
])
83 def _call(self
, args
, stdin
=None, stdout
=None, stderr
=None):
86 kwargs
["stdin"] = stdin
87 if stdout
is not None:
88 kwargs
["stdout"] = stdout
89 if stderr
is not None:
90 kwargs
["stderr"] = stderr
91 return subprocess
.Popen(self
.common_args
+ args
, **kwargs
)
93 def _call_cmd(self
, command
, stdin
=None, stdout
=None, stderr
=None):
94 if isinstance(command
, list):
95 args
= [item
for sub
in command
for item
in ["-c", sub
]]
97 args
= ["-c", command
]
98 return self
._call
(args
, stdin
, stdout
, stderr
)
100 def __call__(self
, command
, stdouts
=None):
102 Call a CLI command (e.g. "show running-config")
104 Output text is automatically redirected, decoded and returned.
105 Multiple commands may be passed as list.
107 proc
= self
._call
_cmd
(command
, stdout
=subprocess
.PIPE
)
108 stdout
, stderr
= proc
.communicate()
110 if stdouts
is not None:
111 stdouts
.append(stdout
.decode("UTF-8"))
112 raise VtyshException(
113 'vtysh returned status %d for command "%s"' % (proc
.returncode
, command
)
115 return stdout
.decode("UTF-8")
117 def is_config_available(self
):
119 Return False if no frr daemon is running or some other vtysh session is
120 in 'configuration terminal' mode which will prevent us from making any
121 configuration changes.
124 output
= self("configure")
126 if "VTY configuration is locked by other VTY" in output
:
127 log
.error("vtysh 'configure' returned\n%s\n" % (output
))
132 def exec_file(self
, filename
):
133 child
= self
._call
(["-f", filename
])
134 if child
.wait() != 0:
135 raise VtyshException(
136 "vtysh (exec file) exited with status %d" % (child
.returncode
)
139 def mark_file(self
, filename
, stdin
=None):
141 ["-m", "-f", filename
],
142 stdout
=subprocess
.PIPE
,
143 stdin
=subprocess
.PIPE
,
144 stderr
=subprocess
.PIPE
,
147 stdout
, stderr
= child
.communicate()
148 except subprocess
.TimeoutExpired
:
150 stdout
, stderr
= child
.communicate()
151 raise VtyshException("vtysh call timed out!")
153 if child
.wait() != 0:
154 raise VtyshException(
155 "vtysh (mark file) exited with status %d:\n%s"
156 % (child
.returncode
, stderr
)
159 return stdout
.decode("UTF-8")
161 def mark_show_run(self
, daemon
=None):
162 cmd
= "show running-config"
164 cmd
+= " %s" % daemon
166 show_run
= self
._call
_cmd
(cmd
, stdout
=subprocess
.PIPE
)
168 ["-m", "-f", "-"], stdin
=show_run
.stdout
, stdout
=subprocess
.PIPE
172 stdout
, stderr
= mark
.communicate()
175 if show_run
.returncode
!= 0:
176 raise VtyshException(
177 "vtysh (show running-config) exited with status %d:"
178 % (show_run
.returncode
)
180 if mark
.returncode
!= 0:
181 raise VtyshException(
182 "vtysh (mark running-config) exited with status %d" % (mark
.returncode
)
185 return stdout
.decode("UTF-8")
188 class Context(object):
191 A Context object represents a section of frr configuration such as:
194 description swp3 -> r8's swp1
199 or a single line context object such as this:
205 def __init__(self
, keys
, lines
):
209 # Keep a dictionary of the lines, this is to make it easy to tell if a
210 # line exists in this Context
211 self
.dlines
= OrderedDict()
214 self
.dlines
[ligne
] = True
216 def add_lines(self
, lines
):
218 Add lines to specified context
221 self
.lines
.extend(lines
)
224 self
.dlines
[ligne
] = True
227 def get_normalized_es_id(line
):
229 The es-id or es-sys-mac need to be converted to lower case
231 sub_strs
= ["evpn mh es-id", "evpn mh es-sys-mac"]
232 for sub_str
in sub_strs
:
233 obj
= re
.match(sub_str
+ " (?P<esi>\S*)", line
)
235 line
= "%s %s" % (sub_str
, obj
.group("esi").lower())
240 def get_normalized_mac_ip_line(line
):
241 if line
.startswith("evpn mh es"):
242 return get_normalized_es_id(line
)
244 if not "ipv6 add" in line
:
245 return get_normalized_ipv6_line(line
)
250 class Config(object):
253 A frr configuration is stored in a Config object. A Config object
254 contains a dictionary of Context objects where the Context keys
255 ('router ospf' for example) are our dictionary key.
258 def __init__(self
, vtysh
):
260 self
.contexts
= OrderedDict()
263 def load_from_file(self
, filename
):
265 Read configuration from specified file and slurp it into internal memory
266 The internal representation has been marked appropriately by passing it
267 through vtysh with the -m parameter
269 log
.info("Loading Config object from file %s", filename
)
271 file_output
= self
.vtysh
.mark_file(filename
)
273 for line
in file_output
.split("\n"):
276 # Compress duplicate whitespaces
277 line
= " ".join(line
.split())
280 line
= get_normalized_mac_ip_line(line
)
283 vrf static routes can be added in two ways. The old way is:
285 "ip route x.x.x.x/x y.y.y.y vrf <vrfname>"
287 but it's rendered in the configuration as the new way::
290 ip route x.x.x.x/x y.y.y.y
293 this difference causes frr-reload to not consider them a
294 match and delete vrf static routes incorrectly.
295 fix the old way to match new "show running" output so a
296 proper match is found.
299 line
.startswith("ip route ") or line
.startswith("ipv6 route ")
300 ) and " vrf " in line
:
301 newline
= line
.split(" ")
302 vrf_index
= newline
.index("vrf")
303 vrf_ctx
= newline
[vrf_index
] + " " + newline
[vrf_index
+ 1]
304 del newline
[vrf_index
: vrf_index
+ 2]
305 newline
= " ".join(newline
)
306 self
.lines
.append(vrf_ctx
)
307 self
.lines
.append(newline
)
308 self
.lines
.append("exit-vrf")
311 self
.lines
.append(line
)
315 def load_from_show_running(self
, daemon
):
317 Read running configuration and slurp it into internal memory
318 The internal representation has been marked appropriately by passing it
319 through vtysh with the -m parameter
321 log
.info("Loading Config object from vtysh show running")
323 config_text
= self
.vtysh
.mark_show_run(daemon
)
325 for line
in config_text
.split("\n"):
329 line
== "Building configuration..."
330 or line
== "Current configuration:"
335 self
.lines
.append(line
)
341 Return the lines read in from the configuration
344 return "\n".join(self
.lines
)
346 def get_contexts(self
):
348 Return the parsed context as strings for display, log etc.
351 for (_
, ctx
) in sorted(iteritems(self
.contexts
)):
352 print(str(ctx
) + "\n")
354 def save_contexts(self
, key
, lines
):
356 Save the provided key and lines as a context
363 IP addresses specified in "network" statements, "ip prefix-lists"
364 etc. can differ in the host part of the specification the user
365 provides and what the running config displays. For example, user
366 can specify 11.1.1.1/24, and the running config displays this as
367 11.1.1.0/24. Ensure we don't do a needless operation for such
368 lines. IS-IS & OSPFv3 have no "network" support.
370 re_key_rt
= re
.match(r
"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0])
372 addr
= re_key_rt
.group(2)
375 if "ipaddress" not in sys
.modules
:
376 newaddr
= IPNetwork(addr
)
377 key
[0] = "%s route %s/%s%s" % (
384 newaddr
= ip_network(addr
, strict
=False)
385 key
[0] = "%s route %s/%s%s" % (
387 str(newaddr
.network_address
),
394 re_key_rt
= re
.match(
395 r
"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0]
398 addr
= re_key_rt
.group(4)
401 if "ipaddress" not in sys
.modules
:
402 newaddr
= "%s/%s" % (
403 IPNetwork(addr
).network
,
404 IPNetwork(addr
).prefixlen
,
407 network_addr
= ip_network(addr
, strict
=False)
408 newaddr
= "%s/%s" % (
409 str(network_addr
.network_address
),
410 network_addr
.prefixlen
,
417 legestr
= re_key_rt
.group(5)
418 re_lege
= re
.search(r
"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr
)
420 legestr
= "%sge %s le %s%s" % (
427 key
[0] = "%s prefix-list%s%s %s%s" % (
435 if lines
and key
[0].startswith("router bgp"):
438 re_net
= re
.match(r
"network\s+([A-Fa-f:.0-9/]+)(.*)$", line
)
440 addr
= re_net
.group(1)
441 if "/" not in addr
and key
[0].startswith("router bgp"):
442 # This is most likely an error because with no
443 # prefixlen, BGP treats the prefixlen as 8
447 if "ipaddress" not in sys
.modules
:
448 newaddr
= IPNetwork(addr
)
449 line
= "network %s/%s %s" % (
455 network_addr
= ip_network(addr
, strict
=False)
456 line
= "network %s/%s %s" % (
457 str(network_addr
.network_address
),
458 network_addr
.prefixlen
,
461 newlines
.append(line
)
463 # Really this should be an error. Whats a network
464 # without an IP Address following it ?
465 newlines
.append(line
)
467 newlines
.append(line
)
471 More fixups in user specification and what running config shows.
472 "null0" in routes must be replaced by Null0.
475 key
[0].startswith("ip route")
476 or key
[0].startswith("ipv6 route")
477 and "null0" in key
[0]
479 key
[0] = re
.sub(r
"\s+null0(\s*$)", " Null0", key
[0])
482 Similar to above, but when the static is in a vrf, it turns into a
483 blackhole nexthop for both null0 and Null0. Fix it accordingly
485 if lines
and key
[0].startswith("vrf "):
488 if line
.startswith("ip route ") or line
.startswith("ipv6 route "):
490 line
= re
.sub(r
"\s+null0(\s*$)", " blackhole", line
)
491 elif "Null0" in line
:
492 line
= re
.sub(r
"\s+Null0(\s*$)", " blackhole", line
)
493 newlines
.append(line
)
495 newlines
.append(line
)
499 if tuple(key
) not in self
.contexts
:
500 ctx
= Context(tuple(key
), lines
)
501 self
.contexts
[tuple(key
)] = ctx
503 ctx
= self
.contexts
[tuple(key
)]
507 if tuple(key
) not in self
.contexts
:
508 ctx
= Context(tuple(key
), [])
509 self
.contexts
[tuple(key
)] = ctx
511 def load_contexts(self
):
513 Parse the configuration and create contexts for each appropriate block
516 current_context_lines
= []
520 The end of a context is flagged via the 'end' keyword:
529 bgp router-id 10.0.0.1
530 bgp log-neighbor-changes
531 no bgp default ipv4-unicast
532 neighbor EBGP peer-group
533 neighbor EBGP advertisement-interval 1
534 neighbor EBGP timers connect 10
535 neighbor 2001:40:1:4::6 remote-as 40
536 neighbor 2001:40:1:8::a remote-as 40
540 neighbor IBGPv6 activate
541 neighbor 2001:10::2 peer-group IBGPv6
542 neighbor 2001:10::3 peer-group IBGPv6
547 neighbor LEAF activate
551 route-target import 10.1.1.1:10100
552 route-target export 10.1.1.1:10100
558 ospf router-id 10.0.0.1
559 log-adjacency-changes detail
560 timers throttle spf 0 50 5000
565 # The code assumes that its working on the output from the "vtysh -m"
566 # command. That provides the appropriate markers to signify end of
567 # a context. This routine uses that to build the contexts for the
570 # There are single line contexts such as "log file /media/node/zebra.log"
571 # and multi-line contexts such as "router ospf" and subcontexts
572 # within a context such as "address-family" within "router bgp"
573 # In each of these cases, the first line of the context becomes the
574 # key of the context. So "router bgp 10" is the key for the non-address
575 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
576 # the key for the subcontext and so on.
581 # the keywords that we know are single line contexts. bgp in this case
582 # is not the main router bgp block, but enabling multi-instance
583 oneline_ctx_keywords
= (
586 "allow-external-route-update",
610 "vrrp autoconfigure",
614 for line
in self
.lines
:
619 if line
.startswith("!") or line
.startswith("#"):
624 and ctx_keys
[0].startswith("bfd")
625 and ctx_keys
[1].startswith("profile ")
628 log
.debug("LINE %-50s: popping from sub context, %-50s", line
, ctx_keys
)
631 self
.save_contexts(ctx_keys
, current_context_lines
)
632 ctx_keys
= copy
.deepcopy(main_ctx_key
)
633 current_context_lines
= []
637 # there is one exception though: ldpd accepts a 'router-id' clause
638 # as part of its 'mpls ldp' config context. If we are processing
639 # ldp configuration and encounter a router-id we should NOT switch
643 and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
)
646 and ctx_keys
[0].startswith("mpls ldp")
647 and line
.startswith("router-id ")
650 self
.save_contexts(ctx_keys
, current_context_lines
)
652 # Start a new context
657 current_context_lines
= []
659 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
660 self
.save_contexts(ctx_keys
, current_context_lines
)
664 self
.save_contexts(ctx_keys
, current_context_lines
)
665 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
667 # Start a new context
671 current_context_lines
= []
673 elif line
== "exit" and ctx_keys
[0].startswith("rpki"):
674 self
.save_contexts(ctx_keys
, current_context_lines
)
675 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
677 # Start a new context
681 current_context_lines
= []
683 elif line
== "exit-vrf":
684 self
.save_contexts(ctx_keys
, current_context_lines
)
685 current_context_lines
.append(line
)
687 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
690 # Start a new context
694 current_context_lines
= []
698 and len(ctx_keys
) > 1
699 and ctx_keys
[0].startswith("segment-routing")
701 self
.save_contexts(ctx_keys
, current_context_lines
)
703 # Start a new context
704 ctx_keys
= ctx_keys
[:-1]
705 current_context_lines
= []
707 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
712 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
713 # if this exit is for address-family ipv4 unicast, ignore the pop
715 self
.save_contexts(ctx_keys
, current_context_lines
)
717 # Start a new context
718 ctx_keys
= copy
.deepcopy(main_ctx_key
)
719 current_context_lines
= []
721 "LINE %-50s: popping from subcontext to ctx%-50s",
726 elif line
in ["exit-vni", "exit-ldp-if"]:
728 self
.save_contexts(ctx_keys
, current_context_lines
)
730 # Start a new context
731 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
732 current_context_lines
= []
734 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
739 elif new_ctx
is True:
745 ctx_keys
= copy
.deepcopy(main_ctx_key
)
748 current_context_lines
= []
750 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
753 line
.startswith("address-family ")
754 or line
.startswith("vnc defaults")
755 or line
.startswith("vnc l2-group")
756 or line
.startswith("vnc nve-group")
757 or line
.startswith("peer")
758 or line
.startswith("key ")
759 or line
.startswith("member pseudowire")
763 # Save old context first
764 self
.save_contexts(ctx_keys
, current_context_lines
)
765 current_context_lines
= []
766 main_ctx_key
= copy
.deepcopy(ctx_keys
)
767 log
.debug("LINE %-50s: entering sub-context, append to ctx_keys", line
)
769 if line
== "address-family ipv6" and not ctx_keys
[0].startswith(
772 ctx_keys
.append("address-family ipv6 unicast")
773 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith(
776 ctx_keys
.append("address-family ipv4 unicast")
777 elif line
== "address-family evpn":
778 ctx_keys
.append("address-family l2vpn evpn")
780 ctx_keys
.append(line
)
783 line
.startswith("vni ")
784 and len(ctx_keys
) == 2
785 and ctx_keys
[0].startswith("router bgp")
786 and ctx_keys
[1] == "address-family l2vpn evpn"
789 # Save old context first
790 self
.save_contexts(ctx_keys
, current_context_lines
)
791 current_context_lines
= []
792 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
794 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
796 ctx_keys
.append(line
)
799 line
.startswith("interface ")
800 and len(ctx_keys
) == 2
801 and ctx_keys
[0].startswith("mpls ldp")
802 and ctx_keys
[1].startswith("address-family")
805 # Save old context first
806 self
.save_contexts(ctx_keys
, current_context_lines
)
807 current_context_lines
= []
808 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
810 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
812 ctx_keys
.append(line
)
815 line
.startswith("traffic-eng")
816 and len(ctx_keys
) == 1
817 and ctx_keys
[0].startswith("segment-routing")
820 # Save old context first
821 self
.save_contexts(ctx_keys
, current_context_lines
)
822 current_context_lines
= []
824 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
827 ctx_keys
.append(line
)
830 line
.startswith("segment-list ")
831 and len(ctx_keys
) == 2
832 and ctx_keys
[0].startswith("segment-routing")
833 and ctx_keys
[1].startswith("traffic-eng")
836 # Save old context first
837 self
.save_contexts(ctx_keys
, current_context_lines
)
838 current_context_lines
= []
840 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
843 ctx_keys
.append(line
)
846 line
.startswith("policy ")
847 and len(ctx_keys
) == 2
848 and ctx_keys
[0].startswith("segment-routing")
849 and ctx_keys
[1].startswith("traffic-eng")
852 # Save old context first
853 self
.save_contexts(ctx_keys
, current_context_lines
)
854 current_context_lines
= []
856 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
859 ctx_keys
.append(line
)
862 line
.startswith("candidate-path ")
863 and line
.endswith(" dynamic")
864 and len(ctx_keys
) == 3
865 and ctx_keys
[0].startswith("segment-routing")
866 and ctx_keys
[1].startswith("traffic-eng")
867 and ctx_keys
[2].startswith("policy")
870 # Save old context first
871 self
.save_contexts(ctx_keys
, current_context_lines
)
872 current_context_lines
= []
873 main_ctx_key
= copy
.deepcopy(ctx_keys
)
875 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
878 ctx_keys
.append(line
)
881 line
.startswith("pcep")
882 and len(ctx_keys
) == 2
883 and ctx_keys
[0].startswith("segment-routing")
884 and ctx_keys
[1].startswith("traffic-eng")
887 # Save old context first
888 self
.save_contexts(ctx_keys
, current_context_lines
)
889 current_context_lines
= []
890 main_ctx_key
= copy
.deepcopy(ctx_keys
)
892 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
894 ctx_keys
.append(line
)
897 line
.startswith("pce-config ")
898 and len(ctx_keys
) == 3
899 and ctx_keys
[0].startswith("segment-routing")
900 and ctx_keys
[1].startswith("traffic-eng")
901 and ctx_keys
[2].startswith("pcep")
904 # Save old context first
905 self
.save_contexts(ctx_keys
, current_context_lines
)
906 current_context_lines
= []
907 main_ctx_key
= copy
.deepcopy(ctx_keys
)
909 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
912 ctx_keys
.append(line
)
915 line
.startswith("pce ")
916 and len(ctx_keys
) == 3
917 and ctx_keys
[0].startswith("segment-routing")
918 and ctx_keys
[1].startswith("traffic-eng")
919 and ctx_keys
[2].startswith("pcep")
922 # Save old context first
923 self
.save_contexts(ctx_keys
, current_context_lines
)
924 current_context_lines
= []
925 main_ctx_key
= copy
.deepcopy(ctx_keys
)
927 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
929 ctx_keys
.append(line
)
932 line
.startswith("pcc")
933 and len(ctx_keys
) == 3
934 and ctx_keys
[0].startswith("segment-routing")
935 and ctx_keys
[1].startswith("traffic-eng")
936 and ctx_keys
[2].startswith("pcep")
939 # Save old context first
940 self
.save_contexts(ctx_keys
, current_context_lines
)
941 current_context_lines
= []
942 main_ctx_key
= copy
.deepcopy(ctx_keys
)
944 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
946 ctx_keys
.append(line
)
949 line
.startswith("profile ")
950 and len(ctx_keys
) == 1
951 and ctx_keys
[0].startswith("bfd")
954 # Save old context first
955 self
.save_contexts(ctx_keys
, current_context_lines
)
956 current_context_lines
= []
957 main_ctx_key
= copy
.deepcopy(ctx_keys
)
959 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
962 ctx_keys
.append(line
)
965 # Continuing in an existing context, add non-commented lines to it
966 current_context_lines
.append(line
)
968 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
971 # Save the context of the last one
972 self
.save_contexts(ctx_keys
, current_context_lines
)
975 def lines_to_config(ctx_keys
, line
, delete
):
977 Return the command as it would appear in frr.conf
982 for (i
, ctx_key
) in enumerate(ctx_keys
):
983 cmd
.append(" " * i
+ ctx_key
)
986 indent
= len(ctx_keys
) * " "
988 # There are some commands that are on by default so their "no" form will be
989 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
990 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
991 # not by doing a "no no bgp default ipv4-unicast"
993 if line
.startswith("no "):
994 cmd
.append("%s%s" % (indent
, line
[3:]))
996 cmd
.append("%sno %s" % (indent
, line
))
999 cmd
.append(indent
+ line
)
1001 # If line is None then we are typically deleting an entire
1002 # context ('no router ospf' for example)
1004 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
1005 cmd
.append("%s%s" % (" " * i
, ctx_key
))
1007 # Only put the 'no' on the last sub-context
1009 if ctx_keys
[-1].startswith("no "):
1010 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
1012 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
1014 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
1019 def get_normalized_ipv6_line(line
):
1021 Return a normalized IPv6 line as produced by frr,
1022 with all letters in lower case and trailing and leading
1023 zeros removed, and only the network portion present if
1024 the IPv6 word is a network
1027 words
= line
.split(" ")
1033 if "ipaddress" not in sys
.modules
:
1034 v6word
= IPNetwork(word
)
1035 norm_word
= "%s/%s" % (v6word
.network
, v6word
.prefixlen
)
1037 v6word
= ip_network(word
, strict
=False)
1038 norm_word
= "%s/%s" % (
1039 str(v6word
.network_address
),
1046 norm_word
= "%s" % IPv6Address(word
)
1051 norm_line
= norm_line
+ " " + norm_word
1053 return norm_line
.strip()
1056 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
1057 for (ctx_keys
, line
) in lines
:
1058 if ctx_keys
== target_ctx_keys
:
1060 if line
== target_line
:
1063 if line
.startswith(target_line
):
1068 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
1070 # exit-vrf is a bit tricky. If the new config is missing it but we
1071 # have configs under a vrf, we need to add it at the end to do the
1072 # right context changes. If exit-vrf exists in both the running and
1073 # new config, we cannot delete it or it will break context changes.
1074 add_exit_vrf
= False
1077 for (ctx_keys
, line
) in lines_to_add
:
1078 if add_exit_vrf
== True:
1079 if ctx_keys
[0] != prior_ctx_key
:
1080 insert_key
= ((prior_ctx_key
),)
1081 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
1082 add_exit_vrf
= False
1084 if ctx_keys
[0].startswith("vrf") and line
:
1085 if line
!= "exit-vrf":
1087 prior_ctx_key
= ctx_keys
[0]
1089 add_exit_vrf
= False
1092 for (ctx_keys
, line
) in lines_to_del
:
1093 if line
== "exit-vrf":
1094 if line_exist(lines_to_add
, ctx_keys
, line
):
1095 lines_to_del
.remove((ctx_keys
, line
))
1097 return (lines_to_add
, lines_to_del
)
1101 This method handles deletion of bgp peer group config.
1102 The objective is to delete config lines related to peers
1103 associated with the peer-group and move the peer-group
1104 config line to the end of the lines_to_del list.
1108 def delete_move_lines(lines_to_add
, lines_to_del
):
1111 # Stores the lines to move to the end of the pending list.
1112 lines_to_del_to_del
= []
1113 # Stores the lines to move to end of the pending list.
1114 lines_to_del_to_app
= []
1115 found_pg_del_cmd
= False
1118 When "neighbor <pg_name> peer-group" under a bgp instance is removed,
1119 it also deletes the associated peer config. Any config line below no form of
1120 peer-group related to a peer are errored out as the peer no longer exists.
1121 To cleanup peer-group and associated peer(s) configs:
1122 - Remove all the peers config lines from the pending list (lines_to_del list).
1123 - Move peer-group deletion line to the end of the pending list, to allow
1124 removal of any of the peer-group specific configs.
1126 Create a dictionary of config context (i.e. router bgp vrf x).
1127 Under each context node, create a dictionary of a peer-group name.
1128 Append a peer associated to the peer-group into a list under a peer-group node.
1129 Remove all of the peer associated config lines from the pending list.
1130 Append peer-group deletion line to end of the pending list.
1133 neighbor underlay peer-group
1134 neighbor underlay remote-as external
1135 neighbor underlay advertisement-interval 0
1136 neighbor underlay timers 3 9
1137 neighbor underlay timers connect 10
1138 neighbor swp1 interface peer-group underlay
1139 neighbor swp1 advertisement-interval 0
1140 neighbor swp1 timers 3 9
1141 neighbor swp1 timers connect 10
1142 neighbor swp2 interface peer-group underlay
1143 neighbor swp2 advertisement-interval 0
1144 neighbor swp2 timers 3 9
1145 neighbor swp2 timers connect 10
1146 neighbor swp3 interface peer-group underlay
1147 neighbor uplink1 interface remote-as internal
1148 neighbor uplink1 advertisement-interval 0
1149 neighbor uplink1 timers 3 9
1150 neighbor uplink1 timers connect 10
1153 "router bgp 200 no bgp bestpath as-path multipath-relax"
1154 "router bgp 200 no neighbor underlay advertisement-interval 0"
1155 "router bgp 200 no neighbor underlay timers 3 9"
1156 "router bgp 200 no neighbor underlay timers connect 10"
1157 "router bgp 200 no neighbor uplink1 advertisement-interval 0"
1158 "router bgp 200 no neighbor uplink1 timers 3 9"
1159 "router bgp 200 no neighbor uplink1 timers connect 10"
1160 "router bgp 200 no neighbor underlay remote-as external"
1161 "router bgp 200 no neighbor uplink1 interface remote-as internal"
1162 "router bgp 200 no neighbor underlay peer-group"
1166 for (ctx_keys
, line
) in lines_to_del
:
1168 ctx_keys
[0].startswith("router bgp")
1170 and line
.startswith("neighbor ")
1173 When 'neighbor <peer> remote-as <>' is removed it deletes the peer,
1174 there might be a peer associated config which also needs to be removed
1176 Append the 'neighbor <peer> remote-as <>' to the lines_to_del.
1179 neighbor uplink1 interface remote-as internal
1180 neighbor uplink1 advertisement-interval 0
1181 neighbor uplink1 timers 3 9
1182 neighbor uplink1 timers connect 10
1185 neighbor uplink1 advertisement-interval 0
1186 neighbor uplink1 timers 3 9
1187 neighbor uplink1 timers connect 10
1190 neighbor uplink1 interface remote-as internal
1193 # 'no neighbor peer [interface] remote-as <>'
1194 nb_remoteas
= "neighbor (\S+) .*remote-as (\S+)"
1195 re_nb_remoteas
= re
.search(nb_remoteas
, line
)
1197 lines_to_del_to_app
.append((ctx_keys
, line
))
1200 {'router bgp 65001': {'PG': [], 'PG1': []},
1201 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}}
1203 if ctx_keys
[0] not in del_dict
:
1204 del_dict
[ctx_keys
[0]] = dict()
1205 # find 'no neighbor <pg_name> peer-group'
1206 re_pg
= re
.match("neighbor (\S+) peer-group$", line
)
1207 if re_pg
and re_pg
.group(1) not in del_dict
[ctx_keys
[0]]:
1208 del_dict
[ctx_keys
[0]][re_pg
.group(1)] = list()
1210 for (ctx_keys
, line
) in lines_to_del_to_app
:
1211 lines_to_del
.remove((ctx_keys
, line
))
1212 lines_to_del
.append((ctx_keys
, line
))
1214 if found_pg_del_cmd
== False:
1215 return (lines_to_add
, lines_to_del
)
1218 {'router bgp 65001': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']},
1219 'router bgp 65001 vrf vrf1': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}}
1221 for (ctx_keys
, line
) in lines_to_del
:
1223 ctx_keys
[0].startswith("router bgp")
1225 and line
.startswith("neighbor ")
1227 if ctx_keys
[0] in del_dict
:
1228 for pg_key
in del_dict
[ctx_keys
[0]]:
1229 # 'neighbor <peer> [interface] peer-group <pg_name>'
1230 nb_pg
= "neighbor (\S+) .*peer-group %s$" % pg_key
1231 re_nbr_pg
= re
.search(nb_pg
, line
)
1234 and re_nbr_pg
.group(1) not in del_dict
[ctx_keys
[0]][pg_key
]
1236 del_dict
[ctx_keys
[0]][pg_key
].append(re_nbr_pg
.group(1))
1238 lines_to_del_to_app
= []
1239 for (ctx_keys
, line
) in lines_to_del
:
1241 ctx_keys
[0].startswith("router bgp")
1243 and line
.startswith("neighbor ")
1245 if ctx_keys
[0] in del_dict
:
1246 for pg
in del_dict
[ctx_keys
[0]]:
1247 for nbr
in del_dict
[ctx_keys
[0]][pg
]:
1248 nb_exp
= "neighbor %s .*" % nbr
1249 re_nb
= re
.search(nb_exp
, line
)
1250 # add peer configs to delete list.
1251 if re_nb
and line
not in lines_to_del_to_del
:
1252 lines_to_del_to_del
.append((ctx_keys
, line
))
1254 pg_exp
= "neighbor %s peer-group$" % pg
1255 re_pg
= re
.match(pg_exp
, line
)
1257 lines_to_del_to_app
.append((ctx_keys
, line
))
1259 for (ctx_keys
, line
) in lines_to_del_to_del
:
1260 lines_to_del
.remove((ctx_keys
, line
))
1262 for (ctx_keys
, line
) in lines_to_del_to_app
:
1263 lines_to_del
.remove((ctx_keys
, line
))
1264 lines_to_del
.append((ctx_keys
, line
))
1266 return (lines_to_add
, lines_to_del
)
1269 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1271 # Quite possibly the most confusing (while accurate) variable names in history
1272 lines_to_add_to_del
= []
1273 lines_to_del_to_del
= []
1275 for (ctx_keys
, line
) in lines_to_del
:
1278 # If there is a change in the segment routing block ranges, do it
1279 # in-place, to avoid requesting spurious label chunks which might fail
1280 if line
and "segment-routing global-block" in line
:
1281 for (add_key
, add_line
) in lines_to_add
:
1283 ctx_keys
[0] == add_key
[0]
1285 and "segment-routing global-block" in add_line
1287 lines_to_del_to_del
.append((ctx_keys
, line
))
1291 if ctx_keys
[0].startswith("router bgp") and line
:
1293 if line
.startswith("neighbor "):
1295 BGP changed how it displays swpX peers that are part of peer-group. Older
1296 versions of frr would display these on separate lines:
1297 neighbor swp1 interface
1298 neighbor swp1 peer-group FOO
1300 but today we display via a single line
1301 neighbor swp1 interface peer-group FOO
1303 This change confuses frr-reload.py so check to see if we are deleting
1304 neighbor swp1 interface peer-group FOO
1307 neighbor swp1 interface
1308 neighbor swp1 peer-group FOO
1310 If so then chop the del line and the corresponding add lines
1313 re_swpx_int_peergroup
= re
.search(
1314 "neighbor (\S+) interface peer-group (\S+)", line
1316 re_swpx_int_v6only_peergroup
= re
.search(
1317 "neighbor (\S+) interface v6only peer-group (\S+)", line
1320 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1321 swpx_interface
= None
1322 swpx_peergroup
= None
1324 if re_swpx_int_peergroup
:
1325 swpx
= re_swpx_int_peergroup
.group(1)
1326 peergroup
= re_swpx_int_peergroup
.group(2)
1327 swpx_interface
= "neighbor %s interface" % swpx
1328 elif re_swpx_int_v6only_peergroup
:
1329 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1330 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1331 swpx_interface
= "neighbor %s interface v6only" % swpx
1333 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1334 found_add_swpx_interface
= line_exist(
1335 lines_to_add
, ctx_keys
, swpx_interface
1337 found_add_swpx_peergroup
= line_exist(
1338 lines_to_add
, ctx_keys
, swpx_peergroup
1340 tmp_ctx_keys
= tuple(list(ctx_keys
))
1342 if not found_add_swpx_peergroup
:
1343 tmp_ctx_keys
= list(ctx_keys
)
1344 tmp_ctx_keys
.append("address-family ipv4 unicast")
1345 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1346 found_add_swpx_peergroup
= line_exist(
1347 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1350 if not found_add_swpx_peergroup
:
1351 tmp_ctx_keys
= list(ctx_keys
)
1352 tmp_ctx_keys
.append("address-family ipv6 unicast")
1353 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1354 found_add_swpx_peergroup
= line_exist(
1355 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1358 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1360 lines_to_del_to_del
.append((ctx_keys
, line
))
1361 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1362 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1365 Changing the bfd timers on neighbors is allowed without doing
1366 a delete/add process. Since doing a "no neighbor blah bfd ..."
1367 will cause the peer to bounce unnecessarily, just skip the delete
1368 and just do the add.
1370 re_nbr_bfd_timers
= re
.search(
1371 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1374 if re_nbr_bfd_timers
:
1375 nbr
= re_nbr_bfd_timers
.group(1)
1376 bfd_nbr
= "neighbor %s" % nbr
1377 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1379 for (ctx_keys
, add_line
) in lines_to_add
:
1380 if ctx_keys
[0].startswith("router bgp"):
1381 re_add_nbr_bfd_timers
= re
.search(
1382 bfd_search_string
, add_line
1385 if re_add_nbr_bfd_timers
:
1386 found_add_bfd_nbr
= line_exist(
1387 lines_to_add
, ctx_keys
, bfd_nbr
, False
1390 if found_add_bfd_nbr
:
1391 lines_to_del_to_del
.append((ctx_keys
, line
))
1394 We changed how we display the neighbor interface command. Older
1395 versions of frr would display the following:
1396 neighbor swp1 interface
1397 neighbor swp1 remote-as external
1398 neighbor swp1 capability extended-nexthop
1400 but today we display via a single line
1401 neighbor swp1 interface remote-as external
1403 and capability extended-nexthop is no longer needed because we
1404 automatically enable it when the neighbor is of type interface.
1406 This change confuses frr-reload.py so check to see if we are deleting
1407 neighbor swp1 interface remote-as (external|internal|ASNUM)
1410 neighbor swp1 interface
1411 neighbor swp1 remote-as (external|internal|ASNUM)
1412 neighbor swp1 capability extended-nexthop
1414 If so then chop the del line and the corresponding add lines
1416 re_swpx_int_remoteas
= re
.search(
1417 "neighbor (\S+) interface remote-as (\S+)", line
1419 re_swpx_int_v6only_remoteas
= re
.search(
1420 "neighbor (\S+) interface v6only remote-as (\S+)", line
1423 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1424 swpx_interface
= None
1425 swpx_remoteas
= None
1427 if re_swpx_int_remoteas
:
1428 swpx
= re_swpx_int_remoteas
.group(1)
1429 remoteas
= re_swpx_int_remoteas
.group(2)
1430 swpx_interface
= "neighbor %s interface" % swpx
1431 elif re_swpx_int_v6only_remoteas
:
1432 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1433 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1434 swpx_interface
= "neighbor %s interface v6only" % swpx
1436 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1437 found_add_swpx_interface
= line_exist(
1438 lines_to_add
, ctx_keys
, swpx_interface
1440 found_add_swpx_remoteas
= line_exist(
1441 lines_to_add
, ctx_keys
, swpx_remoteas
1443 tmp_ctx_keys
= tuple(list(ctx_keys
))
1445 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1447 lines_to_del_to_del
.append((ctx_keys
, line
))
1448 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1449 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1452 We made the 'bgp bestpath as-path multipath-relax' command
1453 automatically assume 'no-as-set' since the lack of this option caused
1454 weird routing problems. When the running config is shown in
1455 releases with this change, the no-as-set keyword is not shown as it
1456 is the default. This causes frr-reload to unnecessarily unapply
1457 this option only to apply it back again, causing unnecessary session
1460 if "multipath-relax" in line
:
1461 re_asrelax_new
= re
.search(
1462 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1464 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1465 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1467 if re_asrelax_new
and found_asrelax_old
:
1469 lines_to_del_to_del
.append((ctx_keys
, line
))
1470 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1473 If we are modifying the BGP table-map we need to avoid a del/add and
1474 instead modify the table-map in place via an add. This is needed to
1475 avoid installing all routes in the RIB the second the 'no table-map'
1478 if line
.startswith("table-map"):
1479 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1482 lines_to_del_to_del
.append((ctx_keys
, line
))
1485 More old-to-new config handling. ip import-table no longer accepts
1486 distance, but we honor the old syntax. But 'show running' shows only
1487 the new syntax. This causes an unnecessary 'no import-table' followed
1488 by the same old 'ip import-table' which causes perturbations in
1489 announced routes leading to traffic blackholes. Fix this issue.
1491 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1493 table_num
= re_importtbl
.group(1)
1494 for ctx
in lines_to_add
:
1495 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1496 lines_to_del_to_del
.append(
1497 (("ip import-table %s" % table_num
,), None)
1499 lines_to_add_to_del
.append((ctx
[0], None))
1502 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1503 However, the running config always adds 'seq x', where x is a number
1504 incremented by 5 for every element of the prefix/access list.
1505 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1506 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1507 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1508 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1509 access-list FOO seq 5 permit 2.2.2.2/32
1510 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1512 re_acl_pfxlst
= re
.search(
1513 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1519 re_acl_pfxlst
.group(1)
1520 + re_acl_pfxlst
.group(2)
1521 + re_acl_pfxlst
.group(3)
1522 + re_acl_pfxlst
.group(5)
1523 + re_acl_pfxlst
.group(6)
1525 for ctx
in lines_to_add
:
1526 if ctx
[0][0] == tmpline
:
1527 lines_to_del_to_del
.append((ctx_keys
, None))
1528 lines_to_add_to_del
.append(((tmpline
,), None))
1531 If prefix-lists or access-lists are being deleted and
1532 not added (see comment above), add command with 'no' to
1533 lines_to_add and remove from lines_to_del to improve
1534 scaling performance.
1537 add_cmd
= ("no " + ctx_keys
[0],)
1538 lines_to_add
.append((add_cmd
, None))
1539 lines_to_del_to_del
.append((ctx_keys
, None))
1543 and ctx_keys
[0].startswith("router bgp")
1544 and ctx_keys
[1] == "address-family l2vpn evpn"
1545 and ctx_keys
[2].startswith("vni")
1549 re
.search("^route-target import (.*)$", line
)
1555 rt
= re_route_target
.group(1).strip()
1556 route_target_import_line
= line
1557 route_target_export_line
= "route-target export %s" % rt
1558 route_target_both_line
= "route-target both %s" % rt
1560 found_route_target_export_line
= line_exist(
1561 lines_to_del
, ctx_keys
, route_target_export_line
1563 found_route_target_both_line
= line_exist(
1564 lines_to_add
, ctx_keys
, route_target_both_line
1568 If the running configs has
1569 route-target import 1:1
1570 route-target export 1:1
1572 and the config we are reloading against has
1573 route-target both 1:1
1575 then we can ignore deleting the import/export and ignore adding the 'both'
1577 if found_route_target_export_line
and found_route_target_both_line
:
1578 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1579 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1580 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1582 # Deleting static routes under a vrf can lead to time-outs if each is sent
1583 # as separate vtysh -c commands. Change them from being in lines_to_del and
1584 # put the "no" form in lines_to_add
1585 if ctx_keys
[0].startswith("vrf ") and line
:
1586 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1587 add_cmd
= "no " + line
1588 lines_to_add
.append((ctx_keys
, add_cmd
))
1589 lines_to_del_to_del
.append((ctx_keys
, line
))
1592 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1595 lines_to_del_to_del
.append((ctx_keys
, line
))
1596 lines_to_add_to_del
.append((ctx_keys
, line
))
1599 We have commands that used to be displayed in the global part
1600 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1604 neighbor ISL advertisement-interval 0
1610 address-family ipv4 unicast
1611 neighbor ISL advertisement-interval 0
1613 Look to see if we are deleting it in one format just to add it back in the other
1616 ctx_keys
[0].startswith("router bgp")
1617 and len(ctx_keys
) > 1
1618 and ctx_keys
[1] == "address-family ipv4 unicast"
1620 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1621 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1623 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1626 lines_to_del_to_del
.append((ctx_keys
, line
))
1627 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1629 for (ctx_keys
, line
) in lines_to_del_to_del
:
1630 lines_to_del
.remove((ctx_keys
, line
))
1632 for (ctx_keys
, line
) in lines_to_add_to_del
:
1633 lines_to_add
.remove((ctx_keys
, line
))
1635 return (lines_to_add
, lines_to_del
)
1638 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1640 There are certain commands that cannot be removed. Remove
1641 those commands from lines_to_del.
1643 lines_to_del_to_del
= []
1645 for (ctx_keys
, line
) in lines_to_del
:
1648 ctx_keys
[0].startswith("frr version")
1649 or ctx_keys
[0].startswith("frr defaults")
1650 or ctx_keys
[0].startswith("username")
1651 or ctx_keys
[0].startswith("password")
1652 or ctx_keys
[0].startswith("line vty")
1654 # This is technically "no"able but if we did so frr-reload would
1655 # stop working so do not let the user shoot themselves in the foot
1657 ctx_keys
[0].startswith("service integrated-vtysh-config")
1660 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1661 lines_to_del_to_del
.append((ctx_keys
, line
))
1663 for (ctx_keys
, line
) in lines_to_del_to_del
:
1664 lines_to_del
.remove((ctx_keys
, line
))
1666 return (lines_to_add
, lines_to_del
)
1669 def compare_context_objects(newconf
, running
):
1671 Create a context diff for the two specified contexts
1674 # Compare the two Config objects to find the lines that we need to add/del
1681 candidates_to_add
= []
1684 # Find contexts that are in newconf but not in running
1685 # Find contexts that are in running but not in newconf
1686 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1688 if running_ctx_keys
not in newconf
.contexts
:
1690 # We check that the len is 1 here so that we only look at ('router bgp 10')
1691 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1692 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1693 # running but not in newconf.
1694 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1696 lines_to_del
.append((running_ctx_keys
, None))
1698 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1699 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1701 ].startswith("vrf"):
1702 for line
in running_ctx
.lines
:
1703 lines_to_del
.append((running_ctx_keys
, line
))
1705 # If this is an address-family under 'router bgp' and we are already deleting the
1706 # entire 'router bgp' context then ignore this sub-context
1708 "router bgp" in running_ctx_keys
[0]
1709 and len(running_ctx_keys
) > 1
1714 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1716 "router bgp" in running_ctx_keys
[0]
1717 and len(running_ctx_keys
) > 2
1718 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1719 and running_ctx_keys
[2].startswith("vni ")
1721 lines_to_del
.append((running_ctx_keys
, None))
1724 "router bgp" in running_ctx_keys
[0]
1725 and len(running_ctx_keys
) > 1
1726 and running_ctx_keys
[1].startswith("address-family")
1728 # There's no 'no address-family' support and so we have to
1729 # delete each line individually again
1730 for line
in running_ctx
.lines
:
1731 lines_to_del
.append((running_ctx_keys
, line
))
1733 # Some commands can happen at higher counts that make
1734 # doing vtysh -c inefficient (and can time out.) For
1735 # these commands, instead of adding them to lines_to_del,
1736 # add the "no " version to lines_to_add.
1737 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1739 ].startswith("ipv6 route"):
1740 add_cmd
= ("no " + running_ctx_keys
[0],)
1741 lines_to_add
.append((add_cmd
, None))
1743 # if this an interface sub-subcontext in an address-family block in ldpd and
1744 # we are already deleting the whole context, then ignore this
1746 len(running_ctx_keys
) > 2
1747 and running_ctx_keys
[0].startswith("mpls ldp")
1748 and running_ctx_keys
[1].startswith("address-family")
1749 and (running_ctx_keys
[:2], None) in lines_to_del
1753 # same thing for a pseudowire sub-context inside an l2vpn context
1755 len(running_ctx_keys
) > 1
1756 and running_ctx_keys
[0].startswith("l2vpn")
1757 and running_ctx_keys
[1].startswith("member pseudowire")
1758 and (running_ctx_keys
[:1], None) in lines_to_del
1762 # Segment routing and traffic engineering never need to be deleted
1764 running_ctx_keys
[0].startswith("segment-routing")
1765 and len(running_ctx_keys
) < 3
1769 # Neither the pcep command
1771 len(running_ctx_keys
) == 3
1772 and running_ctx_keys
[0].startswith("segment-routing")
1773 and running_ctx_keys
[2].startswith("pcep")
1777 # Segment lists can only be deleted after we removed all the candidate paths that
1778 # use them, so add them to a separate array that is going to be appended at the end
1780 len(running_ctx_keys
) == 3
1781 and running_ctx_keys
[0].startswith("segment-routing")
1782 and running_ctx_keys
[2].startswith("segment-list")
1784 seglist_to_del
.append((running_ctx_keys
, None))
1786 # Policies must be deleted after there candidate path, to be sure
1787 # we add them to a separate array that is going to be appended at the end
1789 len(running_ctx_keys
) == 3
1790 and running_ctx_keys
[0].startswith("segment-routing")
1791 and running_ctx_keys
[2].startswith("policy")
1793 pollist_to_del
.append((running_ctx_keys
, None))
1795 # pce-config must be deleted after the pce, to be sure we add them
1796 # to a separate array that is going to be appended at the end
1798 len(running_ctx_keys
) >= 4
1799 and running_ctx_keys
[0].startswith("segment-routing")
1800 and running_ctx_keys
[3].startswith("pce-config")
1802 pceconf_to_del
.append((running_ctx_keys
, None))
1804 # pcc must be deleted after the pce and pce-config too
1806 len(running_ctx_keys
) >= 4
1807 and running_ctx_keys
[0].startswith("segment-routing")
1808 and running_ctx_keys
[3].startswith("pcc")
1810 pcclist_to_del
.append((running_ctx_keys
, None))
1812 # Non-global context
1813 elif running_ctx_keys
and not any(
1814 "address-family" in key
for key
in running_ctx_keys
1816 lines_to_del
.append((running_ctx_keys
, None))
1818 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1819 lines_to_del
.append((running_ctx_keys
, None))
1823 for line
in running_ctx
.lines
:
1824 lines_to_del
.append((running_ctx_keys
, line
))
1826 # if we have some policies commands to delete, append them to lines_to_del
1827 if len(pollist_to_del
) > 0:
1828 lines_to_del
.extend(pollist_to_del
)
1830 # if we have some segment list commands to delete, append them to lines_to_del
1831 if len(seglist_to_del
) > 0:
1832 lines_to_del
.extend(seglist_to_del
)
1834 # if we have some pce list commands to delete, append them to lines_to_del
1835 if len(pceconf_to_del
) > 0:
1836 lines_to_del
.extend(pceconf_to_del
)
1838 # if we have some pcc list commands to delete, append them to lines_to_del
1839 if len(pcclist_to_del
) > 0:
1840 lines_to_del
.extend(pcclist_to_del
)
1842 # Find the lines within each context to add
1843 # Find the lines within each context to del
1844 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1846 if newconf_ctx_keys
in running
.contexts
:
1847 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1849 for line
in newconf_ctx
.lines
:
1850 if line
not in running_ctx
.dlines
:
1852 # candidate paths can only be added after the policy and segment list,
1853 # so add them to a separate array that is going to be appended at the end
1855 len(newconf_ctx_keys
) == 3
1856 and newconf_ctx_keys
[0].startswith("segment-routing")
1857 and newconf_ctx_keys
[2].startswith("policy ")
1858 and line
.startswith("candidate-path ")
1860 candidates_to_add
.append((newconf_ctx_keys
, line
))
1863 lines_to_add
.append((newconf_ctx_keys
, line
))
1865 for line
in running_ctx
.lines
:
1866 if line
not in newconf_ctx
.dlines
:
1867 lines_to_del
.append((newconf_ctx_keys
, line
))
1869 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1871 if newconf_ctx_keys
not in running
.contexts
:
1873 # candidate paths can only be added after the policy and segment list,
1874 # so add them to a separate array that is going to be appended at the end
1876 len(newconf_ctx_keys
) == 4
1877 and newconf_ctx_keys
[0].startswith("segment-routing")
1878 and newconf_ctx_keys
[3].startswith("candidate-path")
1880 candidates_to_add
.append((newconf_ctx_keys
, None))
1881 for line
in newconf_ctx
.lines
:
1882 candidates_to_add
.append((newconf_ctx_keys
, line
))
1885 lines_to_add
.append((newconf_ctx_keys
, None))
1887 for line
in newconf_ctx
.lines
:
1888 lines_to_add
.append((newconf_ctx_keys
, line
))
1890 # if we have some candidate paths commands to add, append them to lines_to_add
1891 if len(candidates_to_add
) > 0:
1892 lines_to_add
.extend(candidates_to_add
)
1894 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1895 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1896 lines_to_add
, lines_to_del
1898 (lines_to_add
, lines_to_del
) = delete_move_lines(lines_to_add
, lines_to_del
)
1899 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1900 lines_to_add
, lines_to_del
1903 return (lines_to_add
, lines_to_del
)
1906 if __name__
== "__main__":
1907 # Command line options
1908 parser
= argparse
.ArgumentParser(
1909 description
="Dynamically apply diff in frr configs"
1911 parser
.add_argument(
1912 "--input", help='Read running config from file instead of "show running"'
1914 group
= parser
.add_mutually_exclusive_group(required
=True)
1916 "--reload", action
="store_true", help="Apply the deltas", default
=False
1919 "--test", action
="store_true", help="Show the deltas", default
=False
1921 level_group
= parser
.add_mutually_exclusive_group()
1922 level_group
.add_argument(
1924 action
="store_true",
1925 help="Enable debugs (synonym for --log-level=debug)",
1928 level_group
.add_argument(
1932 choices
=("critical", "error", "warning", "info", "debug"),
1934 parser
.add_argument(
1935 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1937 parser
.add_argument(
1941 help="Reload specified path/namespace",
1944 parser
.add_argument("filename", help="Location of new frr config file")
1945 parser
.add_argument(
1947 action
="store_true",
1948 help="Overwrite frr.conf with running config output",
1951 parser
.add_argument(
1952 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1954 parser
.add_argument(
1955 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1957 parser
.add_argument(
1958 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1960 parser
.add_argument(
1962 help="socket to be used by vtysh to connect to the daemons",
1965 parser
.add_argument(
1966 "--daemon", help="daemon for which want to replace the config", default
=""
1969 args
= parser
.parse_args()
1972 # For --test log to stdout
1973 # For --reload log to /var/log/frr/frr-reload.log
1974 if args
.test
or args
.stdout
:
1975 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1977 # Color the errors and warnings in red
1978 logging
.addLevelName(
1979 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1981 logging
.addLevelName(
1982 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1986 if not os
.path
.isdir("/var/log/frr/"):
1987 os
.makedirs("/var/log/frr/")
1989 logging
.basicConfig(
1990 filename
="/var/log/frr/frr-reload.log",
1991 format
="%(asctime)s %(levelname)5s: %(message)s",
1994 # argparse should prevent this from happening but just to be safe...
1996 raise Exception("Must specify --reload or --test")
1997 log
= logging
.getLogger(__name__
)
2000 log
.setLevel(logging
.DEBUG
)
2002 log
.setLevel(args
.log_level
.upper())
2004 if args
.reload and not args
.stdout
:
2005 # Additionally send errors and above to STDOUT, with no metadata,
2006 # when we are logging to a file. This specifically does not follow
2007 # args.log_level, and is analagous to behaviour in earlier versions
2008 # which additionally logged most errors using print().
2010 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
2011 stdout_hdlr
.setLevel(logging
.ERROR
)
2012 stdout_hdlr
.setFormatter(logging
.Formatter())
2013 log
.addHandler(stdout_hdlr
)
2015 # Verify the new config file is valid
2016 if not os
.path
.isfile(args
.filename
):
2017 log
.error("Filename %s does not exist" % args
.filename
)
2020 if not os
.path
.getsize(args
.filename
):
2021 log
.error("Filename %s is an empty file" % args
.filename
)
2024 # Verify that confdir is correct
2025 if not os
.path
.isdir(args
.confdir
):
2026 log
.error("Confdir %s is not a valid path" % args
.confdir
)
2029 # Verify that bindir is correct
2030 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
2031 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
2034 # verify that the vty_socket, if specified, is valid
2035 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
2036 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
2039 # verify that the daemon, if specified, is valid
2040 if args
.daemon
and args
.daemon
not in [
2058 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
2063 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
2065 # Verify that 'service integrated-vtysh-config' is configured
2067 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
2069 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
2070 service_integrated_vtysh_config
= True
2072 if os
.path
.isfile(vtysh_filename
):
2073 with
open(vtysh_filename
, "r") as fh
:
2074 for line
in fh
.readlines():
2077 if line
== "no service integrated-vtysh-config":
2078 service_integrated_vtysh_config
= False
2081 if not service_integrated_vtysh_config
and not args
.daemon
:
2083 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
2087 log
.info('Called via "%s"', str(args
))
2089 # Create a Config object from the config generated by newconf
2090 newconf
= Config(vtysh
)
2092 newconf
.load_from_file(args
.filename
)
2094 except VtyshException
as ve
:
2095 log
.error("vtysh failed to process new configuration: {}".format(ve
))
2100 # Create a Config object from the running config
2101 running
= Config(vtysh
)
2104 running
.load_from_file(args
.input)
2106 running
.load_from_show_running(args
.daemon
)
2108 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
2109 lines_to_configure
= []
2112 print("\nLines To Delete")
2113 print("===============")
2115 for (ctx_keys
, line
) in lines_to_del
:
2120 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, True))
2121 lines_to_configure
.append(cmd
)
2125 print("\nLines To Add")
2126 print("============")
2128 for (ctx_keys
, line
) in lines_to_add
:
2133 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False))
2134 lines_to_configure
.append(cmd
)
2139 # We will not be able to do anything, go ahead and exit(1)
2140 if not vtysh
.is_config_available():
2143 log
.debug("New Frr Config\n%s", newconf
.get_lines())
2145 # This looks a little odd but we have to do this twice...here is why
2146 # If the user had this running bgp config:
2149 # neighbor 1.1.1.1 remote-as 50
2150 # neighbor 1.1.1.1 route-map FOO out
2152 # and this config in the newconf config file
2155 # neighbor 1.1.1.1 remote-as 999
2156 # neighbor 1.1.1.1 route-map FOO out
2159 # Then the script will do
2160 # - no neighbor 1.1.1.1 remote-as 50
2161 # - neighbor 1.1.1.1 remote-as 999
2163 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
2164 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
2165 # configs again to put this line back.
2167 # There are many keywords in FRR that can only appear one time under
2168 # a context, take "bgp router-id" for example. If the config that we are
2169 # reloading against has the following:
2172 # bgp router-id 1.1.1.1
2173 # bgp router-id 2.2.2.2
2175 # The final config needs to contain "bgp router-id 2.2.2.2". On the
2176 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
2177 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
2178 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
2179 # second pass to include all of the "adds" from the first pass.
2180 lines_to_add_first_pass
= []
2183 running
= Config(vtysh
)
2184 running
.load_from_show_running(args
.daemon
)
2185 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
2187 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
2190 lines_to_add_first_pass
= lines_to_add
2192 lines_to_add
.extend(lines_to_add_first_pass
)
2194 # Only do deletes on the first pass. The reason being if we
2195 # configure a bgp neighbor via "neighbor swp1 interface" FRR
2196 # will automatically add:
2199 # ipv6 nd ra-interval 10
2200 # no ipv6 nd suppress-ra
2203 # but those lines aren't in the config we are reloading against so
2204 # on the 2nd pass they will show up in lines_to_del. This could
2205 # apply to other scenarios as well where configuring FOO adds BAR
2207 if lines_to_del
and x
== 0:
2208 for (ctx_keys
, line
) in lines_to_del
:
2213 # 'no' commands are tricky, we can't just put them in a file and
2214 # vtysh -f that file. See the next comment for an explanation
2216 cmd
= lines_to_config(ctx_keys
, line
, True)
2219 # Some commands in frr are picky about taking a "no" of the entire line.
2220 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
2221 # only the beginning. If we hit one of these command an exception will be
2222 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2225 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2226 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2227 # % Unknown command.
2228 # frr(config-if)# no ip ospf authentication message-digest
2229 # % Unknown command.
2230 # frr(config-if)# no ip ospf authentication
2236 vtysh(["configure"] + cmd
, stdouts
)
2238 except VtyshException
:
2240 # - Pull the last entry from cmd (this would be
2241 # 'no ip ospf authentication message-digest 1.1.1.1' in
2243 # - Split that last entry by whitespace and drop the last word
2244 log
.info("Failed to execute %s", " ".join(cmd
))
2245 last_arg
= cmd
[-1].split(" ")
2247 if len(last_arg
) <= 2:
2249 '"%s" we failed to remove this command',
2250 " -- ".join(original_cmd
),
2252 # Log first error msg for original_cmd
2254 log
.error(stdouts
[0])
2258 new_last_arg
= last_arg
[0:-1]
2259 cmd
[-1] = " ".join(new_last_arg
)
2261 log
.info('Executed "%s"', " ".join(cmd
))
2265 lines_to_configure
= []
2267 for (ctx_keys
, line
) in lines_to_add
:
2272 # Don't run "no" commands twice since they can error
2273 # out the second time due to first deletion
2274 if x
== 1 and ctx_keys
[0].startswith("no "):
2277 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2278 lines_to_configure
.append(cmd
)
2280 if lines_to_configure
:
2281 random_string
= "".join(
2282 random
.SystemRandom().choice(
2283 string
.ascii_uppercase
+ string
.digits
2288 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2289 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2291 with
open(filename
, "w") as fh
:
2292 for line
in lines_to_configure
:
2293 fh
.write(line
+ "\n")
2296 vtysh
.exec_file(filename
)
2297 except VtyshException
as e
:
2298 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2302 # Make these changes persistent
2303 target
= str(args
.confdir
+ "/frr.conf")
2304 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):