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-vrf":
633 self
.save_contexts(ctx_keys
, current_context_lines
)
634 current_context_lines
.append(line
)
636 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
639 # Start a new context
643 current_context_lines
= []
647 and len(ctx_keys
) > 1
648 and ctx_keys
[0].startswith("segment-routing")
650 self
.save_contexts(ctx_keys
, current_context_lines
)
652 # Start a new context
653 ctx_keys
= ctx_keys
[:-1]
654 current_context_lines
= []
656 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
661 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
662 # if this exit is for address-family ipv4 unicast, ignore the pop
664 self
.save_contexts(ctx_keys
, current_context_lines
)
666 # Start a new context
667 ctx_keys
= copy
.deepcopy(main_ctx_key
)
668 current_context_lines
= []
670 "LINE %-50s: popping from subcontext to ctx%-50s",
675 elif line
in ["exit-vni", "exit-ldp-if"]:
677 self
.save_contexts(ctx_keys
, current_context_lines
)
679 # Start a new context
680 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
681 current_context_lines
= []
683 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
688 elif new_ctx
is True:
694 ctx_keys
= copy
.deepcopy(main_ctx_key
)
697 current_context_lines
= []
699 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
702 line
.startswith("address-family ")
703 or line
.startswith("vnc defaults")
704 or line
.startswith("vnc l2-group")
705 or line
.startswith("vnc nve-group")
706 or line
.startswith("peer")
707 or line
.startswith("key ")
708 or line
.startswith("member pseudowire")
712 # Save old context first
713 self
.save_contexts(ctx_keys
, current_context_lines
)
714 current_context_lines
= []
715 main_ctx_key
= copy
.deepcopy(ctx_keys
)
716 log
.debug("LINE %-50s: entering sub-context, append to ctx_keys", line
)
718 if line
== "address-family ipv6" and not ctx_keys
[0].startswith(
721 ctx_keys
.append("address-family ipv6 unicast")
722 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith(
725 ctx_keys
.append("address-family ipv4 unicast")
726 elif line
== "address-family evpn":
727 ctx_keys
.append("address-family l2vpn evpn")
729 ctx_keys
.append(line
)
732 line
.startswith("vni ")
733 and len(ctx_keys
) == 2
734 and ctx_keys
[0].startswith("router bgp")
735 and ctx_keys
[1] == "address-family l2vpn evpn"
738 # Save old context first
739 self
.save_contexts(ctx_keys
, current_context_lines
)
740 current_context_lines
= []
741 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
743 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
745 ctx_keys
.append(line
)
748 line
.startswith("interface ")
749 and len(ctx_keys
) == 2
750 and ctx_keys
[0].startswith("mpls ldp")
751 and ctx_keys
[1].startswith("address-family")
754 # Save old context first
755 self
.save_contexts(ctx_keys
, current_context_lines
)
756 current_context_lines
= []
757 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
759 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
761 ctx_keys
.append(line
)
764 line
.startswith("traffic-eng")
765 and len(ctx_keys
) == 1
766 and ctx_keys
[0].startswith("segment-routing")
769 # Save old context first
770 self
.save_contexts(ctx_keys
, current_context_lines
)
771 current_context_lines
= []
773 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
776 ctx_keys
.append(line
)
779 line
.startswith("segment-list ")
780 and len(ctx_keys
) == 2
781 and ctx_keys
[0].startswith("segment-routing")
782 and ctx_keys
[1].startswith("traffic-eng")
785 # Save old context first
786 self
.save_contexts(ctx_keys
, current_context_lines
)
787 current_context_lines
= []
789 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
792 ctx_keys
.append(line
)
795 line
.startswith("policy ")
796 and len(ctx_keys
) == 2
797 and ctx_keys
[0].startswith("segment-routing")
798 and ctx_keys
[1].startswith("traffic-eng")
801 # Save old context first
802 self
.save_contexts(ctx_keys
, current_context_lines
)
803 current_context_lines
= []
805 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
808 ctx_keys
.append(line
)
811 line
.startswith("candidate-path ")
812 and line
.endswith(" dynamic")
813 and len(ctx_keys
) == 3
814 and ctx_keys
[0].startswith("segment-routing")
815 and ctx_keys
[1].startswith("traffic-eng")
816 and ctx_keys
[2].startswith("policy")
819 # Save old context first
820 self
.save_contexts(ctx_keys
, current_context_lines
)
821 current_context_lines
= []
822 main_ctx_key
= copy
.deepcopy(ctx_keys
)
824 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
827 ctx_keys
.append(line
)
830 line
.startswith("pcep")
831 and len(ctx_keys
) == 2
832 and ctx_keys
[0].startswith("segment-routing")
833 and ctx_keys
[1].startswith("traffic-eng")
836 # Save old context first
837 self
.save_contexts(ctx_keys
, current_context_lines
)
838 current_context_lines
= []
839 main_ctx_key
= copy
.deepcopy(ctx_keys
)
841 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
843 ctx_keys
.append(line
)
846 line
.startswith("pce-config ")
847 and len(ctx_keys
) == 3
848 and ctx_keys
[0].startswith("segment-routing")
849 and ctx_keys
[1].startswith("traffic-eng")
850 and ctx_keys
[2].startswith("pcep")
853 # Save old context first
854 self
.save_contexts(ctx_keys
, current_context_lines
)
855 current_context_lines
= []
856 main_ctx_key
= copy
.deepcopy(ctx_keys
)
858 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
861 ctx_keys
.append(line
)
864 line
.startswith("pce ")
865 and len(ctx_keys
) == 3
866 and ctx_keys
[0].startswith("segment-routing")
867 and ctx_keys
[1].startswith("traffic-eng")
868 and ctx_keys
[2].startswith("pcep")
871 # Save old context first
872 self
.save_contexts(ctx_keys
, current_context_lines
)
873 current_context_lines
= []
874 main_ctx_key
= copy
.deepcopy(ctx_keys
)
876 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
878 ctx_keys
.append(line
)
881 line
.startswith("pcc")
882 and len(ctx_keys
) == 3
883 and ctx_keys
[0].startswith("segment-routing")
884 and ctx_keys
[1].startswith("traffic-eng")
885 and ctx_keys
[2].startswith("pcep")
888 # Save old context first
889 self
.save_contexts(ctx_keys
, current_context_lines
)
890 current_context_lines
= []
891 main_ctx_key
= copy
.deepcopy(ctx_keys
)
893 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
895 ctx_keys
.append(line
)
898 line
.startswith('profile ')
899 and len(ctx_keys
) == 1
900 and ctx_keys
[0].startswith('bfd')
903 # Save old context first
904 self
.save_contexts(ctx_keys
, current_context_lines
)
905 current_context_lines
= []
906 main_ctx_key
= copy
.deepcopy(ctx_keys
)
908 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
911 ctx_keys
.append(line
)
914 # Continuing in an existing context, add non-commented lines to it
915 current_context_lines
.append(line
)
917 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
920 # Save the context of the last one
921 self
.save_contexts(ctx_keys
, current_context_lines
)
924 def lines_to_config(ctx_keys
, line
, delete
):
926 Return the command as it would appear in frr.conf
931 for (i
, ctx_key
) in enumerate(ctx_keys
):
932 cmd
.append(" " * i
+ ctx_key
)
935 indent
= len(ctx_keys
) * " "
937 # There are some commands that are on by default so their "no" form will be
938 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
939 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
940 # not by doing a "no no bgp default ipv4-unicast"
942 if line
.startswith("no "):
943 cmd
.append("%s%s" % (indent
, line
[3:]))
945 cmd
.append("%sno %s" % (indent
, line
))
948 cmd
.append(indent
+ line
)
950 # If line is None then we are typically deleting an entire
951 # context ('no router ospf' for example)
953 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
954 cmd
.append("%s%s" % (" " * i
, ctx_key
))
956 # Only put the 'no' on the last sub-context
958 if ctx_keys
[-1].startswith("no "):
959 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
961 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
963 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
968 def get_normalized_ipv6_line(line
):
970 Return a normalized IPv6 line as produced by frr,
971 with all letters in lower case and trailing and leading
972 zeros removed, and only the network portion present if
973 the IPv6 word is a network
976 words
= line
.split(" ")
982 if "ipaddress" not in sys
.modules
:
983 v6word
= IPNetwork(word
)
984 norm_word
= "%s/%s" % (v6word
.network
, v6word
.prefixlen
)
986 v6word
= ip_network(word
, strict
=False)
987 norm_word
= "%s/%s" % (
988 str(v6word
.network_address
),
995 norm_word
= "%s" % IPv6Address(word
)
1000 norm_line
= norm_line
+ " " + norm_word
1002 return norm_line
.strip()
1005 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
1006 for (ctx_keys
, line
) in lines
:
1007 if ctx_keys
== target_ctx_keys
:
1009 if line
== target_line
:
1012 if line
.startswith(target_line
):
1017 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
1019 # exit-vrf is a bit tricky. If the new config is missing it but we
1020 # have configs under a vrf, we need to add it at the end to do the
1021 # right context changes. If exit-vrf exists in both the running and
1022 # new config, we cannot delete it or it will break context changes.
1023 add_exit_vrf
= False
1026 for (ctx_keys
, line
) in lines_to_add
:
1027 if add_exit_vrf
== True:
1028 if ctx_keys
[0] != prior_ctx_key
:
1029 insert_key
= ((prior_ctx_key
),)
1030 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
1031 add_exit_vrf
= False
1033 if ctx_keys
[0].startswith("vrf") and line
:
1034 if line
is not "exit-vrf":
1036 prior_ctx_key
= ctx_keys
[0]
1038 add_exit_vrf
= False
1041 for (ctx_keys
, line
) in lines_to_del
:
1042 if line
== "exit-vrf":
1043 if line_exist(lines_to_add
, ctx_keys
, line
):
1044 lines_to_del
.remove((ctx_keys
, line
))
1046 return (lines_to_add
, lines_to_del
)
1049 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1051 # Quite possibly the most confusing (while accurate) variable names in history
1052 lines_to_add_to_del
= []
1053 lines_to_del_to_del
= []
1055 for (ctx_keys
, line
) in lines_to_del
:
1058 if ctx_keys
[0].startswith("router bgp") and line
:
1060 if line
.startswith("neighbor "):
1062 BGP changed how it displays swpX peers that are part of peer-group. Older
1063 versions of frr would display these on separate lines:
1064 neighbor swp1 interface
1065 neighbor swp1 peer-group FOO
1067 but today we display via a single line
1068 neighbor swp1 interface peer-group FOO
1070 This change confuses frr-reload.py so check to see if we are deleting
1071 neighbor swp1 interface peer-group FOO
1074 neighbor swp1 interface
1075 neighbor swp1 peer-group FOO
1077 If so then chop the del line and the corresponding add lines
1080 re_swpx_int_peergroup
= re
.search(
1081 "neighbor (\S+) interface peer-group (\S+)", line
1083 re_swpx_int_v6only_peergroup
= re
.search(
1084 "neighbor (\S+) interface v6only peer-group (\S+)", line
1087 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1088 swpx_interface
= None
1089 swpx_peergroup
= None
1091 if re_swpx_int_peergroup
:
1092 swpx
= re_swpx_int_peergroup
.group(1)
1093 peergroup
= re_swpx_int_peergroup
.group(2)
1094 swpx_interface
= "neighbor %s interface" % swpx
1095 elif re_swpx_int_v6only_peergroup
:
1096 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1097 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1098 swpx_interface
= "neighbor %s interface v6only" % swpx
1100 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1101 found_add_swpx_interface
= line_exist(
1102 lines_to_add
, ctx_keys
, swpx_interface
1104 found_add_swpx_peergroup
= line_exist(
1105 lines_to_add
, ctx_keys
, swpx_peergroup
1107 tmp_ctx_keys
= tuple(list(ctx_keys
))
1109 if not found_add_swpx_peergroup
:
1110 tmp_ctx_keys
= list(ctx_keys
)
1111 tmp_ctx_keys
.append("address-family ipv4 unicast")
1112 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1113 found_add_swpx_peergroup
= line_exist(
1114 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1117 if not found_add_swpx_peergroup
:
1118 tmp_ctx_keys
= list(ctx_keys
)
1119 tmp_ctx_keys
.append("address-family ipv6 unicast")
1120 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1121 found_add_swpx_peergroup
= line_exist(
1122 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1125 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1127 lines_to_del_to_del
.append((ctx_keys
, line
))
1128 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1129 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1132 Changing the bfd timers on neighbors is allowed without doing
1133 a delete/add process. Since doing a "no neighbor blah bfd ..."
1134 will cause the peer to bounce unnecessarily, just skip the delete
1135 and just do the add.
1137 re_nbr_bfd_timers
= re
.search(
1138 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1141 if re_nbr_bfd_timers
:
1142 nbr
= re_nbr_bfd_timers
.group(1)
1143 bfd_nbr
= "neighbor %s" % nbr
1144 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1146 for (ctx_keys
, add_line
) in lines_to_add
:
1147 if ctx_keys
[0].startswith("router bgp"):
1148 re_add_nbr_bfd_timers
= re
.search(
1149 bfd_search_string
, add_line
1152 if re_add_nbr_bfd_timers
:
1153 found_add_bfd_nbr
= line_exist(
1154 lines_to_add
, ctx_keys
, bfd_nbr
, False
1157 if found_add_bfd_nbr
:
1158 lines_to_del_to_del
.append((ctx_keys
, line
))
1161 We changed how we display the neighbor interface command. Older
1162 versions of frr would display the following:
1163 neighbor swp1 interface
1164 neighbor swp1 remote-as external
1165 neighbor swp1 capability extended-nexthop
1167 but today we display via a single line
1168 neighbor swp1 interface remote-as external
1170 and capability extended-nexthop is no longer needed because we
1171 automatically enable it when the neighbor is of type interface.
1173 This change confuses frr-reload.py so check to see if we are deleting
1174 neighbor swp1 interface remote-as (external|internal|ASNUM)
1177 neighbor swp1 interface
1178 neighbor swp1 remote-as (external|internal|ASNUM)
1179 neighbor swp1 capability extended-nexthop
1181 If so then chop the del line and the corresponding add lines
1183 re_swpx_int_remoteas
= re
.search(
1184 "neighbor (\S+) interface remote-as (\S+)", line
1186 re_swpx_int_v6only_remoteas
= re
.search(
1187 "neighbor (\S+) interface v6only remote-as (\S+)", line
1190 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1191 swpx_interface
= None
1192 swpx_remoteas
= None
1194 if re_swpx_int_remoteas
:
1195 swpx
= re_swpx_int_remoteas
.group(1)
1196 remoteas
= re_swpx_int_remoteas
.group(2)
1197 swpx_interface
= "neighbor %s interface" % swpx
1198 elif re_swpx_int_v6only_remoteas
:
1199 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1200 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1201 swpx_interface
= "neighbor %s interface v6only" % swpx
1203 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1204 found_add_swpx_interface
= line_exist(
1205 lines_to_add
, ctx_keys
, swpx_interface
1207 found_add_swpx_remoteas
= line_exist(
1208 lines_to_add
, ctx_keys
, swpx_remoteas
1210 tmp_ctx_keys
= tuple(list(ctx_keys
))
1212 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1214 lines_to_del_to_del
.append((ctx_keys
, line
))
1215 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1216 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1219 We made the 'bgp bestpath as-path multipath-relax' command
1220 automatically assume 'no-as-set' since the lack of this option caused
1221 weird routing problems. When the running config is shown in
1222 releases with this change, the no-as-set keyword is not shown as it
1223 is the default. This causes frr-reload to unnecessarily unapply
1224 this option only to apply it back again, causing unnecessary session
1227 if "multipath-relax" in line
:
1228 re_asrelax_new
= re
.search(
1229 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1231 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1232 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1234 if re_asrelax_new
and found_asrelax_old
:
1236 lines_to_del_to_del
.append((ctx_keys
, line
))
1237 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1240 If we are modifying the BGP table-map we need to avoid a del/add and
1241 instead modify the table-map in place via an add. This is needed to
1242 avoid installing all routes in the RIB the second the 'no table-map'
1245 if line
.startswith("table-map"):
1246 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1249 lines_to_del_to_del
.append((ctx_keys
, line
))
1252 More old-to-new config handling. ip import-table no longer accepts
1253 distance, but we honor the old syntax. But 'show running' shows only
1254 the new syntax. This causes an unnecessary 'no import-table' followed
1255 by the same old 'ip import-table' which causes perturbations in
1256 announced routes leading to traffic blackholes. Fix this issue.
1258 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1260 table_num
= re_importtbl
.group(1)
1261 for ctx
in lines_to_add
:
1262 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1263 lines_to_del_to_del
.append(
1264 (("ip import-table %s" % table_num
,), None)
1266 lines_to_add_to_del
.append((ctx
[0], None))
1269 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1270 However, the running config always adds 'seq x', where x is a number
1271 incremented by 5 for every element of the prefix/access list.
1272 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1273 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1274 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1275 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1276 access-list FOO seq 5 permit 2.2.2.2/32
1277 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1279 re_acl_pfxlst
= re
.search(
1280 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1286 re_acl_pfxlst
.group(1)
1287 + re_acl_pfxlst
.group(2)
1288 + re_acl_pfxlst
.group(3)
1289 + re_acl_pfxlst
.group(5)
1290 + re_acl_pfxlst
.group(6)
1292 for ctx
in lines_to_add
:
1293 if ctx
[0][0] == tmpline
:
1294 lines_to_del_to_del
.append((ctx_keys
, None))
1295 lines_to_add_to_del
.append(((tmpline
,), None))
1298 If prefix-lists or access-lists are being deleted and
1299 not added (see comment above), add command with 'no' to
1300 lines_to_add and remove from lines_to_del to improve
1301 scaling performance.
1304 add_cmd
= ("no " + ctx_keys
[0],)
1305 lines_to_add
.append((add_cmd
, None))
1306 lines_to_del_to_del
.append((ctx_keys
, None))
1310 and ctx_keys
[0].startswith("router bgp")
1311 and ctx_keys
[1] == "address-family l2vpn evpn"
1312 and ctx_keys
[2].startswith("vni")
1316 re
.search("^route-target import (.*)$", line
)
1322 rt
= re_route_target
.group(1).strip()
1323 route_target_import_line
= line
1324 route_target_export_line
= "route-target export %s" % rt
1325 route_target_both_line
= "route-target both %s" % rt
1327 found_route_target_export_line
= line_exist(
1328 lines_to_del
, ctx_keys
, route_target_export_line
1330 found_route_target_both_line
= line_exist(
1331 lines_to_add
, ctx_keys
, route_target_both_line
1335 If the running configs has
1336 route-target import 1:1
1337 route-target export 1:1
1339 and the config we are reloading against has
1340 route-target both 1:1
1342 then we can ignore deleting the import/export and ignore adding the 'both'
1344 if found_route_target_export_line
and found_route_target_both_line
:
1345 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1346 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1347 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1349 # Deleting static routes under a vrf can lead to time-outs if each is sent
1350 # as separate vtysh -c commands. Change them from being in lines_to_del and
1351 # put the "no" form in lines_to_add
1352 if ctx_keys
[0].startswith("vrf ") and line
:
1353 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1354 add_cmd
= "no " + line
1355 lines_to_add
.append((ctx_keys
, add_cmd
))
1356 lines_to_del_to_del
.append((ctx_keys
, line
))
1359 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1362 lines_to_del_to_del
.append((ctx_keys
, line
))
1363 lines_to_add_to_del
.append((ctx_keys
, line
))
1366 We have commands that used to be displayed in the global part
1367 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1371 neighbor ISL advertisement-interval 0
1377 address-family ipv4 unicast
1378 neighbor ISL advertisement-interval 0
1380 Look to see if we are deleting it in one format just to add it back in the other
1383 ctx_keys
[0].startswith("router bgp")
1384 and len(ctx_keys
) > 1
1385 and ctx_keys
[1] == "address-family ipv4 unicast"
1387 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1388 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1390 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1393 lines_to_del_to_del
.append((ctx_keys
, line
))
1394 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1396 for (ctx_keys
, line
) in lines_to_del_to_del
:
1397 lines_to_del
.remove((ctx_keys
, line
))
1399 for (ctx_keys
, line
) in lines_to_add_to_del
:
1400 lines_to_add
.remove((ctx_keys
, line
))
1402 return (lines_to_add
, lines_to_del
)
1405 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1407 There are certain commands that cannot be removed. Remove
1408 those commands from lines_to_del.
1410 lines_to_del_to_del
= []
1412 for (ctx_keys
, line
) in lines_to_del
:
1415 ctx_keys
[0].startswith("frr version")
1416 or ctx_keys
[0].startswith("frr defaults")
1417 or ctx_keys
[0].startswith("username")
1418 or ctx_keys
[0].startswith("password")
1419 or ctx_keys
[0].startswith("line vty")
1421 # This is technically "no"able but if we did so frr-reload would
1422 # stop working so do not let the user shoot themselves in the foot
1424 ctx_keys
[0].startswith("service integrated-vtysh-config")
1427 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1428 lines_to_del_to_del
.append((ctx_keys
, line
))
1430 for (ctx_keys
, line
) in lines_to_del_to_del
:
1431 lines_to_del
.remove((ctx_keys
, line
))
1433 return (lines_to_add
, lines_to_del
)
1436 def compare_context_objects(newconf
, running
):
1438 Create a context diff for the two specified contexts
1441 # Compare the two Config objects to find the lines that we need to add/del
1448 candidates_to_add
= []
1451 # Find contexts that are in newconf but not in running
1452 # Find contexts that are in running but not in newconf
1453 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1455 if running_ctx_keys
not in newconf
.contexts
:
1457 # We check that the len is 1 here so that we only look at ('router bgp 10')
1458 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1459 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1460 # running but not in newconf.
1461 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1463 lines_to_del
.append((running_ctx_keys
, None))
1465 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1466 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1468 ].startswith("vrf"):
1469 for line
in running_ctx
.lines
:
1470 lines_to_del
.append((running_ctx_keys
, line
))
1472 # If this is an address-family under 'router bgp' and we are already deleting the
1473 # entire 'router bgp' context then ignore this sub-context
1475 "router bgp" in running_ctx_keys
[0]
1476 and len(running_ctx_keys
) > 1
1481 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1483 "router bgp" in running_ctx_keys
[0]
1484 and len(running_ctx_keys
) > 2
1485 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1486 and running_ctx_keys
[2].startswith("vni ")
1488 lines_to_del
.append((running_ctx_keys
, None))
1491 "router bgp" in running_ctx_keys
[0]
1492 and len(running_ctx_keys
) > 1
1493 and running_ctx_keys
[1].startswith("address-family")
1495 # There's no 'no address-family' support and so we have to
1496 # delete each line individually again
1497 for line
in running_ctx
.lines
:
1498 lines_to_del
.append((running_ctx_keys
, line
))
1500 # Some commands can happen at higher counts that make
1501 # doing vtysh -c inefficient (and can time out.) For
1502 # these commands, instead of adding them to lines_to_del,
1503 # add the "no " version to lines_to_add.
1504 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1506 ].startswith("ipv6 route"):
1507 add_cmd
= ("no " + running_ctx_keys
[0],)
1508 lines_to_add
.append((add_cmd
, None))
1510 # if this an interface sub-subcontext in an address-family block in ldpd and
1511 # we are already deleting the whole context, then ignore this
1513 len(running_ctx_keys
) > 2
1514 and running_ctx_keys
[0].startswith("mpls ldp")
1515 and running_ctx_keys
[1].startswith("address-family")
1516 and (running_ctx_keys
[:2], None) in lines_to_del
1520 # same thing for a pseudowire sub-context inside an l2vpn context
1522 len(running_ctx_keys
) > 1
1523 and running_ctx_keys
[0].startswith("l2vpn")
1524 and running_ctx_keys
[1].startswith("member pseudowire")
1525 and (running_ctx_keys
[:1], None) in lines_to_del
1529 # Segment routing and traffic engineering never need to be deleted
1531 running_ctx_keys
[0].startswith("segment-routing")
1532 and len(running_ctx_keys
) < 3
1536 # Neither the pcep command
1538 len(running_ctx_keys
) == 3
1539 and running_ctx_keys
[0].startswith("segment-routing")
1540 and running_ctx_keys
[2].startswith("pcep")
1544 # Segment lists can only be deleted after we removed all the candidate paths that
1545 # use them, so add them to a separate array that is going to be appended at the end
1547 len(running_ctx_keys
) == 3
1548 and running_ctx_keys
[0].startswith("segment-routing")
1549 and running_ctx_keys
[2].startswith("segment-list")
1551 seglist_to_del
.append((running_ctx_keys
, None))
1553 # Policies must be deleted after there candidate path, to be sure
1554 # we add them to a separate array that is going to be appended at the end
1556 len(running_ctx_keys
) == 3
1557 and running_ctx_keys
[0].startswith("segment-routing")
1558 and running_ctx_keys
[2].startswith("policy")
1560 pollist_to_del
.append((running_ctx_keys
, None))
1562 # pce-config must be deleted after the pce, to be sure we add them
1563 # to a separate array that is going to be appended at the end
1565 len(running_ctx_keys
) >= 4
1566 and running_ctx_keys
[0].startswith("segment-routing")
1567 and running_ctx_keys
[3].startswith("pce-config")
1569 pceconf_to_del
.append((running_ctx_keys
, None))
1571 # pcc must be deleted after the pce and pce-config too
1573 len(running_ctx_keys
) >= 4
1574 and running_ctx_keys
[0].startswith("segment-routing")
1575 and running_ctx_keys
[3].startswith("pcc")
1577 pcclist_to_del
.append((running_ctx_keys
, None))
1579 # Non-global context
1580 elif running_ctx_keys
and not any(
1581 "address-family" in key
for key
in running_ctx_keys
1583 lines_to_del
.append((running_ctx_keys
, None))
1585 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1586 lines_to_del
.append((running_ctx_keys
, None))
1590 for line
in running_ctx
.lines
:
1591 lines_to_del
.append((running_ctx_keys
, line
))
1593 # if we have some policies commands to delete, append them to lines_to_del
1594 if len(pollist_to_del
) > 0:
1595 lines_to_del
.extend(pollist_to_del
)
1597 # if we have some segment list commands to delete, append them to lines_to_del
1598 if len(seglist_to_del
) > 0:
1599 lines_to_del
.extend(seglist_to_del
)
1601 # if we have some pce list commands to delete, append them to lines_to_del
1602 if len(pceconf_to_del
) > 0:
1603 lines_to_del
.extend(pceconf_to_del
)
1605 # if we have some pcc list commands to delete, append them to lines_to_del
1606 if len(pcclist_to_del
) > 0:
1607 lines_to_del
.extend(pcclist_to_del
)
1609 # Find the lines within each context to add
1610 # Find the lines within each context to del
1611 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1613 if newconf_ctx_keys
in running
.contexts
:
1614 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1616 for line
in newconf_ctx
.lines
:
1617 if line
not in running_ctx
.dlines
:
1619 # candidate paths can only be added after the policy and segment list,
1620 # so add them to a separate array that is going to be appended at the end
1622 len(newconf_ctx_keys
) == 3
1623 and newconf_ctx_keys
[0].startswith("segment-routing")
1624 and newconf_ctx_keys
[2].startswith("policy ")
1625 and line
.startswith("candidate-path ")
1627 candidates_to_add
.append((newconf_ctx_keys
, line
))
1630 lines_to_add
.append((newconf_ctx_keys
, line
))
1632 for line
in running_ctx
.lines
:
1633 if line
not in newconf_ctx
.dlines
:
1634 lines_to_del
.append((newconf_ctx_keys
, line
))
1636 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1638 if newconf_ctx_keys
not in running
.contexts
:
1640 # candidate paths can only be added after the policy and segment list,
1641 # so add them to a separate array that is going to be appended at the end
1643 len(newconf_ctx_keys
) == 4
1644 and newconf_ctx_keys
[0].startswith("segment-routing")
1645 and newconf_ctx_keys
[3].startswith("candidate-path")
1647 candidates_to_add
.append((newconf_ctx_keys
, None))
1648 for line
in newconf_ctx
.lines
:
1649 candidates_to_add
.append((newconf_ctx_keys
, line
))
1652 lines_to_add
.append((newconf_ctx_keys
, None))
1654 for line
in newconf_ctx
.lines
:
1655 lines_to_add
.append((newconf_ctx_keys
, line
))
1657 # if we have some candidate paths commands to add, append them to lines_to_add
1658 if len(candidates_to_add
) > 0:
1659 lines_to_add
.extend(candidates_to_add
)
1661 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1662 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1663 lines_to_add
, lines_to_del
1665 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1666 lines_to_add
, lines_to_del
1669 return (lines_to_add
, lines_to_del
)
1672 if __name__
== "__main__":
1673 # Command line options
1674 parser
= argparse
.ArgumentParser(
1675 description
="Dynamically apply diff in frr configs"
1677 parser
.add_argument(
1678 "--input", help='Read running config from file instead of "show running"'
1680 group
= parser
.add_mutually_exclusive_group(required
=True)
1682 "--reload", action
="store_true", help="Apply the deltas", default
=False
1685 "--test", action
="store_true", help="Show the deltas", default
=False
1687 level_group
= parser
.add_mutually_exclusive_group()
1688 level_group
.add_argument(
1690 action
="store_true",
1691 help="Enable debugs (synonym for --log-level=debug)",
1694 level_group
.add_argument(
1698 choices
=("critical", "error", "warning", "info", "debug"),
1700 parser
.add_argument(
1701 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1703 parser
.add_argument(
1707 help="Reload specified path/namespace",
1710 parser
.add_argument("filename", help="Location of new frr config file")
1711 parser
.add_argument(
1713 action
="store_true",
1714 help="Overwrite frr.conf with running config output",
1717 parser
.add_argument(
1718 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1720 parser
.add_argument(
1721 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1723 parser
.add_argument(
1724 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1726 parser
.add_argument(
1728 help="socket to be used by vtysh to connect to the daemons",
1731 parser
.add_argument(
1732 "--daemon", help="daemon for which want to replace the config", default
=""
1735 args
= parser
.parse_args()
1738 # For --test log to stdout
1739 # For --reload log to /var/log/frr/frr-reload.log
1740 if args
.test
or args
.stdout
:
1741 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1743 # Color the errors and warnings in red
1744 logging
.addLevelName(
1745 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1747 logging
.addLevelName(
1748 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1752 if not os
.path
.isdir("/var/log/frr/"):
1753 os
.makedirs("/var/log/frr/")
1755 logging
.basicConfig(
1756 filename
="/var/log/frr/frr-reload.log",
1757 format
="%(asctime)s %(levelname)5s: %(message)s",
1760 # argparse should prevent this from happening but just to be safe...
1762 raise Exception("Must specify --reload or --test")
1763 log
= logging
.getLogger(__name__
)
1766 log
.setLevel(logging
.DEBUG
)
1768 log
.setLevel(args
.log_level
.upper())
1770 if args
.reload and not args
.stdout
:
1771 # Additionally send errors and above to STDOUT, with no metadata,
1772 # when we are logging to a file. This specifically does not follow
1773 # args.log_level, and is analagous to behaviour in earlier versions
1774 # which additionally logged most errors using print().
1776 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
1777 stdout_hdlr
.setLevel(logging
.ERROR
)
1778 stdout_hdlr
.setFormatter(logging
.Formatter())
1779 log
.addHandler(stdout_hdlr
)
1781 # Verify the new config file is valid
1782 if not os
.path
.isfile(args
.filename
):
1783 log
.error("Filename %s does not exist" % args
.filename
)
1786 if not os
.path
.getsize(args
.filename
):
1787 log
.error("Filename %s is an empty file" % args
.filename
)
1790 # Verify that confdir is correct
1791 if not os
.path
.isdir(args
.confdir
):
1792 log
.error("Confdir %s is not a valid path" % args
.confdir
)
1795 # Verify that bindir is correct
1796 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
1797 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
1800 # verify that the vty_socket, if specified, is valid
1801 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
1802 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
1805 # verify that the daemon, if specified, is valid
1806 if args
.daemon
and args
.daemon
not in [
1824 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1829 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
1831 # Verify that 'service integrated-vtysh-config' is configured
1833 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
1835 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
1836 service_integrated_vtysh_config
= True
1838 if os
.path
.isfile(vtysh_filename
):
1839 with
open(vtysh_filename
, "r") as fh
:
1840 for line
in fh
.readlines():
1843 if line
== "no service integrated-vtysh-config":
1844 service_integrated_vtysh_config
= False
1847 if not service_integrated_vtysh_config
and not args
.daemon
:
1849 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1853 log
.info('Called via "%s"', str(args
))
1855 # Create a Config object from the config generated by newconf
1856 newconf
= Config(vtysh
)
1858 newconf
.load_from_file(args
.filename
)
1860 except VtyshException
as ve
:
1861 log
.error("vtysh failed to process new configuration: {}".format(ve
))
1866 # Create a Config object from the running config
1867 running
= Config(vtysh
)
1870 running
.load_from_file(args
.input)
1872 running
.load_from_show_running(args
.daemon
)
1874 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1875 lines_to_configure
= []
1878 print("\nLines To Delete")
1879 print("===============")
1881 for (ctx_keys
, line
) in lines_to_del
:
1886 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, True))
1887 lines_to_configure
.append(cmd
)
1891 print("\nLines To Add")
1892 print("============")
1894 for (ctx_keys
, line
) in lines_to_add
:
1899 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False))
1900 lines_to_configure
.append(cmd
)
1905 # We will not be able to do anything, go ahead and exit(1)
1906 if not vtysh
.is_config_available():
1909 log
.debug("New Frr Config\n%s", newconf
.get_lines())
1911 # This looks a little odd but we have to do this twice...here is why
1912 # If the user had this running bgp config:
1915 # neighbor 1.1.1.1 remote-as 50
1916 # neighbor 1.1.1.1 route-map FOO out
1918 # and this config in the newconf config file
1921 # neighbor 1.1.1.1 remote-as 999
1922 # neighbor 1.1.1.1 route-map FOO out
1925 # Then the script will do
1926 # - no neighbor 1.1.1.1 remote-as 50
1927 # - neighbor 1.1.1.1 remote-as 999
1929 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1930 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1931 # configs again to put this line back.
1933 # There are many keywords in FRR that can only appear one time under
1934 # a context, take "bgp router-id" for example. If the config that we are
1935 # reloading against has the following:
1938 # bgp router-id 1.1.1.1
1939 # bgp router-id 2.2.2.2
1941 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1942 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1943 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1944 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1945 # second pass to include all of the "adds" from the first pass.
1946 lines_to_add_first_pass
= []
1949 running
= Config(vtysh
)
1950 running
.load_from_show_running(args
.daemon
)
1951 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
1953 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1956 lines_to_add_first_pass
= lines_to_add
1958 lines_to_add
.extend(lines_to_add_first_pass
)
1960 # Only do deletes on the first pass. The reason being if we
1961 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1962 # will automatically add:
1965 # ipv6 nd ra-interval 10
1966 # no ipv6 nd suppress-ra
1969 # but those lines aren't in the config we are reloading against so
1970 # on the 2nd pass they will show up in lines_to_del. This could
1971 # apply to other scenarios as well where configuring FOO adds BAR
1973 if lines_to_del
and x
== 0:
1974 for (ctx_keys
, line
) in lines_to_del
:
1979 # 'no' commands are tricky, we can't just put them in a file and
1980 # vtysh -f that file. See the next comment for an explanation
1982 cmd
= lines_to_config(ctx_keys
, line
, True)
1985 # Some commands in frr are picky about taking a "no" of the entire line.
1986 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1987 # only the beginning. If we hit one of these command an exception will be
1988 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1991 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1992 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1993 # % Unknown command.
1994 # frr(config-if)# no ip ospf authentication message-digest
1995 # % Unknown command.
1996 # frr(config-if)# no ip ospf authentication
2001 vtysh(["configure"] + cmd
)
2003 except VtyshException
:
2005 # - Pull the last entry from cmd (this would be
2006 # 'no ip ospf authentication message-digest 1.1.1.1' in
2008 # - Split that last entry by whitespace and drop the last word
2009 log
.info("Failed to execute %s", " ".join(cmd
))
2010 last_arg
= cmd
[-1].split(" ")
2012 if len(last_arg
) <= 2:
2014 '"%s" we failed to remove this command',
2015 " -- ".join(original_cmd
),
2019 new_last_arg
= last_arg
[0:-1]
2020 cmd
[-1] = " ".join(new_last_arg
)
2022 log
.info('Executed "%s"', " ".join(cmd
))
2026 lines_to_configure
= []
2028 for (ctx_keys
, line
) in lines_to_add
:
2033 # Don't run "no" commands twice since they can error
2034 # out the second time due to first deletion
2035 if x
== 1 and ctx_keys
[0].startswith("no "):
2038 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2039 lines_to_configure
.append(cmd
)
2041 if lines_to_configure
:
2042 random_string
= "".join(
2043 random
.SystemRandom().choice(
2044 string
.ascii_uppercase
+ string
.digits
2049 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2050 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2052 with
open(filename
, "w") as fh
:
2053 for line
in lines_to_configure
:
2054 fh
.write(line
+ "\n")
2057 vtysh
.exec_file(filename
)
2058 except VtyshException
as e
:
2059 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2063 # Make these changes persistent
2064 target
= str(args
.confdir
+ "/frr.conf")
2065 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):