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("#"):
583 if (len(ctx_keys
) == 2
584 and ctx_keys
[0].startswith('bfd')
585 and ctx_keys
[1].startswith('profile ')
587 log
.debug('LINE %-50s: popping from sub context, %-50s', line
, ctx_keys
)
590 self
.save_contexts(ctx_keys
, current_context_lines
)
591 ctx_keys
= copy
.deepcopy(main_ctx_key
)
592 current_context_lines
= []
596 # there is one exception though: ldpd accepts a 'router-id' clause
597 # as part of its 'mpls ldp' config context. If we are processing
598 # ldp configuration and encounter a router-id we should NOT switch
602 and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
)
605 and ctx_keys
[0].startswith("mpls ldp")
606 and line
.startswith("router-id ")
609 self
.save_contexts(ctx_keys
, current_context_lines
)
611 # Start a new context
616 current_context_lines
= []
618 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
619 self
.save_contexts(ctx_keys
, current_context_lines
)
623 self
.save_contexts(ctx_keys
, current_context_lines
)
624 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
626 # Start a new context
630 current_context_lines
= []
632 elif line
== "exit" and ctx_keys
[0].startswith("rpki"):
633 self
.save_contexts(ctx_keys
, current_context_lines
)
634 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
636 # Start a new context
640 current_context_lines
= []
642 elif line
== "exit-vrf":
643 self
.save_contexts(ctx_keys
, current_context_lines
)
644 current_context_lines
.append(line
)
646 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
649 # Start a new context
653 current_context_lines
= []
657 and len(ctx_keys
) > 1
658 and ctx_keys
[0].startswith("segment-routing")
660 self
.save_contexts(ctx_keys
, current_context_lines
)
662 # Start a new context
663 ctx_keys
= ctx_keys
[:-1]
664 current_context_lines
= []
666 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
671 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
672 # if this exit is for address-family ipv4 unicast, ignore the pop
674 self
.save_contexts(ctx_keys
, current_context_lines
)
676 # Start a new context
677 ctx_keys
= copy
.deepcopy(main_ctx_key
)
678 current_context_lines
= []
680 "LINE %-50s: popping from subcontext to ctx%-50s",
685 elif line
in ["exit-vni", "exit-ldp-if"]:
687 self
.save_contexts(ctx_keys
, current_context_lines
)
689 # Start a new context
690 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
691 current_context_lines
= []
693 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
698 elif new_ctx
is True:
704 ctx_keys
= copy
.deepcopy(main_ctx_key
)
707 current_context_lines
= []
709 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
712 line
.startswith("address-family ")
713 or line
.startswith("vnc defaults")
714 or line
.startswith("vnc l2-group")
715 or line
.startswith("vnc nve-group")
716 or line
.startswith("peer")
717 or line
.startswith("key ")
718 or line
.startswith("member pseudowire")
722 # Save old context first
723 self
.save_contexts(ctx_keys
, current_context_lines
)
724 current_context_lines
= []
725 main_ctx_key
= copy
.deepcopy(ctx_keys
)
726 log
.debug("LINE %-50s: entering sub-context, append to ctx_keys", line
)
728 if line
== "address-family ipv6" and not ctx_keys
[0].startswith(
731 ctx_keys
.append("address-family ipv6 unicast")
732 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith(
735 ctx_keys
.append("address-family ipv4 unicast")
736 elif line
== "address-family evpn":
737 ctx_keys
.append("address-family l2vpn evpn")
739 ctx_keys
.append(line
)
742 line
.startswith("vni ")
743 and len(ctx_keys
) == 2
744 and ctx_keys
[0].startswith("router bgp")
745 and ctx_keys
[1] == "address-family l2vpn evpn"
748 # Save old context first
749 self
.save_contexts(ctx_keys
, current_context_lines
)
750 current_context_lines
= []
751 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
753 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
755 ctx_keys
.append(line
)
758 line
.startswith("interface ")
759 and len(ctx_keys
) == 2
760 and ctx_keys
[0].startswith("mpls ldp")
761 and ctx_keys
[1].startswith("address-family")
764 # Save old context first
765 self
.save_contexts(ctx_keys
, current_context_lines
)
766 current_context_lines
= []
767 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
769 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
771 ctx_keys
.append(line
)
774 line
.startswith("traffic-eng")
775 and len(ctx_keys
) == 1
776 and ctx_keys
[0].startswith("segment-routing")
779 # Save old context first
780 self
.save_contexts(ctx_keys
, current_context_lines
)
781 current_context_lines
= []
783 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
786 ctx_keys
.append(line
)
789 line
.startswith("segment-list ")
790 and len(ctx_keys
) == 2
791 and ctx_keys
[0].startswith("segment-routing")
792 and ctx_keys
[1].startswith("traffic-eng")
795 # Save old context first
796 self
.save_contexts(ctx_keys
, current_context_lines
)
797 current_context_lines
= []
799 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
802 ctx_keys
.append(line
)
805 line
.startswith("policy ")
806 and len(ctx_keys
) == 2
807 and ctx_keys
[0].startswith("segment-routing")
808 and ctx_keys
[1].startswith("traffic-eng")
811 # Save old context first
812 self
.save_contexts(ctx_keys
, current_context_lines
)
813 current_context_lines
= []
815 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
818 ctx_keys
.append(line
)
821 line
.startswith("candidate-path ")
822 and line
.endswith(" dynamic")
823 and len(ctx_keys
) == 3
824 and ctx_keys
[0].startswith("segment-routing")
825 and ctx_keys
[1].startswith("traffic-eng")
826 and ctx_keys
[2].startswith("policy")
829 # Save old context first
830 self
.save_contexts(ctx_keys
, current_context_lines
)
831 current_context_lines
= []
832 main_ctx_key
= copy
.deepcopy(ctx_keys
)
834 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
837 ctx_keys
.append(line
)
840 line
.startswith("pcep")
841 and len(ctx_keys
) == 2
842 and ctx_keys
[0].startswith("segment-routing")
843 and ctx_keys
[1].startswith("traffic-eng")
846 # Save old context first
847 self
.save_contexts(ctx_keys
, current_context_lines
)
848 current_context_lines
= []
849 main_ctx_key
= copy
.deepcopy(ctx_keys
)
851 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
853 ctx_keys
.append(line
)
856 line
.startswith("pce-config ")
857 and len(ctx_keys
) == 3
858 and ctx_keys
[0].startswith("segment-routing")
859 and ctx_keys
[1].startswith("traffic-eng")
860 and ctx_keys
[2].startswith("pcep")
863 # Save old context first
864 self
.save_contexts(ctx_keys
, current_context_lines
)
865 current_context_lines
= []
866 main_ctx_key
= copy
.deepcopy(ctx_keys
)
868 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
871 ctx_keys
.append(line
)
874 line
.startswith("pce ")
875 and len(ctx_keys
) == 3
876 and ctx_keys
[0].startswith("segment-routing")
877 and ctx_keys
[1].startswith("traffic-eng")
878 and ctx_keys
[2].startswith("pcep")
881 # Save old context first
882 self
.save_contexts(ctx_keys
, current_context_lines
)
883 current_context_lines
= []
884 main_ctx_key
= copy
.deepcopy(ctx_keys
)
886 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
888 ctx_keys
.append(line
)
891 line
.startswith("pcc")
892 and len(ctx_keys
) == 3
893 and ctx_keys
[0].startswith("segment-routing")
894 and ctx_keys
[1].startswith("traffic-eng")
895 and ctx_keys
[2].startswith("pcep")
898 # Save old context first
899 self
.save_contexts(ctx_keys
, current_context_lines
)
900 current_context_lines
= []
901 main_ctx_key
= copy
.deepcopy(ctx_keys
)
903 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
905 ctx_keys
.append(line
)
908 line
.startswith('profile ')
909 and len(ctx_keys
) == 1
910 and ctx_keys
[0].startswith('bfd')
913 # Save old context first
914 self
.save_contexts(ctx_keys
, current_context_lines
)
915 current_context_lines
= []
916 main_ctx_key
= copy
.deepcopy(ctx_keys
)
918 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
921 ctx_keys
.append(line
)
924 # Continuing in an existing context, add non-commented lines to it
925 current_context_lines
.append(line
)
927 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
930 # Save the context of the last one
931 self
.save_contexts(ctx_keys
, current_context_lines
)
934 def lines_to_config(ctx_keys
, line
, delete
):
936 Return the command as it would appear in frr.conf
941 for (i
, ctx_key
) in enumerate(ctx_keys
):
942 cmd
.append(" " * i
+ ctx_key
)
945 indent
= len(ctx_keys
) * " "
947 # There are some commands that are on by default so their "no" form will be
948 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
949 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
950 # not by doing a "no no bgp default ipv4-unicast"
952 if line
.startswith("no "):
953 cmd
.append("%s%s" % (indent
, line
[3:]))
955 cmd
.append("%sno %s" % (indent
, line
))
958 cmd
.append(indent
+ line
)
960 # If line is None then we are typically deleting an entire
961 # context ('no router ospf' for example)
963 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
964 cmd
.append("%s%s" % (" " * i
, ctx_key
))
966 # Only put the 'no' on the last sub-context
968 if ctx_keys
[-1].startswith("no "):
969 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
971 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
973 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
978 def get_normalized_ipv6_line(line
):
980 Return a normalized IPv6 line as produced by frr,
981 with all letters in lower case and trailing and leading
982 zeros removed, and only the network portion present if
983 the IPv6 word is a network
986 words
= line
.split(" ")
992 if "ipaddress" not in sys
.modules
:
993 v6word
= IPNetwork(word
)
994 norm_word
= "%s/%s" % (v6word
.network
, v6word
.prefixlen
)
996 v6word
= ip_network(word
, strict
=False)
997 norm_word
= "%s/%s" % (
998 str(v6word
.network_address
),
1005 norm_word
= "%s" % IPv6Address(word
)
1010 norm_line
= norm_line
+ " " + norm_word
1012 return norm_line
.strip()
1015 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
1016 for (ctx_keys
, line
) in lines
:
1017 if ctx_keys
== target_ctx_keys
:
1019 if line
== target_line
:
1022 if line
.startswith(target_line
):
1027 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
1029 # exit-vrf is a bit tricky. If the new config is missing it but we
1030 # have configs under a vrf, we need to add it at the end to do the
1031 # right context changes. If exit-vrf exists in both the running and
1032 # new config, we cannot delete it or it will break context changes.
1033 add_exit_vrf
= False
1036 for (ctx_keys
, line
) in lines_to_add
:
1037 if add_exit_vrf
== True:
1038 if ctx_keys
[0] != prior_ctx_key
:
1039 insert_key
= ((prior_ctx_key
),)
1040 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
1041 add_exit_vrf
= False
1043 if ctx_keys
[0].startswith("vrf") and line
:
1044 if line
is not "exit-vrf":
1046 prior_ctx_key
= ctx_keys
[0]
1048 add_exit_vrf
= False
1051 for (ctx_keys
, line
) in lines_to_del
:
1052 if line
== "exit-vrf":
1053 if line_exist(lines_to_add
, ctx_keys
, line
):
1054 lines_to_del
.remove((ctx_keys
, line
))
1056 return (lines_to_add
, lines_to_del
)
1059 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1061 # Quite possibly the most confusing (while accurate) variable names in history
1062 lines_to_add_to_del
= []
1063 lines_to_del_to_del
= []
1065 for (ctx_keys
, line
) in lines_to_del
:
1068 if ctx_keys
[0].startswith("router bgp") and line
:
1070 if line
.startswith("neighbor "):
1072 BGP changed how it displays swpX peers that are part of peer-group. Older
1073 versions of frr would display these on separate lines:
1074 neighbor swp1 interface
1075 neighbor swp1 peer-group FOO
1077 but today we display via a single line
1078 neighbor swp1 interface peer-group FOO
1080 This change confuses frr-reload.py so check to see if we are deleting
1081 neighbor swp1 interface peer-group FOO
1084 neighbor swp1 interface
1085 neighbor swp1 peer-group FOO
1087 If so then chop the del line and the corresponding add lines
1090 re_swpx_int_peergroup
= re
.search(
1091 "neighbor (\S+) interface peer-group (\S+)", line
1093 re_swpx_int_v6only_peergroup
= re
.search(
1094 "neighbor (\S+) interface v6only peer-group (\S+)", line
1097 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1098 swpx_interface
= None
1099 swpx_peergroup
= None
1101 if re_swpx_int_peergroup
:
1102 swpx
= re_swpx_int_peergroup
.group(1)
1103 peergroup
= re_swpx_int_peergroup
.group(2)
1104 swpx_interface
= "neighbor %s interface" % swpx
1105 elif re_swpx_int_v6only_peergroup
:
1106 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1107 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1108 swpx_interface
= "neighbor %s interface v6only" % swpx
1110 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1111 found_add_swpx_interface
= line_exist(
1112 lines_to_add
, ctx_keys
, swpx_interface
1114 found_add_swpx_peergroup
= line_exist(
1115 lines_to_add
, ctx_keys
, swpx_peergroup
1117 tmp_ctx_keys
= tuple(list(ctx_keys
))
1119 if not found_add_swpx_peergroup
:
1120 tmp_ctx_keys
= list(ctx_keys
)
1121 tmp_ctx_keys
.append("address-family ipv4 unicast")
1122 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1123 found_add_swpx_peergroup
= line_exist(
1124 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1127 if not found_add_swpx_peergroup
:
1128 tmp_ctx_keys
= list(ctx_keys
)
1129 tmp_ctx_keys
.append("address-family ipv6 unicast")
1130 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1131 found_add_swpx_peergroup
= line_exist(
1132 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1135 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1137 lines_to_del_to_del
.append((ctx_keys
, line
))
1138 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1139 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1142 Changing the bfd timers on neighbors is allowed without doing
1143 a delete/add process. Since doing a "no neighbor blah bfd ..."
1144 will cause the peer to bounce unnecessarily, just skip the delete
1145 and just do the add.
1147 re_nbr_bfd_timers
= re
.search(
1148 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1151 if re_nbr_bfd_timers
:
1152 nbr
= re_nbr_bfd_timers
.group(1)
1153 bfd_nbr
= "neighbor %s" % nbr
1154 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1156 for (ctx_keys
, add_line
) in lines_to_add
:
1157 if ctx_keys
[0].startswith("router bgp"):
1158 re_add_nbr_bfd_timers
= re
.search(
1159 bfd_search_string
, add_line
1162 if re_add_nbr_bfd_timers
:
1163 found_add_bfd_nbr
= line_exist(
1164 lines_to_add
, ctx_keys
, bfd_nbr
, False
1167 if found_add_bfd_nbr
:
1168 lines_to_del_to_del
.append((ctx_keys
, line
))
1171 We changed how we display the neighbor interface command. Older
1172 versions of frr would display the following:
1173 neighbor swp1 interface
1174 neighbor swp1 remote-as external
1175 neighbor swp1 capability extended-nexthop
1177 but today we display via a single line
1178 neighbor swp1 interface remote-as external
1180 and capability extended-nexthop is no longer needed because we
1181 automatically enable it when the neighbor is of type interface.
1183 This change confuses frr-reload.py so check to see if we are deleting
1184 neighbor swp1 interface remote-as (external|internal|ASNUM)
1187 neighbor swp1 interface
1188 neighbor swp1 remote-as (external|internal|ASNUM)
1189 neighbor swp1 capability extended-nexthop
1191 If so then chop the del line and the corresponding add lines
1193 re_swpx_int_remoteas
= re
.search(
1194 "neighbor (\S+) interface remote-as (\S+)", line
1196 re_swpx_int_v6only_remoteas
= re
.search(
1197 "neighbor (\S+) interface v6only remote-as (\S+)", line
1200 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1201 swpx_interface
= None
1202 swpx_remoteas
= None
1204 if re_swpx_int_remoteas
:
1205 swpx
= re_swpx_int_remoteas
.group(1)
1206 remoteas
= re_swpx_int_remoteas
.group(2)
1207 swpx_interface
= "neighbor %s interface" % swpx
1208 elif re_swpx_int_v6only_remoteas
:
1209 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1210 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1211 swpx_interface
= "neighbor %s interface v6only" % swpx
1213 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1214 found_add_swpx_interface
= line_exist(
1215 lines_to_add
, ctx_keys
, swpx_interface
1217 found_add_swpx_remoteas
= line_exist(
1218 lines_to_add
, ctx_keys
, swpx_remoteas
1220 tmp_ctx_keys
= tuple(list(ctx_keys
))
1222 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1224 lines_to_del_to_del
.append((ctx_keys
, line
))
1225 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1226 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1229 We made the 'bgp bestpath as-path multipath-relax' command
1230 automatically assume 'no-as-set' since the lack of this option caused
1231 weird routing problems. When the running config is shown in
1232 releases with this change, the no-as-set keyword is not shown as it
1233 is the default. This causes frr-reload to unnecessarily unapply
1234 this option only to apply it back again, causing unnecessary session
1237 if "multipath-relax" in line
:
1238 re_asrelax_new
= re
.search(
1239 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1241 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1242 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1244 if re_asrelax_new
and found_asrelax_old
:
1246 lines_to_del_to_del
.append((ctx_keys
, line
))
1247 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1250 If we are modifying the BGP table-map we need to avoid a del/add and
1251 instead modify the table-map in place via an add. This is needed to
1252 avoid installing all routes in the RIB the second the 'no table-map'
1255 if line
.startswith("table-map"):
1256 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1259 lines_to_del_to_del
.append((ctx_keys
, line
))
1262 More old-to-new config handling. ip import-table no longer accepts
1263 distance, but we honor the old syntax. But 'show running' shows only
1264 the new syntax. This causes an unnecessary 'no import-table' followed
1265 by the same old 'ip import-table' which causes perturbations in
1266 announced routes leading to traffic blackholes. Fix this issue.
1268 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1270 table_num
= re_importtbl
.group(1)
1271 for ctx
in lines_to_add
:
1272 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1273 lines_to_del_to_del
.append(
1274 (("ip import-table %s" % table_num
,), None)
1276 lines_to_add_to_del
.append((ctx
[0], None))
1279 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1280 However, the running config always adds 'seq x', where x is a number
1281 incremented by 5 for every element of the prefix/access list.
1282 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1283 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1284 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1285 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1286 access-list FOO seq 5 permit 2.2.2.2/32
1287 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1289 re_acl_pfxlst
= re
.search(
1290 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1296 re_acl_pfxlst
.group(1)
1297 + re_acl_pfxlst
.group(2)
1298 + re_acl_pfxlst
.group(3)
1299 + re_acl_pfxlst
.group(5)
1300 + re_acl_pfxlst
.group(6)
1302 for ctx
in lines_to_add
:
1303 if ctx
[0][0] == tmpline
:
1304 lines_to_del_to_del
.append((ctx_keys
, None))
1305 lines_to_add_to_del
.append(((tmpline
,), None))
1308 If prefix-lists or access-lists are being deleted and
1309 not added (see comment above), add command with 'no' to
1310 lines_to_add and remove from lines_to_del to improve
1311 scaling performance.
1314 add_cmd
= ("no " + ctx_keys
[0],)
1315 lines_to_add
.append((add_cmd
, None))
1316 lines_to_del_to_del
.append((ctx_keys
, None))
1320 and ctx_keys
[0].startswith("router bgp")
1321 and ctx_keys
[1] == "address-family l2vpn evpn"
1322 and ctx_keys
[2].startswith("vni")
1326 re
.search("^route-target import (.*)$", line
)
1332 rt
= re_route_target
.group(1).strip()
1333 route_target_import_line
= line
1334 route_target_export_line
= "route-target export %s" % rt
1335 route_target_both_line
= "route-target both %s" % rt
1337 found_route_target_export_line
= line_exist(
1338 lines_to_del
, ctx_keys
, route_target_export_line
1340 found_route_target_both_line
= line_exist(
1341 lines_to_add
, ctx_keys
, route_target_both_line
1345 If the running configs has
1346 route-target import 1:1
1347 route-target export 1:1
1349 and the config we are reloading against has
1350 route-target both 1:1
1352 then we can ignore deleting the import/export and ignore adding the 'both'
1354 if found_route_target_export_line
and found_route_target_both_line
:
1355 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1356 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1357 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1359 # Deleting static routes under a vrf can lead to time-outs if each is sent
1360 # as separate vtysh -c commands. Change them from being in lines_to_del and
1361 # put the "no" form in lines_to_add
1362 if ctx_keys
[0].startswith("vrf ") and line
:
1363 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1364 add_cmd
= "no " + line
1365 lines_to_add
.append((ctx_keys
, add_cmd
))
1366 lines_to_del_to_del
.append((ctx_keys
, line
))
1369 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1372 lines_to_del_to_del
.append((ctx_keys
, line
))
1373 lines_to_add_to_del
.append((ctx_keys
, line
))
1376 We have commands that used to be displayed in the global part
1377 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1381 neighbor ISL advertisement-interval 0
1387 address-family ipv4 unicast
1388 neighbor ISL advertisement-interval 0
1390 Look to see if we are deleting it in one format just to add it back in the other
1393 ctx_keys
[0].startswith("router bgp")
1394 and len(ctx_keys
) > 1
1395 and ctx_keys
[1] == "address-family ipv4 unicast"
1397 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1398 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1400 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1403 lines_to_del_to_del
.append((ctx_keys
, line
))
1404 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1406 for (ctx_keys
, line
) in lines_to_del_to_del
:
1407 lines_to_del
.remove((ctx_keys
, line
))
1409 for (ctx_keys
, line
) in lines_to_add_to_del
:
1410 lines_to_add
.remove((ctx_keys
, line
))
1412 return (lines_to_add
, lines_to_del
)
1415 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1417 There are certain commands that cannot be removed. Remove
1418 those commands from lines_to_del.
1420 lines_to_del_to_del
= []
1422 for (ctx_keys
, line
) in lines_to_del
:
1425 ctx_keys
[0].startswith("frr version")
1426 or ctx_keys
[0].startswith("frr defaults")
1427 or ctx_keys
[0].startswith("username")
1428 or ctx_keys
[0].startswith("password")
1429 or ctx_keys
[0].startswith("line vty")
1431 # This is technically "no"able but if we did so frr-reload would
1432 # stop working so do not let the user shoot themselves in the foot
1434 ctx_keys
[0].startswith("service integrated-vtysh-config")
1437 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1438 lines_to_del_to_del
.append((ctx_keys
, line
))
1440 for (ctx_keys
, line
) in lines_to_del_to_del
:
1441 lines_to_del
.remove((ctx_keys
, line
))
1443 return (lines_to_add
, lines_to_del
)
1446 def compare_context_objects(newconf
, running
):
1448 Create a context diff for the two specified contexts
1451 # Compare the two Config objects to find the lines that we need to add/del
1458 candidates_to_add
= []
1461 # Find contexts that are in newconf but not in running
1462 # Find contexts that are in running but not in newconf
1463 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1465 if running_ctx_keys
not in newconf
.contexts
:
1467 # We check that the len is 1 here so that we only look at ('router bgp 10')
1468 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1469 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1470 # running but not in newconf.
1471 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1473 lines_to_del
.append((running_ctx_keys
, None))
1475 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1476 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1478 ].startswith("vrf"):
1479 for line
in running_ctx
.lines
:
1480 lines_to_del
.append((running_ctx_keys
, line
))
1482 # If this is an address-family under 'router bgp' and we are already deleting the
1483 # entire 'router bgp' context then ignore this sub-context
1485 "router bgp" in running_ctx_keys
[0]
1486 and len(running_ctx_keys
) > 1
1491 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1493 "router bgp" in running_ctx_keys
[0]
1494 and len(running_ctx_keys
) > 2
1495 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1496 and running_ctx_keys
[2].startswith("vni ")
1498 lines_to_del
.append((running_ctx_keys
, None))
1501 "router bgp" in running_ctx_keys
[0]
1502 and len(running_ctx_keys
) > 1
1503 and running_ctx_keys
[1].startswith("address-family")
1505 # There's no 'no address-family' support and so we have to
1506 # delete each line individually again
1507 for line
in running_ctx
.lines
:
1508 lines_to_del
.append((running_ctx_keys
, line
))
1510 # Some commands can happen at higher counts that make
1511 # doing vtysh -c inefficient (and can time out.) For
1512 # these commands, instead of adding them to lines_to_del,
1513 # add the "no " version to lines_to_add.
1514 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1516 ].startswith("ipv6 route"):
1517 add_cmd
= ("no " + running_ctx_keys
[0],)
1518 lines_to_add
.append((add_cmd
, None))
1520 # if this an interface sub-subcontext in an address-family block in ldpd and
1521 # we are already deleting the whole context, then ignore this
1523 len(running_ctx_keys
) > 2
1524 and running_ctx_keys
[0].startswith("mpls ldp")
1525 and running_ctx_keys
[1].startswith("address-family")
1526 and (running_ctx_keys
[:2], None) in lines_to_del
1530 # same thing for a pseudowire sub-context inside an l2vpn context
1532 len(running_ctx_keys
) > 1
1533 and running_ctx_keys
[0].startswith("l2vpn")
1534 and running_ctx_keys
[1].startswith("member pseudowire")
1535 and (running_ctx_keys
[:1], None) in lines_to_del
1539 # Segment routing and traffic engineering never need to be deleted
1541 running_ctx_keys
[0].startswith("segment-routing")
1542 and len(running_ctx_keys
) < 3
1546 # Neither the pcep command
1548 len(running_ctx_keys
) == 3
1549 and running_ctx_keys
[0].startswith("segment-routing")
1550 and running_ctx_keys
[2].startswith("pcep")
1554 # Segment lists can only be deleted after we removed all the candidate paths that
1555 # use them, so add them to a separate array that is going to be appended at the end
1557 len(running_ctx_keys
) == 3
1558 and running_ctx_keys
[0].startswith("segment-routing")
1559 and running_ctx_keys
[2].startswith("segment-list")
1561 seglist_to_del
.append((running_ctx_keys
, None))
1563 # Policies must be deleted after there candidate path, to be sure
1564 # we add them to a separate array that is going to be appended at the end
1566 len(running_ctx_keys
) == 3
1567 and running_ctx_keys
[0].startswith("segment-routing")
1568 and running_ctx_keys
[2].startswith("policy")
1570 pollist_to_del
.append((running_ctx_keys
, None))
1572 # pce-config must be deleted after the pce, to be sure we add them
1573 # to a separate array that is going to be appended at the end
1575 len(running_ctx_keys
) >= 4
1576 and running_ctx_keys
[0].startswith("segment-routing")
1577 and running_ctx_keys
[3].startswith("pce-config")
1579 pceconf_to_del
.append((running_ctx_keys
, None))
1581 # pcc must be deleted after the pce and pce-config too
1583 len(running_ctx_keys
) >= 4
1584 and running_ctx_keys
[0].startswith("segment-routing")
1585 and running_ctx_keys
[3].startswith("pcc")
1587 pcclist_to_del
.append((running_ctx_keys
, None))
1589 # Non-global context
1590 elif running_ctx_keys
and not any(
1591 "address-family" in key
for key
in running_ctx_keys
1593 lines_to_del
.append((running_ctx_keys
, None))
1595 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1596 lines_to_del
.append((running_ctx_keys
, None))
1600 for line
in running_ctx
.lines
:
1601 lines_to_del
.append((running_ctx_keys
, line
))
1603 # if we have some policies commands to delete, append them to lines_to_del
1604 if len(pollist_to_del
) > 0:
1605 lines_to_del
.extend(pollist_to_del
)
1607 # if we have some segment list commands to delete, append them to lines_to_del
1608 if len(seglist_to_del
) > 0:
1609 lines_to_del
.extend(seglist_to_del
)
1611 # if we have some pce list commands to delete, append them to lines_to_del
1612 if len(pceconf_to_del
) > 0:
1613 lines_to_del
.extend(pceconf_to_del
)
1615 # if we have some pcc list commands to delete, append them to lines_to_del
1616 if len(pcclist_to_del
) > 0:
1617 lines_to_del
.extend(pcclist_to_del
)
1619 # Find the lines within each context to add
1620 # Find the lines within each context to del
1621 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1623 if newconf_ctx_keys
in running
.contexts
:
1624 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1626 for line
in newconf_ctx
.lines
:
1627 if line
not in running_ctx
.dlines
:
1629 # candidate paths can only be added after the policy and segment list,
1630 # so add them to a separate array that is going to be appended at the end
1632 len(newconf_ctx_keys
) == 3
1633 and newconf_ctx_keys
[0].startswith("segment-routing")
1634 and newconf_ctx_keys
[2].startswith("policy ")
1635 and line
.startswith("candidate-path ")
1637 candidates_to_add
.append((newconf_ctx_keys
, line
))
1640 lines_to_add
.append((newconf_ctx_keys
, line
))
1642 for line
in running_ctx
.lines
:
1643 if line
not in newconf_ctx
.dlines
:
1644 lines_to_del
.append((newconf_ctx_keys
, line
))
1646 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1648 if newconf_ctx_keys
not in running
.contexts
:
1650 # candidate paths can only be added after the policy and segment list,
1651 # so add them to a separate array that is going to be appended at the end
1653 len(newconf_ctx_keys
) == 4
1654 and newconf_ctx_keys
[0].startswith("segment-routing")
1655 and newconf_ctx_keys
[3].startswith("candidate-path")
1657 candidates_to_add
.append((newconf_ctx_keys
, None))
1658 for line
in newconf_ctx
.lines
:
1659 candidates_to_add
.append((newconf_ctx_keys
, line
))
1662 lines_to_add
.append((newconf_ctx_keys
, None))
1664 for line
in newconf_ctx
.lines
:
1665 lines_to_add
.append((newconf_ctx_keys
, line
))
1667 # if we have some candidate paths commands to add, append them to lines_to_add
1668 if len(candidates_to_add
) > 0:
1669 lines_to_add
.extend(candidates_to_add
)
1671 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1672 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1673 lines_to_add
, lines_to_del
1675 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1676 lines_to_add
, lines_to_del
1679 return (lines_to_add
, lines_to_del
)
1682 if __name__
== "__main__":
1683 # Command line options
1684 parser
= argparse
.ArgumentParser(
1685 description
="Dynamically apply diff in frr configs"
1687 parser
.add_argument(
1688 "--input", help='Read running config from file instead of "show running"'
1690 group
= parser
.add_mutually_exclusive_group(required
=True)
1692 "--reload", action
="store_true", help="Apply the deltas", default
=False
1695 "--test", action
="store_true", help="Show the deltas", default
=False
1697 level_group
= parser
.add_mutually_exclusive_group()
1698 level_group
.add_argument(
1700 action
="store_true",
1701 help="Enable debugs (synonym for --log-level=debug)",
1704 level_group
.add_argument(
1708 choices
=("critical", "error", "warning", "info", "debug"),
1710 parser
.add_argument(
1711 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1713 parser
.add_argument(
1717 help="Reload specified path/namespace",
1720 parser
.add_argument("filename", help="Location of new frr config file")
1721 parser
.add_argument(
1723 action
="store_true",
1724 help="Overwrite frr.conf with running config output",
1727 parser
.add_argument(
1728 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1730 parser
.add_argument(
1731 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1733 parser
.add_argument(
1734 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1736 parser
.add_argument(
1738 help="socket to be used by vtysh to connect to the daemons",
1741 parser
.add_argument(
1742 "--daemon", help="daemon for which want to replace the config", default
=""
1745 args
= parser
.parse_args()
1748 # For --test log to stdout
1749 # For --reload log to /var/log/frr/frr-reload.log
1750 if args
.test
or args
.stdout
:
1751 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1753 # Color the errors and warnings in red
1754 logging
.addLevelName(
1755 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1757 logging
.addLevelName(
1758 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1762 if not os
.path
.isdir("/var/log/frr/"):
1763 os
.makedirs("/var/log/frr/")
1765 logging
.basicConfig(
1766 filename
="/var/log/frr/frr-reload.log",
1767 format
="%(asctime)s %(levelname)5s: %(message)s",
1770 # argparse should prevent this from happening but just to be safe...
1772 raise Exception("Must specify --reload or --test")
1773 log
= logging
.getLogger(__name__
)
1776 log
.setLevel(logging
.DEBUG
)
1778 log
.setLevel(args
.log_level
.upper())
1780 if args
.reload and not args
.stdout
:
1781 # Additionally send errors and above to STDOUT, with no metadata,
1782 # when we are logging to a file. This specifically does not follow
1783 # args.log_level, and is analagous to behaviour in earlier versions
1784 # which additionally logged most errors using print().
1786 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
1787 stdout_hdlr
.setLevel(logging
.ERROR
)
1788 stdout_hdlr
.setFormatter(logging
.Formatter())
1789 log
.addHandler(stdout_hdlr
)
1791 # Verify the new config file is valid
1792 if not os
.path
.isfile(args
.filename
):
1793 log
.error("Filename %s does not exist" % args
.filename
)
1796 if not os
.path
.getsize(args
.filename
):
1797 log
.error("Filename %s is an empty file" % args
.filename
)
1800 # Verify that confdir is correct
1801 if not os
.path
.isdir(args
.confdir
):
1802 log
.error("Confdir %s is not a valid path" % args
.confdir
)
1805 # Verify that bindir is correct
1806 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
1807 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
1810 # verify that the vty_socket, if specified, is valid
1811 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
1812 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
1815 # verify that the daemon, if specified, is valid
1816 if args
.daemon
and args
.daemon
not in [
1834 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1839 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
1841 # Verify that 'service integrated-vtysh-config' is configured
1843 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
1845 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
1846 service_integrated_vtysh_config
= True
1848 if os
.path
.isfile(vtysh_filename
):
1849 with
open(vtysh_filename
, "r") as fh
:
1850 for line
in fh
.readlines():
1853 if line
== "no service integrated-vtysh-config":
1854 service_integrated_vtysh_config
= False
1857 if not service_integrated_vtysh_config
and not args
.daemon
:
1859 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1863 log
.info('Called via "%s"', str(args
))
1865 # Create a Config object from the config generated by newconf
1866 newconf
= Config(vtysh
)
1868 newconf
.load_from_file(args
.filename
)
1870 except VtyshException
as ve
:
1871 log
.error("vtysh failed to process new configuration: {}".format(ve
))
1876 # Create a Config object from the running config
1877 running
= Config(vtysh
)
1880 running
.load_from_file(args
.input)
1882 running
.load_from_show_running(args
.daemon
)
1884 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1885 lines_to_configure
= []
1888 print("\nLines To Delete")
1889 print("===============")
1891 for (ctx_keys
, line
) in lines_to_del
:
1896 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, True))
1897 lines_to_configure
.append(cmd
)
1901 print("\nLines To Add")
1902 print("============")
1904 for (ctx_keys
, line
) in lines_to_add
:
1909 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False))
1910 lines_to_configure
.append(cmd
)
1915 # We will not be able to do anything, go ahead and exit(1)
1916 if not vtysh
.is_config_available():
1919 log
.debug("New Frr Config\n%s", newconf
.get_lines())
1921 # This looks a little odd but we have to do this twice...here is why
1922 # If the user had this running bgp config:
1925 # neighbor 1.1.1.1 remote-as 50
1926 # neighbor 1.1.1.1 route-map FOO out
1928 # and this config in the newconf config file
1931 # neighbor 1.1.1.1 remote-as 999
1932 # neighbor 1.1.1.1 route-map FOO out
1935 # Then the script will do
1936 # - no neighbor 1.1.1.1 remote-as 50
1937 # - neighbor 1.1.1.1 remote-as 999
1939 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1940 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1941 # configs again to put this line back.
1943 # There are many keywords in FRR that can only appear one time under
1944 # a context, take "bgp router-id" for example. If the config that we are
1945 # reloading against has the following:
1948 # bgp router-id 1.1.1.1
1949 # bgp router-id 2.2.2.2
1951 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1952 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1953 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1954 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1955 # second pass to include all of the "adds" from the first pass.
1956 lines_to_add_first_pass
= []
1959 running
= Config(vtysh
)
1960 running
.load_from_show_running(args
.daemon
)
1961 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
1963 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1966 lines_to_add_first_pass
= lines_to_add
1968 lines_to_add
.extend(lines_to_add_first_pass
)
1970 # Only do deletes on the first pass. The reason being if we
1971 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1972 # will automatically add:
1975 # ipv6 nd ra-interval 10
1976 # no ipv6 nd suppress-ra
1979 # but those lines aren't in the config we are reloading against so
1980 # on the 2nd pass they will show up in lines_to_del. This could
1981 # apply to other scenarios as well where configuring FOO adds BAR
1983 if lines_to_del
and x
== 0:
1984 for (ctx_keys
, line
) in lines_to_del
:
1989 # 'no' commands are tricky, we can't just put them in a file and
1990 # vtysh -f that file. See the next comment for an explanation
1992 cmd
= lines_to_config(ctx_keys
, line
, True)
1995 # Some commands in frr are picky about taking a "no" of the entire line.
1996 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1997 # only the beginning. If we hit one of these command an exception will be
1998 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2001 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2002 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2003 # % Unknown command.
2004 # frr(config-if)# no ip ospf authentication message-digest
2005 # % Unknown command.
2006 # frr(config-if)# no ip ospf authentication
2011 vtysh(["configure"] + cmd
)
2013 except VtyshException
:
2015 # - Pull the last entry from cmd (this would be
2016 # 'no ip ospf authentication message-digest 1.1.1.1' in
2018 # - Split that last entry by whitespace and drop the last word
2019 log
.info("Failed to execute %s", " ".join(cmd
))
2020 last_arg
= cmd
[-1].split(" ")
2022 if len(last_arg
) <= 2:
2024 '"%s" we failed to remove this command',
2025 " -- ".join(original_cmd
),
2029 new_last_arg
= last_arg
[0:-1]
2030 cmd
[-1] = " ".join(new_last_arg
)
2032 log
.info('Executed "%s"', " ".join(cmd
))
2036 lines_to_configure
= []
2038 for (ctx_keys
, line
) in lines_to_add
:
2043 # Don't run "no" commands twice since they can error
2044 # out the second time due to first deletion
2045 if x
== 1 and ctx_keys
[0].startswith("no "):
2048 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2049 lines_to_configure
.append(cmd
)
2051 if lines_to_configure
:
2052 random_string
= "".join(
2053 random
.SystemRandom().choice(
2054 string
.ascii_uppercase
+ string
.digits
2059 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2060 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2062 with
open(filename
, "w") as fh
:
2063 for line
in lines_to_configure
:
2064 fh
.write(line
+ "\n")
2067 vtysh
.exec_file(filename
)
2068 except VtyshException
as e
:
2069 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2073 # Make these changes persistent
2074 target
= str(args
.confdir
+ "/frr.conf")
2075 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):