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
):
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 raise VtyshException(
111 'vtysh returned status %d for command "%s"' % (proc
.returncode
, command
)
113 return stdout
.decode("UTF-8")
115 def is_config_available(self
):
117 Return False if no frr daemon is running or some other vtysh session is
118 in 'configuration terminal' mode which will prevent us from making any
119 configuration changes.
122 output
= self("configure")
124 if "VTY configuration is locked by other VTY" in output
:
125 log
.error("vtysh 'configure' returned\n%s\n" % (output
))
130 def exec_file(self
, filename
):
131 child
= self
._call
(["-f", filename
])
132 if child
.wait() != 0:
133 raise VtyshException(
134 "vtysh (exec file) exited with status %d" % (child
.returncode
)
137 def mark_file(self
, filename
, stdin
=None):
139 ["-m", "-f", filename
],
140 stdout
=subprocess
.PIPE
,
141 stdin
=subprocess
.PIPE
,
142 stderr
=subprocess
.PIPE
,
145 stdout
, stderr
= child
.communicate()
146 except subprocess
.TimeoutExpired
:
148 stdout
, stderr
= child
.communicate()
149 raise VtyshException("vtysh call timed out!")
151 if child
.wait() != 0:
152 raise VtyshException(
153 "vtysh (mark file) exited with status %d:\n%s"
154 % (child
.returncode
, stderr
)
157 return stdout
.decode("UTF-8")
159 def mark_show_run(self
, daemon
=None):
160 cmd
= "show running-config"
162 cmd
+= " %s" % daemon
164 show_run
= self
._call
_cmd
(cmd
, stdout
=subprocess
.PIPE
)
166 ["-m", "-f", "-"], stdin
=show_run
.stdout
, stdout
=subprocess
.PIPE
170 stdout
, stderr
= mark
.communicate()
173 if show_run
.returncode
!= 0:
174 raise VtyshException(
175 "vtysh (show running-config) exited with status %d:"
176 % (show_run
.returncode
)
178 if mark
.returncode
!= 0:
179 raise VtyshException(
180 "vtysh (mark running-config) exited with status %d" % (mark
.returncode
)
183 return stdout
.decode("UTF-8")
186 class Context(object):
189 A Context object represents a section of frr configuration such as:
192 description swp3 -> r8's swp1
197 or a single line context object such as this:
203 def __init__(self
, keys
, lines
):
207 # Keep a dictionary of the lines, this is to make it easy to tell if a
208 # line exists in this Context
209 self
.dlines
= OrderedDict()
212 self
.dlines
[ligne
] = True
214 def add_lines(self
, lines
):
216 Add lines to specified context
219 self
.lines
.extend(lines
)
222 self
.dlines
[ligne
] = True
225 def get_normalized_es_id(line
):
227 The es-id or es-sys-mac need to be converted to lower case
229 sub_strs
= ["evpn mh es-id", "evpn mh es-sys-mac"]
230 for sub_str
in sub_strs
:
231 obj
= re
.match(sub_str
+ " (?P<esi>\S*)", line
)
233 line
= "%s %s" % (sub_str
, obj
.group("esi").lower())
238 def get_normalized_mac_ip_line(line
):
239 if line
.startswith("evpn mh es"):
240 return get_normalized_es_id(line
)
242 if not "ipv6 add" in line
:
243 return get_normalized_ipv6_line(line
)
248 class Config(object):
251 A frr configuration is stored in a Config object. A Config object
252 contains a dictionary of Context objects where the Context keys
253 ('router ospf' for example) are our dictionary key.
256 def __init__(self
, vtysh
):
258 self
.contexts
= OrderedDict()
261 def load_from_file(self
, filename
):
263 Read configuration from specified file and slurp it into internal memory
264 The internal representation has been marked appropriately by passing it
265 through vtysh with the -m parameter
267 log
.info("Loading Config object from file %s", filename
)
269 file_output
= self
.vtysh
.mark_file(filename
)
271 for line
in file_output
.split("\n"):
274 # Compress duplicate whitespaces
275 line
= " ".join(line
.split())
278 line
= get_normalized_mac_ip_line(line
)
280 self
.lines
.append(line
)
284 def load_from_show_running(self
, daemon
):
286 Read running configuration and slurp it into internal memory
287 The internal representation has been marked appropriately by passing it
288 through vtysh with the -m parameter
290 log
.info("Loading Config object from vtysh show running")
292 config_text
= self
.vtysh
.mark_show_run(daemon
)
294 for line
in config_text
.split("\n"):
298 line
== "Building configuration..."
299 or line
== "Current configuration:"
304 self
.lines
.append(line
)
310 Return the lines read in from the configuration
313 return "\n".join(self
.lines
)
315 def get_contexts(self
):
317 Return the parsed context as strings for display, log etc.
320 for (_
, ctx
) in sorted(iteritems(self
.contexts
)):
321 print(str(ctx
) + "\n")
323 def save_contexts(self
, key
, lines
):
325 Save the provided key and lines as a context
332 IP addresses specified in "network" statements, "ip prefix-lists"
333 etc. can differ in the host part of the specification the user
334 provides and what the running config displays. For example, user
335 can specify 11.1.1.1/24, and the running config displays this as
336 11.1.1.0/24. Ensure we don't do a needless operation for such
337 lines. IS-IS & OSPFv3 have no "network" support.
339 re_key_rt
= re
.match(r
"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0])
341 addr
= re_key_rt
.group(2)
344 if "ipaddress" not in sys
.modules
:
345 newaddr
= IPNetwork(addr
)
346 key
[0] = "%s route %s/%s%s" % (
353 newaddr
= ip_network(addr
, strict
=False)
354 key
[0] = "%s route %s/%s%s" % (
356 str(newaddr
.network_address
),
363 re_key_rt
= re
.match(
364 r
"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0]
367 addr
= re_key_rt
.group(4)
370 if "ipaddress" not in sys
.modules
:
371 newaddr
= "%s/%s" % (
372 IPNetwork(addr
).network
,
373 IPNetwork(addr
).prefixlen
,
376 network_addr
= ip_network(addr
, strict
=False)
377 newaddr
= "%s/%s" % (
378 str(network_addr
.network_address
),
379 network_addr
.prefixlen
,
386 legestr
= re_key_rt
.group(5)
387 re_lege
= re
.search(r
"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr
)
389 legestr
= "%sge %s le %s%s" % (
395 re_lege
= re
.search(r
"(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)", legestr
)
398 (re_key_rt
.group(1) == "ip" and re_lege
.group(3) == "32")
399 or (re_key_rt
.group(1) == "ipv6" and re_lege
.group(3) == "128")
401 legestr
= "%sge %s%s" % (
407 key
[0] = "%s prefix-list%s%s %s%s" % (
415 if lines
and key
[0].startswith("router bgp"):
418 re_net
= re
.match(r
"network\s+([A-Fa-f:.0-9/]+)(.*)$", line
)
420 addr
= re_net
.group(1)
421 if "/" not in addr
and key
[0].startswith("router bgp"):
422 # This is most likely an error because with no
423 # prefixlen, BGP treats the prefixlen as 8
427 if "ipaddress" not in sys
.modules
:
428 newaddr
= IPNetwork(addr
)
429 line
= "network %s/%s %s" % (
435 network_addr
= ip_network(addr
, strict
=False)
436 line
= "network %s/%s %s" % (
437 str(network_addr
.network_address
),
438 network_addr
.prefixlen
,
441 newlines
.append(line
)
443 # Really this should be an error. Whats a network
444 # without an IP Address following it ?
445 newlines
.append(line
)
447 newlines
.append(line
)
451 More fixups in user specification and what running config shows.
452 "null0" in routes must be replaced by Null0.
455 key
[0].startswith("ip route")
456 or key
[0].startswith("ipv6 route")
457 and "null0" in key
[0]
459 key
[0] = re
.sub(r
"\s+null0(\s*$)", " Null0", key
[0])
462 if tuple(key
) not in self
.contexts
:
463 ctx
= Context(tuple(key
), lines
)
464 self
.contexts
[tuple(key
)] = ctx
466 ctx
= self
.contexts
[tuple(key
)]
470 if tuple(key
) not in self
.contexts
:
471 ctx
= Context(tuple(key
), [])
472 self
.contexts
[tuple(key
)] = ctx
474 def load_contexts(self
):
476 Parse the configuration and create contexts for each appropriate block
479 current_context_lines
= []
483 The end of a context is flagged via the 'end' keyword:
492 bgp router-id 10.0.0.1
493 bgp log-neighbor-changes
494 no bgp default ipv4-unicast
495 neighbor EBGP peer-group
496 neighbor EBGP advertisement-interval 1
497 neighbor EBGP timers connect 10
498 neighbor 2001:40:1:4::6 remote-as 40
499 neighbor 2001:40:1:8::a remote-as 40
503 neighbor IBGPv6 activate
504 neighbor 2001:10::2 peer-group IBGPv6
505 neighbor 2001:10::3 peer-group IBGPv6
510 neighbor LEAF activate
514 route-target import 10.1.1.1:10100
515 route-target export 10.1.1.1:10100
521 ospf router-id 10.0.0.1
522 log-adjacency-changes detail
523 timers throttle spf 0 50 5000
528 # The code assumes that its working on the output from the "vtysh -m"
529 # command. That provides the appropriate markers to signify end of
530 # a context. This routine uses that to build the contexts for the
533 # There are single line contexts such as "log file /media/node/zebra.log"
534 # and multi-line contexts such as "router ospf" and subcontexts
535 # within a context such as "address-family" within "router bgp"
536 # In each of these cases, the first line of the context becomes the
537 # key of the context. So "router bgp 10" is the key for the non-address
538 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
539 # the key for the subcontext and so on.
544 # the keywords that we know are single line contexts. bgp in this case
545 # is not the main router bgp block, but enabling multi-instance
546 oneline_ctx_keywords
= (
549 "allow-external-route-update",
571 "vrrp autoconfigure",
575 for line
in self
.lines
:
580 if line
.startswith("!") or line
.startswith("#"):
584 # there is one exception though: ldpd accepts a 'router-id' clause
585 # as part of its 'mpls ldp' config context. If we are processing
586 # ldp configuration and encounter a router-id we should NOT switch
590 and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
)
593 and ctx_keys
[0].startswith("mpls ldp")
594 and line
.startswith("router-id ")
597 self
.save_contexts(ctx_keys
, current_context_lines
)
599 # Start a new context
604 current_context_lines
= []
606 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
607 self
.save_contexts(ctx_keys
, current_context_lines
)
611 self
.save_contexts(ctx_keys
, current_context_lines
)
612 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
614 # Start a new context
618 current_context_lines
= []
620 elif line
== "exit-vrf":
621 self
.save_contexts(ctx_keys
, current_context_lines
)
622 current_context_lines
.append(line
)
624 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
627 # Start a new context
631 current_context_lines
= []
635 and len(ctx_keys
) > 1
636 and ctx_keys
[0].startswith("segment-routing")
638 self
.save_contexts(ctx_keys
, current_context_lines
)
640 # Start a new context
641 ctx_keys
= ctx_keys
[:-1]
642 current_context_lines
= []
644 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
649 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
650 # if this exit is for address-family ipv4 unicast, ignore the pop
652 self
.save_contexts(ctx_keys
, current_context_lines
)
654 # Start a new context
655 ctx_keys
= copy
.deepcopy(main_ctx_key
)
656 current_context_lines
= []
658 "LINE %-50s: popping from subcontext to ctx%-50s",
663 elif line
in ["exit-vni", "exit-ldp-if"]:
665 self
.save_contexts(ctx_keys
, current_context_lines
)
667 # Start a new context
668 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
669 current_context_lines
= []
671 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
676 elif new_ctx
is True:
682 ctx_keys
= copy
.deepcopy(main_ctx_key
)
685 current_context_lines
= []
687 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
690 line
.startswith("address-family ")
691 or line
.startswith("vnc defaults")
692 or line
.startswith("vnc l2-group")
693 or line
.startswith("vnc nve-group")
694 or line
.startswith("peer")
695 or line
.startswith("key ")
696 or line
.startswith("member pseudowire")
700 # Save old context first
701 self
.save_contexts(ctx_keys
, current_context_lines
)
702 current_context_lines
= []
703 main_ctx_key
= copy
.deepcopy(ctx_keys
)
704 log
.debug("LINE %-50s: entering sub-context, append to ctx_keys", line
)
706 if line
== "address-family ipv6" and not ctx_keys
[0].startswith(
709 ctx_keys
.append("address-family ipv6 unicast")
710 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith(
713 ctx_keys
.append("address-family ipv4 unicast")
714 elif line
== "address-family evpn":
715 ctx_keys
.append("address-family l2vpn evpn")
717 ctx_keys
.append(line
)
720 line
.startswith("vni ")
721 and len(ctx_keys
) == 2
722 and ctx_keys
[0].startswith("router bgp")
723 and ctx_keys
[1] == "address-family l2vpn evpn"
726 # Save old context first
727 self
.save_contexts(ctx_keys
, current_context_lines
)
728 current_context_lines
= []
729 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
731 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
733 ctx_keys
.append(line
)
736 line
.startswith("interface ")
737 and len(ctx_keys
) == 2
738 and ctx_keys
[0].startswith("mpls ldp")
739 and ctx_keys
[1].startswith("address-family")
742 # Save old context first
743 self
.save_contexts(ctx_keys
, current_context_lines
)
744 current_context_lines
= []
745 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
747 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
749 ctx_keys
.append(line
)
752 line
.startswith("traffic-eng")
753 and len(ctx_keys
) == 1
754 and ctx_keys
[0].startswith("segment-routing")
757 # Save old context first
758 self
.save_contexts(ctx_keys
, current_context_lines
)
759 current_context_lines
= []
761 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
764 ctx_keys
.append(line
)
767 line
.startswith("segment-list ")
768 and len(ctx_keys
) == 2
769 and ctx_keys
[0].startswith("segment-routing")
770 and ctx_keys
[1].startswith("traffic-eng")
773 # Save old context first
774 self
.save_contexts(ctx_keys
, current_context_lines
)
775 current_context_lines
= []
777 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
780 ctx_keys
.append(line
)
783 line
.startswith("policy ")
784 and len(ctx_keys
) == 2
785 and ctx_keys
[0].startswith("segment-routing")
786 and ctx_keys
[1].startswith("traffic-eng")
789 # Save old context first
790 self
.save_contexts(ctx_keys
, current_context_lines
)
791 current_context_lines
= []
793 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
796 ctx_keys
.append(line
)
799 line
.startswith("candidate-path ")
800 and line
.endswith(" dynamic")
801 and len(ctx_keys
) == 3
802 and ctx_keys
[0].startswith("segment-routing")
803 and ctx_keys
[1].startswith("traffic-eng")
804 and ctx_keys
[2].startswith("policy")
807 # Save old context first
808 self
.save_contexts(ctx_keys
, current_context_lines
)
809 current_context_lines
= []
810 main_ctx_key
= copy
.deepcopy(ctx_keys
)
812 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
815 ctx_keys
.append(line
)
818 line
.startswith("pcep")
819 and len(ctx_keys
) == 2
820 and ctx_keys
[0].startswith("segment-routing")
821 and ctx_keys
[1].startswith("traffic-eng")
824 # Save old context first
825 self
.save_contexts(ctx_keys
, current_context_lines
)
826 current_context_lines
= []
827 main_ctx_key
= copy
.deepcopy(ctx_keys
)
829 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
831 ctx_keys
.append(line
)
834 line
.startswith("pce-config ")
835 and len(ctx_keys
) == 3
836 and ctx_keys
[0].startswith("segment-routing")
837 and ctx_keys
[1].startswith("traffic-eng")
838 and ctx_keys
[2].startswith("pcep")
841 # Save old context first
842 self
.save_contexts(ctx_keys
, current_context_lines
)
843 current_context_lines
= []
844 main_ctx_key
= copy
.deepcopy(ctx_keys
)
846 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
849 ctx_keys
.append(line
)
852 line
.startswith("pce ")
853 and len(ctx_keys
) == 3
854 and ctx_keys
[0].startswith("segment-routing")
855 and ctx_keys
[1].startswith("traffic-eng")
856 and ctx_keys
[2].startswith("pcep")
859 # Save old context first
860 self
.save_contexts(ctx_keys
, current_context_lines
)
861 current_context_lines
= []
862 main_ctx_key
= copy
.deepcopy(ctx_keys
)
864 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
866 ctx_keys
.append(line
)
869 line
.startswith("pcc")
870 and len(ctx_keys
) == 3
871 and ctx_keys
[0].startswith("segment-routing")
872 and ctx_keys
[1].startswith("traffic-eng")
873 and ctx_keys
[2].startswith("pcep")
876 # Save old context first
877 self
.save_contexts(ctx_keys
, current_context_lines
)
878 current_context_lines
= []
879 main_ctx_key
= copy
.deepcopy(ctx_keys
)
881 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
883 ctx_keys
.append(line
)
886 # Continuing in an existing context, add non-commented lines to it
887 current_context_lines
.append(line
)
889 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
892 # Save the context of the last one
893 self
.save_contexts(ctx_keys
, current_context_lines
)
896 def lines_to_config(ctx_keys
, line
, delete
):
898 Return the command as it would appear in frr.conf
903 for (i
, ctx_key
) in enumerate(ctx_keys
):
904 cmd
.append(" " * i
+ ctx_key
)
907 indent
= len(ctx_keys
) * " "
909 # There are some commands that are on by default so their "no" form will be
910 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
911 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
912 # not by doing a "no no bgp default ipv4-unicast"
914 if line
.startswith("no "):
915 cmd
.append("%s%s" % (indent
, line
[3:]))
917 cmd
.append("%sno %s" % (indent
, line
))
920 cmd
.append(indent
+ line
)
922 # If line is None then we are typically deleting an entire
923 # context ('no router ospf' for example)
925 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
926 cmd
.append("%s%s" % (" " * i
, ctx_key
))
928 # Only put the 'no' on the last sub-context
930 if ctx_keys
[-1].startswith("no "):
931 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
933 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
935 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
940 def get_normalized_ipv6_line(line
):
942 Return a normalized IPv6 line as produced by frr,
943 with all letters in lower case and trailing and leading
944 zeros removed, and only the network portion present if
945 the IPv6 word is a network
948 words
= line
.split(" ")
954 if "ipaddress" not in sys
.modules
:
955 v6word
= IPNetwork(word
)
956 norm_word
= "%s/%s" % (v6word
.network
, v6word
.prefixlen
)
958 v6word
= ip_network(word
, strict
=False)
959 norm_word
= "%s/%s" % (
960 str(v6word
.network_address
),
967 norm_word
= "%s" % IPv6Address(word
)
972 norm_line
= norm_line
+ " " + norm_word
974 return norm_line
.strip()
977 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
978 for (ctx_keys
, line
) in lines
:
979 if ctx_keys
== target_ctx_keys
:
981 if line
== target_line
:
984 if line
.startswith(target_line
):
989 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
991 # exit-vrf is a bit tricky. If the new config is missing it but we
992 # have configs under a vrf, we need to add it at the end to do the
993 # right context changes. If exit-vrf exists in both the running and
994 # new config, we cannot delete it or it will break context changes.
998 for (ctx_keys
, line
) in lines_to_add
:
999 if add_exit_vrf
== True:
1000 if ctx_keys
[0] != prior_ctx_key
:
1001 insert_key
= ((prior_ctx_key
),)
1002 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
1003 add_exit_vrf
= False
1005 if ctx_keys
[0].startswith("vrf") and line
:
1006 if line
is not "exit-vrf":
1008 prior_ctx_key
= ctx_keys
[0]
1010 add_exit_vrf
= False
1013 for (ctx_keys
, line
) in lines_to_del
:
1014 if line
== "exit-vrf":
1015 if line_exist(lines_to_add
, ctx_keys
, line
):
1016 lines_to_del
.remove((ctx_keys
, line
))
1018 return (lines_to_add
, lines_to_del
)
1021 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1023 # Quite possibly the most confusing (while accurate) variable names in history
1024 lines_to_add_to_del
= []
1025 lines_to_del_to_del
= []
1027 for (ctx_keys
, line
) in lines_to_del
:
1030 if ctx_keys
[0].startswith("router bgp") and line
:
1032 if line
.startswith("neighbor "):
1034 BGP changed how it displays swpX peers that are part of peer-group. Older
1035 versions of frr would display these on separate lines:
1036 neighbor swp1 interface
1037 neighbor swp1 peer-group FOO
1039 but today we display via a single line
1040 neighbor swp1 interface peer-group FOO
1042 This change confuses frr-reload.py so check to see if we are deleting
1043 neighbor swp1 interface peer-group FOO
1046 neighbor swp1 interface
1047 neighbor swp1 peer-group FOO
1049 If so then chop the del line and the corresponding add lines
1052 re_swpx_int_peergroup
= re
.search(
1053 "neighbor (\S+) interface peer-group (\S+)", line
1055 re_swpx_int_v6only_peergroup
= re
.search(
1056 "neighbor (\S+) interface v6only peer-group (\S+)", line
1059 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1060 swpx_interface
= None
1061 swpx_peergroup
= None
1063 if re_swpx_int_peergroup
:
1064 swpx
= re_swpx_int_peergroup
.group(1)
1065 peergroup
= re_swpx_int_peergroup
.group(2)
1066 swpx_interface
= "neighbor %s interface" % swpx
1067 elif re_swpx_int_v6only_peergroup
:
1068 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1069 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1070 swpx_interface
= "neighbor %s interface v6only" % swpx
1072 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1073 found_add_swpx_interface
= line_exist(
1074 lines_to_add
, ctx_keys
, swpx_interface
1076 found_add_swpx_peergroup
= line_exist(
1077 lines_to_add
, ctx_keys
, swpx_peergroup
1079 tmp_ctx_keys
= tuple(list(ctx_keys
))
1081 if not found_add_swpx_peergroup
:
1082 tmp_ctx_keys
= list(ctx_keys
)
1083 tmp_ctx_keys
.append("address-family ipv4 unicast")
1084 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1085 found_add_swpx_peergroup
= line_exist(
1086 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1089 if not found_add_swpx_peergroup
:
1090 tmp_ctx_keys
= list(ctx_keys
)
1091 tmp_ctx_keys
.append("address-family ipv6 unicast")
1092 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1093 found_add_swpx_peergroup
= line_exist(
1094 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1097 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1099 lines_to_del_to_del
.append((ctx_keys
, line
))
1100 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1101 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1104 Changing the bfd timers on neighbors is allowed without doing
1105 a delete/add process. Since doing a "no neighbor blah bfd ..."
1106 will cause the peer to bounce unnecessarily, just skip the delete
1107 and just do the add.
1109 re_nbr_bfd_timers
= re
.search(
1110 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1113 if re_nbr_bfd_timers
:
1114 nbr
= re_nbr_bfd_timers
.group(1)
1115 bfd_nbr
= "neighbor %s" % nbr
1116 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1118 for (ctx_keys
, add_line
) in lines_to_add
:
1119 if ctx_keys
[0].startswith("router bgp"):
1120 re_add_nbr_bfd_timers
= re
.search(
1121 bfd_search_string
, add_line
1124 if re_add_nbr_bfd_timers
:
1125 found_add_bfd_nbr
= line_exist(
1126 lines_to_add
, ctx_keys
, bfd_nbr
, False
1129 if found_add_bfd_nbr
:
1130 lines_to_del_to_del
.append((ctx_keys
, line
))
1133 We changed how we display the neighbor interface command. Older
1134 versions of frr would display the following:
1135 neighbor swp1 interface
1136 neighbor swp1 remote-as external
1137 neighbor swp1 capability extended-nexthop
1139 but today we display via a single line
1140 neighbor swp1 interface remote-as external
1142 and capability extended-nexthop is no longer needed because we
1143 automatically enable it when the neighbor is of type interface.
1145 This change confuses frr-reload.py so check to see if we are deleting
1146 neighbor swp1 interface remote-as (external|internal|ASNUM)
1149 neighbor swp1 interface
1150 neighbor swp1 remote-as (external|internal|ASNUM)
1151 neighbor swp1 capability extended-nexthop
1153 If so then chop the del line and the corresponding add lines
1155 re_swpx_int_remoteas
= re
.search(
1156 "neighbor (\S+) interface remote-as (\S+)", line
1158 re_swpx_int_v6only_remoteas
= re
.search(
1159 "neighbor (\S+) interface v6only remote-as (\S+)", line
1162 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1163 swpx_interface
= None
1164 swpx_remoteas
= None
1166 if re_swpx_int_remoteas
:
1167 swpx
= re_swpx_int_remoteas
.group(1)
1168 remoteas
= re_swpx_int_remoteas
.group(2)
1169 swpx_interface
= "neighbor %s interface" % swpx
1170 elif re_swpx_int_v6only_remoteas
:
1171 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1172 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1173 swpx_interface
= "neighbor %s interface v6only" % swpx
1175 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1176 found_add_swpx_interface
= line_exist(
1177 lines_to_add
, ctx_keys
, swpx_interface
1179 found_add_swpx_remoteas
= line_exist(
1180 lines_to_add
, ctx_keys
, swpx_remoteas
1182 tmp_ctx_keys
= tuple(list(ctx_keys
))
1184 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1186 lines_to_del_to_del
.append((ctx_keys
, line
))
1187 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1188 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1191 We made the 'bgp bestpath as-path multipath-relax' command
1192 automatically assume 'no-as-set' since the lack of this option caused
1193 weird routing problems. When the running config is shown in
1194 releases with this change, the no-as-set keyword is not shown as it
1195 is the default. This causes frr-reload to unnecessarily unapply
1196 this option only to apply it back again, causing unnecessary session
1199 if "multipath-relax" in line
:
1200 re_asrelax_new
= re
.search(
1201 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1203 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1204 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1206 if re_asrelax_new
and found_asrelax_old
:
1208 lines_to_del_to_del
.append((ctx_keys
, line
))
1209 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1212 If we are modifying the BGP table-map we need to avoid a del/add and
1213 instead modify the table-map in place via an add. This is needed to
1214 avoid installing all routes in the RIB the second the 'no table-map'
1217 if line
.startswith("table-map"):
1218 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1221 lines_to_del_to_del
.append((ctx_keys
, line
))
1224 More old-to-new config handling. ip import-table no longer accepts
1225 distance, but we honor the old syntax. But 'show running' shows only
1226 the new syntax. This causes an unnecessary 'no import-table' followed
1227 by the same old 'ip import-table' which causes perturbations in
1228 announced routes leading to traffic blackholes. Fix this issue.
1230 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1232 table_num
= re_importtbl
.group(1)
1233 for ctx
in lines_to_add
:
1234 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1235 lines_to_del_to_del
.append(
1236 (("ip import-table %s" % table_num
,), None)
1238 lines_to_add_to_del
.append((ctx
[0], None))
1241 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1242 However, the running config always adds 'seq x', where x is a number
1243 incremented by 5 for every element of the prefix/access list.
1244 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1245 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1246 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1247 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1248 access-list FOO seq 5 permit 2.2.2.2/32
1249 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1251 re_acl_pfxlst
= re
.search(
1252 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1258 re_acl_pfxlst
.group(1)
1259 + re_acl_pfxlst
.group(2)
1260 + re_acl_pfxlst
.group(3)
1261 + re_acl_pfxlst
.group(5)
1262 + re_acl_pfxlst
.group(6)
1264 for ctx
in lines_to_add
:
1265 if ctx
[0][0] == tmpline
:
1266 lines_to_del_to_del
.append((ctx_keys
, None))
1267 lines_to_add_to_del
.append(((tmpline
,), None))
1270 If prefix-lists or access-lists are being deleted and
1271 not added (see comment above), add command with 'no' to
1272 lines_to_add and remove from lines_to_del to improve
1273 scaling performance.
1276 add_cmd
= ("no " + ctx_keys
[0],)
1277 lines_to_add
.append((add_cmd
, None))
1278 lines_to_del_to_del
.append((ctx_keys
, None))
1282 and ctx_keys
[0].startswith("router bgp")
1283 and ctx_keys
[1] == "address-family l2vpn evpn"
1284 and ctx_keys
[2].startswith("vni")
1288 re
.search("^route-target import (.*)$", line
)
1294 rt
= re_route_target
.group(1).strip()
1295 route_target_import_line
= line
1296 route_target_export_line
= "route-target export %s" % rt
1297 route_target_both_line
= "route-target both %s" % rt
1299 found_route_target_export_line
= line_exist(
1300 lines_to_del
, ctx_keys
, route_target_export_line
1302 found_route_target_both_line
= line_exist(
1303 lines_to_add
, ctx_keys
, route_target_both_line
1307 If the running configs has
1308 route-target import 1:1
1309 route-target export 1:1
1311 and the config we are reloading against has
1312 route-target both 1:1
1314 then we can ignore deleting the import/export and ignore adding the 'both'
1316 if found_route_target_export_line
and found_route_target_both_line
:
1317 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1318 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1319 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1321 # Deleting static routes under a vrf can lead to time-outs if each is sent
1322 # as separate vtysh -c commands. Change them from being in lines_to_del and
1323 # put the "no" form in lines_to_add
1324 if ctx_keys
[0].startswith("vrf ") and line
:
1325 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1326 add_cmd
= "no " + line
1327 lines_to_add
.append((ctx_keys
, add_cmd
))
1328 lines_to_del_to_del
.append((ctx_keys
, line
))
1331 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1334 lines_to_del_to_del
.append((ctx_keys
, line
))
1335 lines_to_add_to_del
.append((ctx_keys
, line
))
1338 We have commands that used to be displayed in the global part
1339 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1343 neighbor ISL advertisement-interval 0
1349 address-family ipv4 unicast
1350 neighbor ISL advertisement-interval 0
1352 Look to see if we are deleting it in one format just to add it back in the other
1355 ctx_keys
[0].startswith("router bgp")
1356 and len(ctx_keys
) > 1
1357 and ctx_keys
[1] == "address-family ipv4 unicast"
1359 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1360 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1362 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1365 lines_to_del_to_del
.append((ctx_keys
, line
))
1366 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1368 for (ctx_keys
, line
) in lines_to_del_to_del
:
1369 lines_to_del
.remove((ctx_keys
, line
))
1371 for (ctx_keys
, line
) in lines_to_add_to_del
:
1372 lines_to_add
.remove((ctx_keys
, line
))
1374 return (lines_to_add
, lines_to_del
)
1377 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1379 There are certain commands that cannot be removed. Remove
1380 those commands from lines_to_del.
1382 lines_to_del_to_del
= []
1384 for (ctx_keys
, line
) in lines_to_del
:
1387 ctx_keys
[0].startswith("frr version")
1388 or ctx_keys
[0].startswith("frr defaults")
1389 or ctx_keys
[0].startswith("username")
1390 or ctx_keys
[0].startswith("password")
1391 or ctx_keys
[0].startswith("line vty")
1393 # This is technically "no"able but if we did so frr-reload would
1394 # stop working so do not let the user shoot themselves in the foot
1396 ctx_keys
[0].startswith("service integrated-vtysh-config")
1399 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1400 lines_to_del_to_del
.append((ctx_keys
, line
))
1402 for (ctx_keys
, line
) in lines_to_del_to_del
:
1403 lines_to_del
.remove((ctx_keys
, line
))
1405 return (lines_to_add
, lines_to_del
)
1408 def compare_context_objects(newconf
, running
):
1410 Create a context diff for the two specified contexts
1413 # Compare the two Config objects to find the lines that we need to add/del
1420 candidates_to_add
= []
1423 # Find contexts that are in newconf but not in running
1424 # Find contexts that are in running but not in newconf
1425 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1427 if running_ctx_keys
not in newconf
.contexts
:
1429 # We check that the len is 1 here so that we only look at ('router bgp 10')
1430 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1431 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1432 # running but not in newconf.
1433 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1435 lines_to_del
.append((running_ctx_keys
, None))
1437 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1438 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1440 ].startswith("vrf"):
1441 for line
in running_ctx
.lines
:
1442 lines_to_del
.append((running_ctx_keys
, line
))
1444 # If this is an address-family under 'router bgp' and we are already deleting the
1445 # entire 'router bgp' context then ignore this sub-context
1447 "router bgp" in running_ctx_keys
[0]
1448 and len(running_ctx_keys
) > 1
1453 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1455 "router bgp" in running_ctx_keys
[0]
1456 and len(running_ctx_keys
) > 2
1457 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1458 and running_ctx_keys
[2].startswith("vni ")
1460 lines_to_del
.append((running_ctx_keys
, None))
1463 "router bgp" in running_ctx_keys
[0]
1464 and len(running_ctx_keys
) > 1
1465 and running_ctx_keys
[1].startswith("address-family")
1467 # There's no 'no address-family' support and so we have to
1468 # delete each line individually again
1469 for line
in running_ctx
.lines
:
1470 lines_to_del
.append((running_ctx_keys
, line
))
1472 # Some commands can happen at higher counts that make
1473 # doing vtysh -c inefficient (and can time out.) For
1474 # these commands, instead of adding them to lines_to_del,
1475 # add the "no " version to lines_to_add.
1476 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1478 ].startswith("ipv6 route"):
1479 add_cmd
= ("no " + running_ctx_keys
[0],)
1480 lines_to_add
.append((add_cmd
, None))
1482 # if this an interface sub-subcontext in an address-family block in ldpd and
1483 # we are already deleting the whole context, then ignore this
1485 len(running_ctx_keys
) > 2
1486 and running_ctx_keys
[0].startswith("mpls ldp")
1487 and running_ctx_keys
[1].startswith("address-family")
1488 and (running_ctx_keys
[:2], None) in lines_to_del
1492 # same thing for a pseudowire sub-context inside an l2vpn context
1494 len(running_ctx_keys
) > 1
1495 and running_ctx_keys
[0].startswith("l2vpn")
1496 and running_ctx_keys
[1].startswith("member pseudowire")
1497 and (running_ctx_keys
[:1], None) in lines_to_del
1501 # Segment routing and traffic engineering never need to be deleted
1503 running_ctx_keys
[0].startswith("segment-routing")
1504 and len(running_ctx_keys
) < 3
1508 # Neither the pcep command
1510 len(running_ctx_keys
) == 3
1511 and running_ctx_keys
[0].startswith("segment-routing")
1512 and running_ctx_keys
[2].startswith("pcep")
1516 # Segment lists can only be deleted after we removed all the candidate paths that
1517 # use them, so add them to a separate array that is going to be appended at the end
1519 len(running_ctx_keys
) == 3
1520 and running_ctx_keys
[0].startswith("segment-routing")
1521 and running_ctx_keys
[2].startswith("segment-list")
1523 seglist_to_del
.append((running_ctx_keys
, None))
1525 # Policies must be deleted after there candidate path, to be sure
1526 # we add them to a separate array that is going to be appended at the end
1528 len(running_ctx_keys
) == 3
1529 and running_ctx_keys
[0].startswith("segment-routing")
1530 and running_ctx_keys
[2].startswith("policy")
1532 pollist_to_del
.append((running_ctx_keys
, None))
1534 # pce-config must be deleted after the pce, to be sure we add them
1535 # to a separate array that is going to be appended at the end
1537 len(running_ctx_keys
) >= 4
1538 and running_ctx_keys
[0].startswith("segment-routing")
1539 and running_ctx_keys
[3].startswith("pce-config")
1541 pceconf_to_del
.append((running_ctx_keys
, None))
1543 # pcc must be deleted after the pce and pce-config too
1545 len(running_ctx_keys
) >= 4
1546 and running_ctx_keys
[0].startswith("segment-routing")
1547 and running_ctx_keys
[3].startswith("pcc")
1549 pcclist_to_del
.append((running_ctx_keys
, None))
1551 # Non-global context
1552 elif running_ctx_keys
and not any(
1553 "address-family" in key
for key
in running_ctx_keys
1555 lines_to_del
.append((running_ctx_keys
, None))
1557 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1558 lines_to_del
.append((running_ctx_keys
, None))
1562 for line
in running_ctx
.lines
:
1563 lines_to_del
.append((running_ctx_keys
, line
))
1565 # if we have some policies commands to delete, append them to lines_to_del
1566 if len(pollist_to_del
) > 0:
1567 lines_to_del
.extend(pollist_to_del
)
1569 # if we have some segment list commands to delete, append them to lines_to_del
1570 if len(seglist_to_del
) > 0:
1571 lines_to_del
.extend(seglist_to_del
)
1573 # if we have some pce list commands to delete, append them to lines_to_del
1574 if len(pceconf_to_del
) > 0:
1575 lines_to_del
.extend(pceconf_to_del
)
1577 # if we have some pcc list commands to delete, append them to lines_to_del
1578 if len(pcclist_to_del
) > 0:
1579 lines_to_del
.extend(pcclist_to_del
)
1581 # Find the lines within each context to add
1582 # Find the lines within each context to del
1583 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1585 if newconf_ctx_keys
in running
.contexts
:
1586 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1588 for line
in newconf_ctx
.lines
:
1589 if line
not in running_ctx
.dlines
:
1591 # candidate paths can only be added after the policy and segment list,
1592 # so add them to a separate array that is going to be appended at the end
1594 len(newconf_ctx_keys
) == 3
1595 and newconf_ctx_keys
[0].startswith("segment-routing")
1596 and newconf_ctx_keys
[2].startswith("policy ")
1597 and line
.startswith("candidate-path ")
1599 candidates_to_add
.append((newconf_ctx_keys
, line
))
1602 lines_to_add
.append((newconf_ctx_keys
, line
))
1604 for line
in running_ctx
.lines
:
1605 if line
not in newconf_ctx
.dlines
:
1606 lines_to_del
.append((newconf_ctx_keys
, line
))
1608 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1610 if newconf_ctx_keys
not in running
.contexts
:
1612 # candidate paths can only be added after the policy and segment list,
1613 # so add them to a separate array that is going to be appended at the end
1615 len(newconf_ctx_keys
) == 4
1616 and newconf_ctx_keys
[0].startswith("segment-routing")
1617 and newconf_ctx_keys
[3].startswith("candidate-path")
1619 candidates_to_add
.append((newconf_ctx_keys
, None))
1620 for line
in newconf_ctx
.lines
:
1621 candidates_to_add
.append((newconf_ctx_keys
, line
))
1624 lines_to_add
.append((newconf_ctx_keys
, None))
1626 for line
in newconf_ctx
.lines
:
1627 lines_to_add
.append((newconf_ctx_keys
, line
))
1629 # if we have some candidate paths commands to add, append them to lines_to_add
1630 if len(candidates_to_add
) > 0:
1631 lines_to_add
.extend(candidates_to_add
)
1633 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1634 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1635 lines_to_add
, lines_to_del
1637 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1638 lines_to_add
, lines_to_del
1641 return (lines_to_add
, lines_to_del
)
1644 if __name__
== "__main__":
1645 # Command line options
1646 parser
= argparse
.ArgumentParser(
1647 description
="Dynamically apply diff in frr configs"
1649 parser
.add_argument(
1650 "--input", help='Read running config from file instead of "show running"'
1652 group
= parser
.add_mutually_exclusive_group(required
=True)
1654 "--reload", action
="store_true", help="Apply the deltas", default
=False
1657 "--test", action
="store_true", help="Show the deltas", default
=False
1659 level_group
= parser
.add_mutually_exclusive_group()
1660 level_group
.add_argument(
1662 action
="store_true",
1663 help="Enable debugs (synonym for --log-level=debug)",
1666 level_group
.add_argument(
1670 choices
=("critical", "error", "warning", "info", "debug"),
1672 parser
.add_argument(
1673 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1675 parser
.add_argument(
1679 help="Reload specified path/namespace",
1682 parser
.add_argument("filename", help="Location of new frr config file")
1683 parser
.add_argument(
1685 action
="store_true",
1686 help="Overwrite frr.conf with running config output",
1689 parser
.add_argument(
1690 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1692 parser
.add_argument(
1693 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1695 parser
.add_argument(
1696 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1698 parser
.add_argument(
1700 help="socket to be used by vtysh to connect to the daemons",
1703 parser
.add_argument(
1704 "--daemon", help="daemon for which want to replace the config", default
=""
1707 args
= parser
.parse_args()
1710 # For --test log to stdout
1711 # For --reload log to /var/log/frr/frr-reload.log
1712 if args
.test
or args
.stdout
:
1713 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1715 # Color the errors and warnings in red
1716 logging
.addLevelName(
1717 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1719 logging
.addLevelName(
1720 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1724 if not os
.path
.isdir("/var/log/frr/"):
1725 os
.makedirs("/var/log/frr/")
1727 logging
.basicConfig(
1728 filename
="/var/log/frr/frr-reload.log",
1729 format
="%(asctime)s %(levelname)5s: %(message)s",
1732 # argparse should prevent this from happening but just to be safe...
1734 raise Exception("Must specify --reload or --test")
1735 log
= logging
.getLogger(__name__
)
1738 log
.setLevel(logging
.DEBUG
)
1740 log
.setLevel(args
.log_level
.upper())
1742 if args
.reload and not args
.stdout
:
1743 # Additionally send errors and above to STDOUT, with no metadata,
1744 # when we are logging to a file. This specifically does not follow
1745 # args.log_level, and is analagous to behaviour in earlier versions
1746 # which additionally logged most errors using print().
1748 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
1749 stdout_hdlr
.setLevel(logging
.ERROR
)
1750 stdout_hdlr
.setFormatter(logging
.Formatter())
1751 log
.addHandler(stdout_hdlr
)
1753 # Verify the new config file is valid
1754 if not os
.path
.isfile(args
.filename
):
1755 log
.error("Filename %s does not exist" % args
.filename
)
1758 if not os
.path
.getsize(args
.filename
):
1759 log
.error("Filename %s is an empty file" % args
.filename
)
1762 # Verify that confdir is correct
1763 if not os
.path
.isdir(args
.confdir
):
1764 log
.error("Confdir %s is not a valid path" % args
.confdir
)
1767 # Verify that bindir is correct
1768 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
1769 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
1772 # verify that the vty_socket, if specified, is valid
1773 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
1774 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
1777 # verify that the daemon, if specified, is valid
1778 if args
.daemon
and args
.daemon
not in [
1796 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1801 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
1803 # Verify that 'service integrated-vtysh-config' is configured
1805 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
1807 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
1808 service_integrated_vtysh_config
= True
1810 if os
.path
.isfile(vtysh_filename
):
1811 with
open(vtysh_filename
, "r") as fh
:
1812 for line
in fh
.readlines():
1815 if line
== "no service integrated-vtysh-config":
1816 service_integrated_vtysh_config
= False
1819 if not service_integrated_vtysh_config
and not args
.daemon
:
1821 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1825 log
.info('Called via "%s"', str(args
))
1827 # Create a Config object from the config generated by newconf
1828 newconf
= Config(vtysh
)
1830 newconf
.load_from_file(args
.filename
)
1832 except VtyshException
as ve
:
1833 log
.error("vtysh failed to process new configuration: {}".format(ve
))
1838 # Create a Config object from the running config
1839 running
= Config(vtysh
)
1842 running
.load_from_file(args
.input)
1844 running
.load_from_show_running(args
.daemon
)
1846 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1847 lines_to_configure
= []
1850 print("\nLines To Delete")
1851 print("===============")
1853 for (ctx_keys
, line
) in lines_to_del
:
1858 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, True))
1859 lines_to_configure
.append(cmd
)
1863 print("\nLines To Add")
1864 print("============")
1866 for (ctx_keys
, line
) in lines_to_add
:
1871 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False))
1872 lines_to_configure
.append(cmd
)
1877 # We will not be able to do anything, go ahead and exit(1)
1878 if not vtysh
.is_config_available():
1881 log
.debug("New Frr Config\n%s", newconf
.get_lines())
1883 # This looks a little odd but we have to do this twice...here is why
1884 # If the user had this running bgp config:
1887 # neighbor 1.1.1.1 remote-as 50
1888 # neighbor 1.1.1.1 route-map FOO out
1890 # and this config in the newconf config file
1893 # neighbor 1.1.1.1 remote-as 999
1894 # neighbor 1.1.1.1 route-map FOO out
1897 # Then the script will do
1898 # - no neighbor 1.1.1.1 remote-as 50
1899 # - neighbor 1.1.1.1 remote-as 999
1901 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1902 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1903 # configs again to put this line back.
1905 # There are many keywords in FRR that can only appear one time under
1906 # a context, take "bgp router-id" for example. If the config that we are
1907 # reloading against has the following:
1910 # bgp router-id 1.1.1.1
1911 # bgp router-id 2.2.2.2
1913 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1914 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1915 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1916 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1917 # second pass to include all of the "adds" from the first pass.
1918 lines_to_add_first_pass
= []
1921 running
= Config(vtysh
)
1922 running
.load_from_show_running(args
.daemon
)
1923 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
1925 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1928 lines_to_add_first_pass
= lines_to_add
1930 lines_to_add
.extend(lines_to_add_first_pass
)
1932 # Only do deletes on the first pass. The reason being if we
1933 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1934 # will automatically add:
1937 # ipv6 nd ra-interval 10
1938 # no ipv6 nd suppress-ra
1941 # but those lines aren't in the config we are reloading against so
1942 # on the 2nd pass they will show up in lines_to_del. This could
1943 # apply to other scenarios as well where configuring FOO adds BAR
1945 if lines_to_del
and x
== 0:
1946 for (ctx_keys
, line
) in lines_to_del
:
1951 # 'no' commands are tricky, we can't just put them in a file and
1952 # vtysh -f that file. See the next comment for an explanation
1954 cmd
= lines_to_config(ctx_keys
, line
, True)
1957 # Some commands in frr are picky about taking a "no" of the entire line.
1958 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1959 # only the beginning. If we hit one of these command an exception will be
1960 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1963 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1964 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1965 # % Unknown command.
1966 # frr(config-if)# no ip ospf authentication message-digest
1967 # % Unknown command.
1968 # frr(config-if)# no ip ospf authentication
1973 vtysh(["configure"] + cmd
)
1975 except VtyshException
:
1977 # - Pull the last entry from cmd (this would be
1978 # 'no ip ospf authentication message-digest 1.1.1.1' in
1980 # - Split that last entry by whitespace and drop the last word
1981 log
.info("Failed to execute %s", " ".join(cmd
))
1982 last_arg
= cmd
[-1].split(" ")
1984 if len(last_arg
) <= 2:
1986 '"%s" we failed to remove this command',
1987 " -- ".join(original_cmd
),
1991 new_last_arg
= last_arg
[0:-1]
1992 cmd
[-1] = " ".join(new_last_arg
)
1994 log
.info('Executed "%s"', " ".join(cmd
))
1998 lines_to_configure
= []
2000 for (ctx_keys
, line
) in lines_to_add
:
2005 # Don't run "no" commands twice since they can error
2006 # out the second time due to first deletion
2007 if x
== 1 and ctx_keys
[0].startswith("no "):
2010 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2011 lines_to_configure
.append(cmd
)
2013 if lines_to_configure
:
2014 random_string
= "".join(
2015 random
.SystemRandom().choice(
2016 string
.ascii_uppercase
+ string
.digits
2021 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2022 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2024 with
open(filename
, "w") as fh
:
2025 for line
in lines_to_configure
:
2026 fh
.write(line
+ "\n")
2029 vtysh
.exec_file(filename
)
2030 except VtyshException
as e
:
2031 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2035 # Make these changes persistent
2036 target
= str(args
.confdir
+ "/frr.conf")
2037 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):