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",
608 "vrrp autoconfigure",
612 for line
in self
.lines
:
617 if line
.startswith("!") or line
.startswith("#"):
622 and ctx_keys
[0].startswith("bfd")
623 and ctx_keys
[1].startswith("profile ")
626 log
.debug("LINE %-50s: popping from sub context, %-50s", line
, ctx_keys
)
629 self
.save_contexts(ctx_keys
, current_context_lines
)
630 ctx_keys
= copy
.deepcopy(main_ctx_key
)
631 current_context_lines
= []
635 # there is one exception though: ldpd accepts a 'router-id' clause
636 # as part of its 'mpls ldp' config context. If we are processing
637 # ldp configuration and encounter a router-id we should NOT switch
641 and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
)
644 and ctx_keys
[0].startswith("mpls ldp")
645 and line
.startswith("router-id ")
648 self
.save_contexts(ctx_keys
, current_context_lines
)
650 # Start a new context
655 current_context_lines
= []
657 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
658 self
.save_contexts(ctx_keys
, current_context_lines
)
662 self
.save_contexts(ctx_keys
, current_context_lines
)
663 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
665 # Start a new context
669 current_context_lines
= []
671 elif line
== "exit" and ctx_keys
[0].startswith("rpki"):
672 self
.save_contexts(ctx_keys
, current_context_lines
)
673 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
675 # Start a new context
679 current_context_lines
= []
681 elif line
== "exit-vrf":
682 self
.save_contexts(ctx_keys
, current_context_lines
)
683 current_context_lines
.append(line
)
685 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
688 # Start a new context
692 current_context_lines
= []
696 and len(ctx_keys
) > 1
697 and ctx_keys
[0].startswith("segment-routing")
699 self
.save_contexts(ctx_keys
, current_context_lines
)
701 # Start a new context
702 ctx_keys
= ctx_keys
[:-1]
703 current_context_lines
= []
705 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
710 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
711 # if this exit is for address-family ipv4 unicast, ignore the pop
713 self
.save_contexts(ctx_keys
, current_context_lines
)
715 # Start a new context
716 ctx_keys
= copy
.deepcopy(main_ctx_key
)
717 current_context_lines
= []
719 "LINE %-50s: popping from subcontext to ctx%-50s",
724 elif line
in ["exit-vni", "exit-ldp-if"]:
726 self
.save_contexts(ctx_keys
, current_context_lines
)
728 # Start a new context
729 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
730 current_context_lines
= []
732 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
737 elif new_ctx
is True:
743 ctx_keys
= copy
.deepcopy(main_ctx_key
)
746 current_context_lines
= []
748 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
751 line
.startswith("address-family ")
752 or line
.startswith("vnc defaults")
753 or line
.startswith("vnc l2-group")
754 or line
.startswith("vnc nve-group")
755 or line
.startswith("peer")
756 or line
.startswith("key ")
757 or line
.startswith("member pseudowire")
761 # Save old context first
762 self
.save_contexts(ctx_keys
, current_context_lines
)
763 current_context_lines
= []
764 main_ctx_key
= copy
.deepcopy(ctx_keys
)
765 log
.debug("LINE %-50s: entering sub-context, append to ctx_keys", line
)
767 if line
== "address-family ipv6" and not ctx_keys
[0].startswith(
770 ctx_keys
.append("address-family ipv6 unicast")
771 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith(
774 ctx_keys
.append("address-family ipv4 unicast")
775 elif line
== "address-family evpn":
776 ctx_keys
.append("address-family l2vpn evpn")
778 ctx_keys
.append(line
)
781 line
.startswith("vni ")
782 and len(ctx_keys
) == 2
783 and ctx_keys
[0].startswith("router bgp")
784 and ctx_keys
[1] == "address-family l2vpn evpn"
787 # Save old context first
788 self
.save_contexts(ctx_keys
, current_context_lines
)
789 current_context_lines
= []
790 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
792 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
794 ctx_keys
.append(line
)
797 line
.startswith("interface ")
798 and len(ctx_keys
) == 2
799 and ctx_keys
[0].startswith("mpls ldp")
800 and ctx_keys
[1].startswith("address-family")
803 # Save old context first
804 self
.save_contexts(ctx_keys
, current_context_lines
)
805 current_context_lines
= []
806 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
808 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
810 ctx_keys
.append(line
)
813 line
.startswith("traffic-eng")
814 and len(ctx_keys
) == 1
815 and ctx_keys
[0].startswith("segment-routing")
818 # Save old context first
819 self
.save_contexts(ctx_keys
, current_context_lines
)
820 current_context_lines
= []
822 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
825 ctx_keys
.append(line
)
828 line
.startswith("segment-list ")
829 and len(ctx_keys
) == 2
830 and ctx_keys
[0].startswith("segment-routing")
831 and ctx_keys
[1].startswith("traffic-eng")
834 # Save old context first
835 self
.save_contexts(ctx_keys
, current_context_lines
)
836 current_context_lines
= []
838 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
841 ctx_keys
.append(line
)
844 line
.startswith("policy ")
845 and len(ctx_keys
) == 2
846 and ctx_keys
[0].startswith("segment-routing")
847 and ctx_keys
[1].startswith("traffic-eng")
850 # Save old context first
851 self
.save_contexts(ctx_keys
, current_context_lines
)
852 current_context_lines
= []
854 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
857 ctx_keys
.append(line
)
860 line
.startswith("candidate-path ")
861 and line
.endswith(" dynamic")
862 and len(ctx_keys
) == 3
863 and ctx_keys
[0].startswith("segment-routing")
864 and ctx_keys
[1].startswith("traffic-eng")
865 and ctx_keys
[2].startswith("policy")
868 # Save old context first
869 self
.save_contexts(ctx_keys
, current_context_lines
)
870 current_context_lines
= []
871 main_ctx_key
= copy
.deepcopy(ctx_keys
)
873 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
876 ctx_keys
.append(line
)
879 line
.startswith("pcep")
880 and len(ctx_keys
) == 2
881 and ctx_keys
[0].startswith("segment-routing")
882 and ctx_keys
[1].startswith("traffic-eng")
885 # Save old context first
886 self
.save_contexts(ctx_keys
, current_context_lines
)
887 current_context_lines
= []
888 main_ctx_key
= copy
.deepcopy(ctx_keys
)
890 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
892 ctx_keys
.append(line
)
895 line
.startswith("pce-config ")
896 and len(ctx_keys
) == 3
897 and ctx_keys
[0].startswith("segment-routing")
898 and ctx_keys
[1].startswith("traffic-eng")
899 and ctx_keys
[2].startswith("pcep")
902 # Save old context first
903 self
.save_contexts(ctx_keys
, current_context_lines
)
904 current_context_lines
= []
905 main_ctx_key
= copy
.deepcopy(ctx_keys
)
907 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
910 ctx_keys
.append(line
)
913 line
.startswith("pce ")
914 and len(ctx_keys
) == 3
915 and ctx_keys
[0].startswith("segment-routing")
916 and ctx_keys
[1].startswith("traffic-eng")
917 and ctx_keys
[2].startswith("pcep")
920 # Save old context first
921 self
.save_contexts(ctx_keys
, current_context_lines
)
922 current_context_lines
= []
923 main_ctx_key
= copy
.deepcopy(ctx_keys
)
925 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
927 ctx_keys
.append(line
)
930 line
.startswith("pcc")
931 and len(ctx_keys
) == 3
932 and ctx_keys
[0].startswith("segment-routing")
933 and ctx_keys
[1].startswith("traffic-eng")
934 and ctx_keys
[2].startswith("pcep")
937 # Save old context first
938 self
.save_contexts(ctx_keys
, current_context_lines
)
939 current_context_lines
= []
940 main_ctx_key
= copy
.deepcopy(ctx_keys
)
942 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
944 ctx_keys
.append(line
)
947 line
.startswith("profile ")
948 and len(ctx_keys
) == 1
949 and ctx_keys
[0].startswith("bfd")
952 # Save old context first
953 self
.save_contexts(ctx_keys
, current_context_lines
)
954 current_context_lines
= []
955 main_ctx_key
= copy
.deepcopy(ctx_keys
)
957 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
960 ctx_keys
.append(line
)
963 # Continuing in an existing context, add non-commented lines to it
964 current_context_lines
.append(line
)
966 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
969 # Save the context of the last one
970 self
.save_contexts(ctx_keys
, current_context_lines
)
973 def lines_to_config(ctx_keys
, line
, delete
):
975 Return the command as it would appear in frr.conf
980 for (i
, ctx_key
) in enumerate(ctx_keys
):
981 cmd
.append(" " * i
+ ctx_key
)
984 indent
= len(ctx_keys
) * " "
986 # There are some commands that are on by default so their "no" form will be
987 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
988 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
989 # not by doing a "no no bgp default ipv4-unicast"
991 if line
.startswith("no "):
992 cmd
.append("%s%s" % (indent
, line
[3:]))
994 cmd
.append("%sno %s" % (indent
, line
))
997 cmd
.append(indent
+ line
)
999 # If line is None then we are typically deleting an entire
1000 # context ('no router ospf' for example)
1002 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
1003 cmd
.append("%s%s" % (" " * i
, ctx_key
))
1005 # Only put the 'no' on the last sub-context
1007 if ctx_keys
[-1].startswith("no "):
1008 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
1010 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
1012 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
1017 def get_normalized_ipv6_line(line
):
1019 Return a normalized IPv6 line as produced by frr,
1020 with all letters in lower case and trailing and leading
1021 zeros removed, and only the network portion present if
1022 the IPv6 word is a network
1025 words
= line
.split(" ")
1031 if "ipaddress" not in sys
.modules
:
1032 v6word
= IPNetwork(word
)
1033 norm_word
= "%s/%s" % (v6word
.network
, v6word
.prefixlen
)
1035 v6word
= ip_network(word
, strict
=False)
1036 norm_word
= "%s/%s" % (
1037 str(v6word
.network_address
),
1044 norm_word
= "%s" % IPv6Address(word
)
1049 norm_line
= norm_line
+ " " + norm_word
1051 return norm_line
.strip()
1054 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
1055 for (ctx_keys
, line
) in lines
:
1056 if ctx_keys
== target_ctx_keys
:
1058 if line
== target_line
:
1061 if line
.startswith(target_line
):
1066 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
1068 # exit-vrf is a bit tricky. If the new config is missing it but we
1069 # have configs under a vrf, we need to add it at the end to do the
1070 # right context changes. If exit-vrf exists in both the running and
1071 # new config, we cannot delete it or it will break context changes.
1072 add_exit_vrf
= False
1075 for (ctx_keys
, line
) in lines_to_add
:
1076 if add_exit_vrf
== True:
1077 if ctx_keys
[0] != prior_ctx_key
:
1078 insert_key
= ((prior_ctx_key
),)
1079 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
1080 add_exit_vrf
= False
1082 if ctx_keys
[0].startswith("vrf") and line
:
1083 if line
!= "exit-vrf":
1085 prior_ctx_key
= ctx_keys
[0]
1087 add_exit_vrf
= False
1090 for (ctx_keys
, line
) in lines_to_del
:
1091 if line
== "exit-vrf":
1092 if line_exist(lines_to_add
, ctx_keys
, line
):
1093 lines_to_del
.remove((ctx_keys
, line
))
1095 return (lines_to_add
, lines_to_del
)
1099 This method handles deletion of bgp peer group config.
1100 The objective is to delete config lines related to peers
1101 associated with the peer-group and move the peer-group
1102 config line to the end of the lines_to_del list.
1106 def delete_move_lines(lines_to_add
, lines_to_del
):
1109 # Stores the lines to move to the end of the pending list.
1110 lines_to_del_to_del
= []
1111 # Stores the lines to move to end of the pending list.
1112 lines_to_del_to_app
= []
1113 found_pg_del_cmd
= False
1116 When "neighbor <pg_name> peer-group" under a bgp instance is removed,
1117 it also deletes the associated peer config. Any config line below no form of
1118 peer-group related to a peer are errored out as the peer no longer exists.
1119 To cleanup peer-group and associated peer(s) configs:
1120 - Remove all the peers config lines from the pending list (lines_to_del list).
1121 - Move peer-group deletion line to the end of the pending list, to allow
1122 removal of any of the peer-group specific configs.
1124 Create a dictionary of config context (i.e. router bgp vrf x).
1125 Under each context node, create a dictionary of a peer-group name.
1126 Append a peer associated to the peer-group into a list under a peer-group node.
1127 Remove all of the peer associated config lines from the pending list.
1128 Append peer-group deletion line to end of the pending list.
1131 neighbor underlay peer-group
1132 neighbor underlay remote-as external
1133 neighbor underlay advertisement-interval 0
1134 neighbor underlay timers 3 9
1135 neighbor underlay timers connect 10
1136 neighbor swp1 interface peer-group underlay
1137 neighbor swp1 advertisement-interval 0
1138 neighbor swp1 timers 3 9
1139 neighbor swp1 timers connect 10
1140 neighbor swp2 interface peer-group underlay
1141 neighbor swp2 advertisement-interval 0
1142 neighbor swp2 timers 3 9
1143 neighbor swp2 timers connect 10
1144 neighbor swp3 interface peer-group underlay
1145 neighbor uplink1 interface remote-as internal
1146 neighbor uplink1 advertisement-interval 0
1147 neighbor uplink1 timers 3 9
1148 neighbor uplink1 timers connect 10
1151 "router bgp 200 no bgp bestpath as-path multipath-relax"
1152 "router bgp 200 no neighbor underlay advertisement-interval 0"
1153 "router bgp 200 no neighbor underlay timers 3 9"
1154 "router bgp 200 no neighbor underlay timers connect 10"
1155 "router bgp 200 no neighbor uplink1 advertisement-interval 0"
1156 "router bgp 200 no neighbor uplink1 timers 3 9"
1157 "router bgp 200 no neighbor uplink1 timers connect 10"
1158 "router bgp 200 no neighbor underlay remote-as external"
1159 "router bgp 200 no neighbor uplink1 interface remote-as internal"
1160 "router bgp 200 no neighbor underlay peer-group"
1164 for (ctx_keys
, line
) in lines_to_del
:
1166 ctx_keys
[0].startswith("router bgp")
1168 and line
.startswith("neighbor ")
1171 When 'neighbor <peer> remote-as <>' is removed it deletes the peer,
1172 there might be a peer associated config which also needs to be removed
1174 Append the 'neighbor <peer> remote-as <>' to the lines_to_del.
1177 neighbor uplink1 interface remote-as internal
1178 neighbor uplink1 advertisement-interval 0
1179 neighbor uplink1 timers 3 9
1180 neighbor uplink1 timers connect 10
1183 neighbor uplink1 advertisement-interval 0
1184 neighbor uplink1 timers 3 9
1185 neighbor uplink1 timers connect 10
1188 neighbor uplink1 interface remote-as internal
1191 # 'no neighbor peer [interface] remote-as <>'
1192 nb_remoteas
= "neighbor (\S+) .*remote-as (\S+)"
1193 re_nb_remoteas
= re
.search(nb_remoteas
, line
)
1195 lines_to_del_to_app
.append((ctx_keys
, line
))
1198 {'router bgp 65001': {'PG': [], 'PG1': []},
1199 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}}
1201 if ctx_keys
[0] not in del_dict
:
1202 del_dict
[ctx_keys
[0]] = dict()
1203 # find 'no neighbor <pg_name> peer-group'
1204 re_pg
= re
.match("neighbor (\S+) peer-group$", line
)
1205 if re_pg
and re_pg
.group(1) not in del_dict
[ctx_keys
[0]]:
1206 del_dict
[ctx_keys
[0]][re_pg
.group(1)] = list()
1208 for (ctx_keys
, line
) in lines_to_del_to_app
:
1209 lines_to_del
.remove((ctx_keys
, line
))
1210 lines_to_del
.append((ctx_keys
, line
))
1212 if found_pg_del_cmd
== False:
1213 return (lines_to_add
, lines_to_del
)
1216 {'router bgp 65001': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']},
1217 'router bgp 65001 vrf vrf1': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}}
1219 for (ctx_keys
, line
) in lines_to_del
:
1221 ctx_keys
[0].startswith("router bgp")
1223 and line
.startswith("neighbor ")
1225 if ctx_keys
[0] in del_dict
:
1226 for pg_key
in del_dict
[ctx_keys
[0]]:
1227 # 'neighbor <peer> [interface] peer-group <pg_name>'
1228 nb_pg
= "neighbor (\S+) .*peer-group %s$" % pg_key
1229 re_nbr_pg
= re
.search(nb_pg
, line
)
1232 and re_nbr_pg
.group(1) not in del_dict
[ctx_keys
[0]][pg_key
]
1234 del_dict
[ctx_keys
[0]][pg_key
].append(re_nbr_pg
.group(1))
1236 lines_to_del_to_app
= []
1237 for (ctx_keys
, line
) in lines_to_del
:
1239 ctx_keys
[0].startswith("router bgp")
1241 and line
.startswith("neighbor ")
1243 if ctx_keys
[0] in del_dict
:
1244 for pg
in del_dict
[ctx_keys
[0]]:
1245 for nbr
in del_dict
[ctx_keys
[0]][pg
]:
1246 nb_exp
= "neighbor %s .*" % nbr
1247 re_nb
= re
.search(nb_exp
, line
)
1248 # add peer configs to delete list.
1249 if re_nb
and line
not in lines_to_del_to_del
:
1250 lines_to_del_to_del
.append((ctx_keys
, line
))
1252 pg_exp
= "neighbor %s peer-group$" % pg
1253 re_pg
= re
.match(pg_exp
, line
)
1255 lines_to_del_to_app
.append((ctx_keys
, line
))
1257 for (ctx_keys
, line
) in lines_to_del_to_del
:
1258 lines_to_del
.remove((ctx_keys
, line
))
1260 for (ctx_keys
, line
) in lines_to_del_to_app
:
1261 lines_to_del
.remove((ctx_keys
, line
))
1262 lines_to_del
.append((ctx_keys
, line
))
1264 return (lines_to_add
, lines_to_del
)
1267 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1269 # Quite possibly the most confusing (while accurate) variable names in history
1270 lines_to_add_to_del
= []
1271 lines_to_del_to_del
= []
1273 for (ctx_keys
, line
) in lines_to_del
:
1276 # If there is a change in the segment routing block ranges, do it
1277 # in-place, to avoid requesting spurious label chunks which might fail
1278 if line
and "segment-routing global-block" in line
:
1279 for (add_key
, add_line
) in lines_to_add
:
1281 ctx_keys
[0] == add_key
[0]
1283 and "segment-routing global-block" in add_line
1285 lines_to_del_to_del
.append((ctx_keys
, line
))
1289 if ctx_keys
[0].startswith("router bgp") and line
:
1291 if line
.startswith("neighbor "):
1293 BGP changed how it displays swpX peers that are part of peer-group. Older
1294 versions of frr would display these on separate lines:
1295 neighbor swp1 interface
1296 neighbor swp1 peer-group FOO
1298 but today we display via a single line
1299 neighbor swp1 interface peer-group FOO
1301 This change confuses frr-reload.py so check to see if we are deleting
1302 neighbor swp1 interface peer-group FOO
1305 neighbor swp1 interface
1306 neighbor swp1 peer-group FOO
1308 If so then chop the del line and the corresponding add lines
1311 re_swpx_int_peergroup
= re
.search(
1312 "neighbor (\S+) interface peer-group (\S+)", line
1314 re_swpx_int_v6only_peergroup
= re
.search(
1315 "neighbor (\S+) interface v6only peer-group (\S+)", line
1318 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1319 swpx_interface
= None
1320 swpx_peergroup
= None
1322 if re_swpx_int_peergroup
:
1323 swpx
= re_swpx_int_peergroup
.group(1)
1324 peergroup
= re_swpx_int_peergroup
.group(2)
1325 swpx_interface
= "neighbor %s interface" % swpx
1326 elif re_swpx_int_v6only_peergroup
:
1327 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1328 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1329 swpx_interface
= "neighbor %s interface v6only" % swpx
1331 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1332 found_add_swpx_interface
= line_exist(
1333 lines_to_add
, ctx_keys
, swpx_interface
1335 found_add_swpx_peergroup
= line_exist(
1336 lines_to_add
, ctx_keys
, swpx_peergroup
1338 tmp_ctx_keys
= tuple(list(ctx_keys
))
1340 if not found_add_swpx_peergroup
:
1341 tmp_ctx_keys
= list(ctx_keys
)
1342 tmp_ctx_keys
.append("address-family ipv4 unicast")
1343 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1344 found_add_swpx_peergroup
= line_exist(
1345 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1348 if not found_add_swpx_peergroup
:
1349 tmp_ctx_keys
= list(ctx_keys
)
1350 tmp_ctx_keys
.append("address-family ipv6 unicast")
1351 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1352 found_add_swpx_peergroup
= line_exist(
1353 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1356 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1358 lines_to_del_to_del
.append((ctx_keys
, line
))
1359 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1360 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1363 Changing the bfd timers on neighbors is allowed without doing
1364 a delete/add process. Since doing a "no neighbor blah bfd ..."
1365 will cause the peer to bounce unnecessarily, just skip the delete
1366 and just do the add.
1368 re_nbr_bfd_timers
= re
.search(
1369 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1372 if re_nbr_bfd_timers
:
1373 nbr
= re_nbr_bfd_timers
.group(1)
1374 bfd_nbr
= "neighbor %s" % nbr
1375 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1377 for (ctx_keys
, add_line
) in lines_to_add
:
1378 if ctx_keys
[0].startswith("router bgp"):
1379 re_add_nbr_bfd_timers
= re
.search(
1380 bfd_search_string
, add_line
1383 if re_add_nbr_bfd_timers
:
1384 found_add_bfd_nbr
= line_exist(
1385 lines_to_add
, ctx_keys
, bfd_nbr
, False
1388 if found_add_bfd_nbr
:
1389 lines_to_del_to_del
.append((ctx_keys
, line
))
1392 We changed how we display the neighbor interface command. Older
1393 versions of frr would display the following:
1394 neighbor swp1 interface
1395 neighbor swp1 remote-as external
1396 neighbor swp1 capability extended-nexthop
1398 but today we display via a single line
1399 neighbor swp1 interface remote-as external
1401 and capability extended-nexthop is no longer needed because we
1402 automatically enable it when the neighbor is of type interface.
1404 This change confuses frr-reload.py so check to see if we are deleting
1405 neighbor swp1 interface remote-as (external|internal|ASNUM)
1408 neighbor swp1 interface
1409 neighbor swp1 remote-as (external|internal|ASNUM)
1410 neighbor swp1 capability extended-nexthop
1412 If so then chop the del line and the corresponding add lines
1414 re_swpx_int_remoteas
= re
.search(
1415 "neighbor (\S+) interface remote-as (\S+)", line
1417 re_swpx_int_v6only_remoteas
= re
.search(
1418 "neighbor (\S+) interface v6only remote-as (\S+)", line
1421 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1422 swpx_interface
= None
1423 swpx_remoteas
= None
1425 if re_swpx_int_remoteas
:
1426 swpx
= re_swpx_int_remoteas
.group(1)
1427 remoteas
= re_swpx_int_remoteas
.group(2)
1428 swpx_interface
= "neighbor %s interface" % swpx
1429 elif re_swpx_int_v6only_remoteas
:
1430 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1431 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1432 swpx_interface
= "neighbor %s interface v6only" % swpx
1434 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1435 found_add_swpx_interface
= line_exist(
1436 lines_to_add
, ctx_keys
, swpx_interface
1438 found_add_swpx_remoteas
= line_exist(
1439 lines_to_add
, ctx_keys
, swpx_remoteas
1441 tmp_ctx_keys
= tuple(list(ctx_keys
))
1443 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1445 lines_to_del_to_del
.append((ctx_keys
, line
))
1446 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1447 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1450 We made the 'bgp bestpath as-path multipath-relax' command
1451 automatically assume 'no-as-set' since the lack of this option caused
1452 weird routing problems. When the running config is shown in
1453 releases with this change, the no-as-set keyword is not shown as it
1454 is the default. This causes frr-reload to unnecessarily unapply
1455 this option only to apply it back again, causing unnecessary session
1458 if "multipath-relax" in line
:
1459 re_asrelax_new
= re
.search(
1460 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1462 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1463 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1465 if re_asrelax_new
and found_asrelax_old
:
1467 lines_to_del_to_del
.append((ctx_keys
, line
))
1468 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1471 If we are modifying the BGP table-map we need to avoid a del/add and
1472 instead modify the table-map in place via an add. This is needed to
1473 avoid installing all routes in the RIB the second the 'no table-map'
1476 if line
.startswith("table-map"):
1477 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1480 lines_to_del_to_del
.append((ctx_keys
, line
))
1483 More old-to-new config handling. ip import-table no longer accepts
1484 distance, but we honor the old syntax. But 'show running' shows only
1485 the new syntax. This causes an unnecessary 'no import-table' followed
1486 by the same old 'ip import-table' which causes perturbations in
1487 announced routes leading to traffic blackholes. Fix this issue.
1489 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1491 table_num
= re_importtbl
.group(1)
1492 for ctx
in lines_to_add
:
1493 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1494 lines_to_del_to_del
.append(
1495 (("ip import-table %s" % table_num
,), None)
1497 lines_to_add_to_del
.append((ctx
[0], None))
1500 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1501 However, the running config always adds 'seq x', where x is a number
1502 incremented by 5 for every element of the prefix/access list.
1503 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1504 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1505 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1506 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1507 access-list FOO seq 5 permit 2.2.2.2/32
1508 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1510 re_acl_pfxlst
= re
.search(
1511 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1517 re_acl_pfxlst
.group(1)
1518 + re_acl_pfxlst
.group(2)
1519 + re_acl_pfxlst
.group(3)
1520 + re_acl_pfxlst
.group(5)
1521 + re_acl_pfxlst
.group(6)
1523 for ctx
in lines_to_add
:
1524 if ctx
[0][0] == tmpline
:
1525 lines_to_del_to_del
.append((ctx_keys
, None))
1526 lines_to_add_to_del
.append(((tmpline
,), None))
1529 If prefix-lists or access-lists are being deleted and
1530 not added (see comment above), add command with 'no' to
1531 lines_to_add and remove from lines_to_del to improve
1532 scaling performance.
1535 add_cmd
= ("no " + ctx_keys
[0],)
1536 lines_to_add
.append((add_cmd
, None))
1537 lines_to_del_to_del
.append((ctx_keys
, None))
1541 and ctx_keys
[0].startswith("router bgp")
1542 and ctx_keys
[1] == "address-family l2vpn evpn"
1543 and ctx_keys
[2].startswith("vni")
1547 re
.search("^route-target import (.*)$", line
)
1553 rt
= re_route_target
.group(1).strip()
1554 route_target_import_line
= line
1555 route_target_export_line
= "route-target export %s" % rt
1556 route_target_both_line
= "route-target both %s" % rt
1558 found_route_target_export_line
= line_exist(
1559 lines_to_del
, ctx_keys
, route_target_export_line
1561 found_route_target_both_line
= line_exist(
1562 lines_to_add
, ctx_keys
, route_target_both_line
1566 If the running configs has
1567 route-target import 1:1
1568 route-target export 1:1
1570 and the config we are reloading against has
1571 route-target both 1:1
1573 then we can ignore deleting the import/export and ignore adding the 'both'
1575 if found_route_target_export_line
and found_route_target_both_line
:
1576 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1577 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1578 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1580 # Deleting static routes under a vrf can lead to time-outs if each is sent
1581 # as separate vtysh -c commands. Change them from being in lines_to_del and
1582 # put the "no" form in lines_to_add
1583 if ctx_keys
[0].startswith("vrf ") and line
:
1584 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1585 add_cmd
= "no " + line
1586 lines_to_add
.append((ctx_keys
, add_cmd
))
1587 lines_to_del_to_del
.append((ctx_keys
, line
))
1590 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1593 lines_to_del_to_del
.append((ctx_keys
, line
))
1594 lines_to_add_to_del
.append((ctx_keys
, line
))
1597 We have commands that used to be displayed in the global part
1598 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1602 neighbor ISL advertisement-interval 0
1608 address-family ipv4 unicast
1609 neighbor ISL advertisement-interval 0
1611 Look to see if we are deleting it in one format just to add it back in the other
1614 ctx_keys
[0].startswith("router bgp")
1615 and len(ctx_keys
) > 1
1616 and ctx_keys
[1] == "address-family ipv4 unicast"
1618 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1619 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1621 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1624 lines_to_del_to_del
.append((ctx_keys
, line
))
1625 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1627 for (ctx_keys
, line
) in lines_to_del_to_del
:
1628 lines_to_del
.remove((ctx_keys
, line
))
1630 for (ctx_keys
, line
) in lines_to_add_to_del
:
1631 lines_to_add
.remove((ctx_keys
, line
))
1633 return (lines_to_add
, lines_to_del
)
1636 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1638 There are certain commands that cannot be removed. Remove
1639 those commands from lines_to_del.
1641 lines_to_del_to_del
= []
1643 for (ctx_keys
, line
) in lines_to_del
:
1646 ctx_keys
[0].startswith("frr version")
1647 or ctx_keys
[0].startswith("frr defaults")
1648 or ctx_keys
[0].startswith("username")
1649 or ctx_keys
[0].startswith("password")
1650 or ctx_keys
[0].startswith("line vty")
1652 # This is technically "no"able but if we did so frr-reload would
1653 # stop working so do not let the user shoot themselves in the foot
1655 ctx_keys
[0].startswith("service integrated-vtysh-config")
1658 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1659 lines_to_del_to_del
.append((ctx_keys
, line
))
1661 for (ctx_keys
, line
) in lines_to_del_to_del
:
1662 lines_to_del
.remove((ctx_keys
, line
))
1664 return (lines_to_add
, lines_to_del
)
1667 def compare_context_objects(newconf
, running
):
1669 Create a context diff for the two specified contexts
1672 # Compare the two Config objects to find the lines that we need to add/del
1679 candidates_to_add
= []
1682 # Find contexts that are in newconf but not in running
1683 # Find contexts that are in running but not in newconf
1684 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1686 if running_ctx_keys
not in newconf
.contexts
:
1688 # We check that the len is 1 here so that we only look at ('router bgp 10')
1689 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1690 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1691 # running but not in newconf.
1692 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1694 lines_to_del
.append((running_ctx_keys
, None))
1696 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1697 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1699 ].startswith("vrf"):
1700 for line
in running_ctx
.lines
:
1701 lines_to_del
.append((running_ctx_keys
, line
))
1703 # If this is an address-family under 'router bgp' and we are already deleting the
1704 # entire 'router bgp' context then ignore this sub-context
1706 "router bgp" in running_ctx_keys
[0]
1707 and len(running_ctx_keys
) > 1
1712 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1714 "router bgp" in running_ctx_keys
[0]
1715 and len(running_ctx_keys
) > 2
1716 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1717 and running_ctx_keys
[2].startswith("vni ")
1719 lines_to_del
.append((running_ctx_keys
, None))
1722 "router bgp" in running_ctx_keys
[0]
1723 and len(running_ctx_keys
) > 1
1724 and running_ctx_keys
[1].startswith("address-family")
1726 # There's no 'no address-family' support and so we have to
1727 # delete each line individually again
1728 for line
in running_ctx
.lines
:
1729 lines_to_del
.append((running_ctx_keys
, line
))
1731 # Some commands can happen at higher counts that make
1732 # doing vtysh -c inefficient (and can time out.) For
1733 # these commands, instead of adding them to lines_to_del,
1734 # add the "no " version to lines_to_add.
1735 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1737 ].startswith("ipv6 route"):
1738 add_cmd
= ("no " + running_ctx_keys
[0],)
1739 lines_to_add
.append((add_cmd
, None))
1741 # if this an interface sub-subcontext in an address-family block in ldpd and
1742 # we are already deleting the whole context, then ignore this
1744 len(running_ctx_keys
) > 2
1745 and running_ctx_keys
[0].startswith("mpls ldp")
1746 and running_ctx_keys
[1].startswith("address-family")
1747 and (running_ctx_keys
[:2], None) in lines_to_del
1751 # same thing for a pseudowire sub-context inside an l2vpn context
1753 len(running_ctx_keys
) > 1
1754 and running_ctx_keys
[0].startswith("l2vpn")
1755 and running_ctx_keys
[1].startswith("member pseudowire")
1756 and (running_ctx_keys
[:1], None) in lines_to_del
1760 # Segment routing and traffic engineering never need to be deleted
1762 running_ctx_keys
[0].startswith("segment-routing")
1763 and len(running_ctx_keys
) < 3
1767 # Neither the pcep command
1769 len(running_ctx_keys
) == 3
1770 and running_ctx_keys
[0].startswith("segment-routing")
1771 and running_ctx_keys
[2].startswith("pcep")
1775 # Segment lists can only be deleted after we removed all the candidate paths that
1776 # use them, so add them to a separate array that is going to be appended at the end
1778 len(running_ctx_keys
) == 3
1779 and running_ctx_keys
[0].startswith("segment-routing")
1780 and running_ctx_keys
[2].startswith("segment-list")
1782 seglist_to_del
.append((running_ctx_keys
, None))
1784 # Policies must be deleted after there candidate path, to be sure
1785 # we add them to a separate array that is going to be appended at the end
1787 len(running_ctx_keys
) == 3
1788 and running_ctx_keys
[0].startswith("segment-routing")
1789 and running_ctx_keys
[2].startswith("policy")
1791 pollist_to_del
.append((running_ctx_keys
, None))
1793 # pce-config must be deleted after the pce, to be sure we add them
1794 # to a separate array that is going to be appended at the end
1796 len(running_ctx_keys
) >= 4
1797 and running_ctx_keys
[0].startswith("segment-routing")
1798 and running_ctx_keys
[3].startswith("pce-config")
1800 pceconf_to_del
.append((running_ctx_keys
, None))
1802 # pcc must be deleted after the pce and pce-config too
1804 len(running_ctx_keys
) >= 4
1805 and running_ctx_keys
[0].startswith("segment-routing")
1806 and running_ctx_keys
[3].startswith("pcc")
1808 pcclist_to_del
.append((running_ctx_keys
, None))
1810 # Non-global context
1811 elif running_ctx_keys
and not any(
1812 "address-family" in key
for key
in running_ctx_keys
1814 lines_to_del
.append((running_ctx_keys
, None))
1816 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1817 lines_to_del
.append((running_ctx_keys
, None))
1821 for line
in running_ctx
.lines
:
1822 lines_to_del
.append((running_ctx_keys
, line
))
1824 # if we have some policies commands to delete, append them to lines_to_del
1825 if len(pollist_to_del
) > 0:
1826 lines_to_del
.extend(pollist_to_del
)
1828 # if we have some segment list commands to delete, append them to lines_to_del
1829 if len(seglist_to_del
) > 0:
1830 lines_to_del
.extend(seglist_to_del
)
1832 # if we have some pce list commands to delete, append them to lines_to_del
1833 if len(pceconf_to_del
) > 0:
1834 lines_to_del
.extend(pceconf_to_del
)
1836 # if we have some pcc list commands to delete, append them to lines_to_del
1837 if len(pcclist_to_del
) > 0:
1838 lines_to_del
.extend(pcclist_to_del
)
1840 # Find the lines within each context to add
1841 # Find the lines within each context to del
1842 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1844 if newconf_ctx_keys
in running
.contexts
:
1845 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1847 for line
in newconf_ctx
.lines
:
1848 if line
not in running_ctx
.dlines
:
1850 # candidate paths can only be added after the policy and segment list,
1851 # so add them to a separate array that is going to be appended at the end
1853 len(newconf_ctx_keys
) == 3
1854 and newconf_ctx_keys
[0].startswith("segment-routing")
1855 and newconf_ctx_keys
[2].startswith("policy ")
1856 and line
.startswith("candidate-path ")
1858 candidates_to_add
.append((newconf_ctx_keys
, line
))
1861 lines_to_add
.append((newconf_ctx_keys
, line
))
1863 for line
in running_ctx
.lines
:
1864 if line
not in newconf_ctx
.dlines
:
1865 lines_to_del
.append((newconf_ctx_keys
, line
))
1867 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1869 if newconf_ctx_keys
not in running
.contexts
:
1871 # candidate paths can only be added after the policy and segment list,
1872 # so add them to a separate array that is going to be appended at the end
1874 len(newconf_ctx_keys
) == 4
1875 and newconf_ctx_keys
[0].startswith("segment-routing")
1876 and newconf_ctx_keys
[3].startswith("candidate-path")
1878 candidates_to_add
.append((newconf_ctx_keys
, None))
1879 for line
in newconf_ctx
.lines
:
1880 candidates_to_add
.append((newconf_ctx_keys
, line
))
1883 lines_to_add
.append((newconf_ctx_keys
, None))
1885 for line
in newconf_ctx
.lines
:
1886 lines_to_add
.append((newconf_ctx_keys
, line
))
1888 # if we have some candidate paths commands to add, append them to lines_to_add
1889 if len(candidates_to_add
) > 0:
1890 lines_to_add
.extend(candidates_to_add
)
1892 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1893 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1894 lines_to_add
, lines_to_del
1896 (lines_to_add
, lines_to_del
) = delete_move_lines(lines_to_add
, lines_to_del
)
1897 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1898 lines_to_add
, lines_to_del
1901 return (lines_to_add
, lines_to_del
)
1904 if __name__
== "__main__":
1905 # Command line options
1906 parser
= argparse
.ArgumentParser(
1907 description
="Dynamically apply diff in frr configs"
1909 parser
.add_argument(
1910 "--input", help='Read running config from file instead of "show running"'
1912 group
= parser
.add_mutually_exclusive_group(required
=True)
1914 "--reload", action
="store_true", help="Apply the deltas", default
=False
1917 "--test", action
="store_true", help="Show the deltas", default
=False
1919 level_group
= parser
.add_mutually_exclusive_group()
1920 level_group
.add_argument(
1922 action
="store_true",
1923 help="Enable debugs (synonym for --log-level=debug)",
1926 level_group
.add_argument(
1930 choices
=("critical", "error", "warning", "info", "debug"),
1932 parser
.add_argument(
1933 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1935 parser
.add_argument(
1939 help="Reload specified path/namespace",
1942 parser
.add_argument("filename", help="Location of new frr config file")
1943 parser
.add_argument(
1945 action
="store_true",
1946 help="Overwrite frr.conf with running config output",
1949 parser
.add_argument(
1950 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1952 parser
.add_argument(
1953 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1955 parser
.add_argument(
1956 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1958 parser
.add_argument(
1960 help="socket to be used by vtysh to connect to the daemons",
1963 parser
.add_argument(
1964 "--daemon", help="daemon for which want to replace the config", default
=""
1967 args
= parser
.parse_args()
1970 # For --test log to stdout
1971 # For --reload log to /var/log/frr/frr-reload.log
1972 if args
.test
or args
.stdout
:
1973 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1975 # Color the errors and warnings in red
1976 logging
.addLevelName(
1977 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1979 logging
.addLevelName(
1980 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1984 if not os
.path
.isdir("/var/log/frr/"):
1985 os
.makedirs("/var/log/frr/")
1987 logging
.basicConfig(
1988 filename
="/var/log/frr/frr-reload.log",
1989 format
="%(asctime)s %(levelname)5s: %(message)s",
1992 # argparse should prevent this from happening but just to be safe...
1994 raise Exception("Must specify --reload or --test")
1995 log
= logging
.getLogger(__name__
)
1998 log
.setLevel(logging
.DEBUG
)
2000 log
.setLevel(args
.log_level
.upper())
2002 if args
.reload and not args
.stdout
:
2003 # Additionally send errors and above to STDOUT, with no metadata,
2004 # when we are logging to a file. This specifically does not follow
2005 # args.log_level, and is analagous to behaviour in earlier versions
2006 # which additionally logged most errors using print().
2008 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
2009 stdout_hdlr
.setLevel(logging
.ERROR
)
2010 stdout_hdlr
.setFormatter(logging
.Formatter())
2011 log
.addHandler(stdout_hdlr
)
2013 # Verify the new config file is valid
2014 if not os
.path
.isfile(args
.filename
):
2015 log
.error("Filename %s does not exist" % args
.filename
)
2018 if not os
.path
.getsize(args
.filename
):
2019 log
.error("Filename %s is an empty file" % args
.filename
)
2022 # Verify that confdir is correct
2023 if not os
.path
.isdir(args
.confdir
):
2024 log
.error("Confdir %s is not a valid path" % args
.confdir
)
2027 # Verify that bindir is correct
2028 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
2029 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
2032 # verify that the vty_socket, if specified, is valid
2033 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
2034 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
2037 # verify that the daemon, if specified, is valid
2038 if args
.daemon
and args
.daemon
not in [
2056 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
2061 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
2063 # Verify that 'service integrated-vtysh-config' is configured
2065 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
2067 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
2068 service_integrated_vtysh_config
= True
2070 if os
.path
.isfile(vtysh_filename
):
2071 with
open(vtysh_filename
, "r") as fh
:
2072 for line
in fh
.readlines():
2075 if line
== "no service integrated-vtysh-config":
2076 service_integrated_vtysh_config
= False
2079 if not service_integrated_vtysh_config
and not args
.daemon
:
2081 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
2085 log
.info('Called via "%s"', str(args
))
2087 # Create a Config object from the config generated by newconf
2088 newconf
= Config(vtysh
)
2090 newconf
.load_from_file(args
.filename
)
2092 except VtyshException
as ve
:
2093 log
.error("vtysh failed to process new configuration: {}".format(ve
))
2098 # Create a Config object from the running config
2099 running
= Config(vtysh
)
2102 running
.load_from_file(args
.input)
2104 running
.load_from_show_running(args
.daemon
)
2106 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
2107 lines_to_configure
= []
2110 print("\nLines To Delete")
2111 print("===============")
2113 for (ctx_keys
, line
) in lines_to_del
:
2118 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, True))
2119 lines_to_configure
.append(cmd
)
2123 print("\nLines To Add")
2124 print("============")
2126 for (ctx_keys
, line
) in lines_to_add
:
2131 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False))
2132 lines_to_configure
.append(cmd
)
2137 # We will not be able to do anything, go ahead and exit(1)
2138 if not vtysh
.is_config_available():
2141 log
.debug("New Frr Config\n%s", newconf
.get_lines())
2143 # This looks a little odd but we have to do this twice...here is why
2144 # If the user had this running bgp config:
2147 # neighbor 1.1.1.1 remote-as 50
2148 # neighbor 1.1.1.1 route-map FOO out
2150 # and this config in the newconf config file
2153 # neighbor 1.1.1.1 remote-as 999
2154 # neighbor 1.1.1.1 route-map FOO out
2157 # Then the script will do
2158 # - no neighbor 1.1.1.1 remote-as 50
2159 # - neighbor 1.1.1.1 remote-as 999
2161 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
2162 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
2163 # configs again to put this line back.
2165 # There are many keywords in FRR that can only appear one time under
2166 # a context, take "bgp router-id" for example. If the config that we are
2167 # reloading against has the following:
2170 # bgp router-id 1.1.1.1
2171 # bgp router-id 2.2.2.2
2173 # The final config needs to contain "bgp router-id 2.2.2.2". On the
2174 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
2175 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
2176 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
2177 # second pass to include all of the "adds" from the first pass.
2178 lines_to_add_first_pass
= []
2181 running
= Config(vtysh
)
2182 running
.load_from_show_running(args
.daemon
)
2183 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
2185 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
2188 lines_to_add_first_pass
= lines_to_add
2190 lines_to_add
.extend(lines_to_add_first_pass
)
2192 # Only do deletes on the first pass. The reason being if we
2193 # configure a bgp neighbor via "neighbor swp1 interface" FRR
2194 # will automatically add:
2197 # ipv6 nd ra-interval 10
2198 # no ipv6 nd suppress-ra
2201 # but those lines aren't in the config we are reloading against so
2202 # on the 2nd pass they will show up in lines_to_del. This could
2203 # apply to other scenarios as well where configuring FOO adds BAR
2205 if lines_to_del
and x
== 0:
2206 for (ctx_keys
, line
) in lines_to_del
:
2211 # 'no' commands are tricky, we can't just put them in a file and
2212 # vtysh -f that file. See the next comment for an explanation
2214 cmd
= lines_to_config(ctx_keys
, line
, True)
2217 # Some commands in frr are picky about taking a "no" of the entire line.
2218 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
2219 # only the beginning. If we hit one of these command an exception will be
2220 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2223 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2224 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2225 # % Unknown command.
2226 # frr(config-if)# no ip ospf authentication message-digest
2227 # % Unknown command.
2228 # frr(config-if)# no ip ospf authentication
2234 vtysh(["configure"] + cmd
, stdouts
)
2236 except VtyshException
:
2238 # - Pull the last entry from cmd (this would be
2239 # 'no ip ospf authentication message-digest 1.1.1.1' in
2241 # - Split that last entry by whitespace and drop the last word
2242 log
.info("Failed to execute %s", " ".join(cmd
))
2243 last_arg
= cmd
[-1].split(" ")
2245 if len(last_arg
) <= 2:
2247 '"%s" we failed to remove this command',
2248 " -- ".join(original_cmd
),
2250 # Log first error msg for original_cmd
2252 log
.error(stdouts
[0])
2256 new_last_arg
= last_arg
[0:-1]
2257 cmd
[-1] = " ".join(new_last_arg
)
2259 log
.info('Executed "%s"', " ".join(cmd
))
2263 lines_to_configure
= []
2265 for (ctx_keys
, line
) in lines_to_add
:
2270 # Don't run "no" commands twice since they can error
2271 # out the second time due to first deletion
2272 if x
== 1 and ctx_keys
[0].startswith("no "):
2275 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2276 lines_to_configure
.append(cmd
)
2278 if lines_to_configure
:
2279 random_string
= "".join(
2280 random
.SystemRandom().choice(
2281 string
.ascii_uppercase
+ string
.digits
2286 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2287 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2289 with
open(filename
, "w") as fh
:
2290 for line
in lines_to_configure
:
2291 fh
.write(line
+ "\n")
2294 vtysh
.exec_file(filename
)
2295 except VtyshException
as e
:
2296 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2300 # Make these changes persistent
2301 target
= str(args
.confdir
+ "/frr.conf")
2302 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):