3 # Copyright (C) 2014 Cumulus Networks, Inc.
5 # This file is part of Frr.
7 # Frr is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by the
9 # Free Software Foundation; either version 2, or (at your option) any
12 # Frr is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Frr; see the file COPYING. If not, write to the Free
19 # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
24 - reads a frr configuration text file
25 - reads frr's current running configuration via "vtysh -c 'show running'"
26 - compares the two configs and determines what commands to execute to
27 synchronize frr's running configuration with the configuation in the
31 from __future__
import print_function
, unicode_literals
41 from collections
import OrderedDict
44 from ipaddress
import IPv6Address
, ip_network
46 from ipaddr
import IPv6Address
, IPNetwork
47 from pprint
import pformat
51 except AttributeError:
54 return iter(d
.items())
63 log
= logging
.getLogger(__name__
)
66 class VtyshException(Exception):
71 def __init__(self
, bindir
=None, confdir
=None, sockdir
=None, pathspace
=None):
73 self
.confdir
= confdir
74 self
.pathspace
= pathspace
75 self
.common_args
= [os
.path
.join(bindir
or "", "vtysh")]
77 self
.common_args
.extend(["--config_dir", confdir
])
79 self
.common_args
.extend(["--vty_socket", sockdir
])
81 self
.common_args
.extend(["-N", pathspace
])
83 def _call(self
, args
, stdin
=None, stdout
=None, stderr
=None):
86 kwargs
["stdin"] = stdin
87 if stdout
is not None:
88 kwargs
["stdout"] = stdout
89 if stderr
is not None:
90 kwargs
["stderr"] = stderr
91 return subprocess
.Popen(self
.common_args
+ args
, **kwargs
)
93 def _call_cmd(self
, command
, stdin
=None, stdout
=None, stderr
=None):
94 if isinstance(command
, list):
95 args
= [item
for sub
in command
for item
in ["-c", sub
]]
97 args
= ["-c", command
]
98 return self
._call
(args
, stdin
, stdout
, stderr
)
100 def __call__(self
, command
, stdouts
=None):
102 Call a CLI command (e.g. "show running-config")
104 Output text is automatically redirected, decoded and returned.
105 Multiple commands may be passed as list.
107 proc
= self
._call
_cmd
(command
, stdout
=subprocess
.PIPE
)
108 stdout
, stderr
= proc
.communicate()
110 if stdouts
is not None:
111 stdouts
.append(stdout
.decode("UTF-8"))
112 raise VtyshException(
113 'vtysh returned status %d for command "%s"' % (proc
.returncode
, command
)
115 return stdout
.decode("UTF-8")
117 def is_config_available(self
):
119 Return False if no frr daemon is running or some other vtysh session is
120 in 'configuration terminal' mode which will prevent us from making any
121 configuration changes.
124 output
= self("configure")
126 if "VTY configuration is locked by other VTY" in output
:
127 log
.error("vtysh 'configure' returned\n%s\n" % (output
))
132 def exec_file(self
, filename
):
133 child
= self
._call
(["-f", filename
])
134 if child
.wait() != 0:
135 raise VtyshException(
136 "vtysh (exec file) exited with status %d" % (child
.returncode
)
139 def mark_file(self
, filename
, stdin
=None):
141 ["-m", "-f", filename
],
142 stdout
=subprocess
.PIPE
,
143 stdin
=subprocess
.PIPE
,
144 stderr
=subprocess
.PIPE
,
147 stdout
, stderr
= child
.communicate()
148 except subprocess
.TimeoutExpired
:
150 stdout
, stderr
= child
.communicate()
151 raise VtyshException("vtysh call timed out!")
153 if child
.wait() != 0:
154 raise VtyshException(
155 "vtysh (mark file) exited with status %d:\n%s"
156 % (child
.returncode
, stderr
)
159 return stdout
.decode("UTF-8")
161 def mark_show_run(self
, daemon
=None):
162 cmd
= "show running-config"
164 cmd
+= " %s" % daemon
166 show_run
= self
._call
_cmd
(cmd
, stdout
=subprocess
.PIPE
)
168 ["-m", "-f", "-"], stdin
=show_run
.stdout
, stdout
=subprocess
.PIPE
172 stdout
, stderr
= mark
.communicate()
175 if show_run
.returncode
!= 0:
176 raise VtyshException(
177 "vtysh (show running-config) exited with status %d:"
178 % (show_run
.returncode
)
180 if mark
.returncode
!= 0:
181 raise VtyshException(
182 "vtysh (mark running-config) exited with status %d" % (mark
.returncode
)
185 return stdout
.decode("UTF-8")
188 class Context(object):
191 A Context object represents a section of frr configuration such as:
194 description swp3 -> r8's swp1
199 or a single line context object such as this:
205 def __init__(self
, keys
, lines
):
209 # Keep a dictionary of the lines, this is to make it easy to tell if a
210 # line exists in this Context
211 self
.dlines
= OrderedDict()
214 self
.dlines
[ligne
] = True
216 def add_lines(self
, lines
):
218 Add lines to specified context
221 self
.lines
.extend(lines
)
224 self
.dlines
[ligne
] = True
227 def get_normalized_es_id(line
):
229 The es-id or es-sys-mac need to be converted to lower case
231 sub_strs
= ["evpn mh es-id", "evpn mh es-sys-mac"]
232 for sub_str
in sub_strs
:
233 obj
= re
.match(sub_str
+ " (?P<esi>\S*)", line
)
235 line
= "%s %s" % (sub_str
, obj
.group("esi").lower())
240 def get_normalized_mac_ip_line(line
):
241 if line
.startswith("evpn mh es"):
242 return get_normalized_es_id(line
)
244 if not "ipv6 add" in line
:
245 return get_normalized_ipv6_line(line
)
250 class Config(object):
253 A frr configuration is stored in a Config object. A Config object
254 contains a dictionary of Context objects where the Context keys
255 ('router ospf' for example) are our dictionary key.
258 def __init__(self
, vtysh
):
260 self
.contexts
= OrderedDict()
263 def load_from_file(self
, filename
):
265 Read configuration from specified file and slurp it into internal memory
266 The internal representation has been marked appropriately by passing it
267 through vtysh with the -m parameter
269 log
.info("Loading Config object from file %s", filename
)
271 file_output
= self
.vtysh
.mark_file(filename
)
273 for line
in file_output
.split("\n"):
276 # Compress duplicate whitespaces
277 line
= " ".join(line
.split())
280 line
= get_normalized_mac_ip_line(line
)
283 vrf static routes can be added in two ways. The old way is:
285 "ip route x.x.x.x/x y.y.y.y vrf <vrfname>"
287 but it's rendered in the configuration as the new way::
290 ip route x.x.x.x/x y.y.y.y
293 this difference causes frr-reload to not consider them a
294 match and delete vrf static routes incorrectly.
295 fix the old way to match new "show running" output so a
296 proper match is found.
299 line
.startswith("ip route ") or line
.startswith("ipv6 route ")
300 ) and " vrf " in line
:
301 newline
= line
.split(" ")
302 vrf_index
= newline
.index("vrf")
303 vrf_ctx
= newline
[vrf_index
] + " " + newline
[vrf_index
+ 1]
304 del newline
[vrf_index
: vrf_index
+ 2]
305 newline
= " ".join(newline
)
306 self
.lines
.append(vrf_ctx
)
307 self
.lines
.append(newline
)
308 self
.lines
.append("exit-vrf")
311 self
.lines
.append(line
)
315 def load_from_show_running(self
, daemon
):
317 Read running configuration and slurp it into internal memory
318 The internal representation has been marked appropriately by passing it
319 through vtysh with the -m parameter
321 log
.info("Loading Config object from vtysh show running")
323 config_text
= self
.vtysh
.mark_show_run(daemon
)
325 for line
in config_text
.split("\n"):
329 line
== "Building configuration..."
330 or line
== "Current configuration:"
335 self
.lines
.append(line
)
341 Return the lines read in from the configuration
344 return "\n".join(self
.lines
)
346 def get_contexts(self
):
348 Return the parsed context as strings for display, log etc.
351 for (_
, ctx
) in sorted(iteritems(self
.contexts
)):
352 print(str(ctx
) + "\n")
354 def save_contexts(self
, key
, lines
):
356 Save the provided key and lines as a context
363 IP addresses specified in "network" statements, "ip prefix-lists"
364 etc. can differ in the host part of the specification the user
365 provides and what the running config displays. For example, user
366 can specify 11.1.1.1/24, and the running config displays this as
367 11.1.1.0/24. Ensure we don't do a needless operation for such
368 lines. IS-IS & OSPFv3 have no "network" support.
370 re_key_rt
= re
.match(r
"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0])
372 addr
= re_key_rt
.group(2)
375 if "ipaddress" not in sys
.modules
:
376 newaddr
= IPNetwork(addr
)
377 key
[0] = "%s route %s/%s%s" % (
384 newaddr
= ip_network(addr
, strict
=False)
385 key
[0] = "%s route %s/%s%s" % (
387 str(newaddr
.network_address
),
394 re_key_rt
= re
.match(
395 r
"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0]
398 addr
= re_key_rt
.group(4)
401 if "ipaddress" not in sys
.modules
:
402 newaddr
= "%s/%s" % (
403 IPNetwork(addr
).network
,
404 IPNetwork(addr
).prefixlen
,
407 network_addr
= ip_network(addr
, strict
=False)
408 newaddr
= "%s/%s" % (
409 str(network_addr
.network_address
),
410 network_addr
.prefixlen
,
417 legestr
= re_key_rt
.group(5)
418 re_lege
= re
.search(r
"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr
)
420 legestr
= "%sge %s le %s%s" % (
426 re_lege
= re
.search(r
"(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)", legestr
)
429 (re_key_rt
.group(1) == "ip" and re_lege
.group(3) == "32")
430 or (re_key_rt
.group(1) == "ipv6" and re_lege
.group(3) == "128")
432 legestr
= "%sge %s%s" % (
438 key
[0] = "%s prefix-list%s%s %s%s" % (
446 if lines
and key
[0].startswith("router bgp"):
449 re_net
= re
.match(r
"network\s+([A-Fa-f:.0-9/]+)(.*)$", line
)
451 addr
= re_net
.group(1)
452 if "/" not in addr
and key
[0].startswith("router bgp"):
453 # This is most likely an error because with no
454 # prefixlen, BGP treats the prefixlen as 8
458 if "ipaddress" not in sys
.modules
:
459 newaddr
= IPNetwork(addr
)
460 line
= "network %s/%s %s" % (
466 network_addr
= ip_network(addr
, strict
=False)
467 line
= "network %s/%s %s" % (
468 str(network_addr
.network_address
),
469 network_addr
.prefixlen
,
472 newlines
.append(line
)
474 # Really this should be an error. Whats a network
475 # without an IP Address following it ?
476 newlines
.append(line
)
478 newlines
.append(line
)
482 More fixups in user specification and what running config shows.
483 "null0" in routes must be replaced by Null0.
486 key
[0].startswith("ip route")
487 or key
[0].startswith("ipv6 route")
488 and "null0" in key
[0]
490 key
[0] = re
.sub(r
"\s+null0(\s*$)", " Null0", key
[0])
493 Similar to above, but when the static is in a vrf, it turns into a
494 blackhole nexthop for both null0 and Null0. Fix it accordingly
496 if lines
and key
[0].startswith("vrf "):
499 if line
.startswith("ip route ") or line
.startswith("ipv6 route "):
501 line
= re
.sub(r
"\s+null0(\s*$)", " blackhole", line
)
502 elif "Null0" in line
:
503 line
= re
.sub(r
"\s+Null0(\s*$)", " blackhole", line
)
504 newlines
.append(line
)
506 newlines
.append(line
)
510 if tuple(key
) not in self
.contexts
:
511 ctx
= Context(tuple(key
), lines
)
512 self
.contexts
[tuple(key
)] = ctx
514 ctx
= self
.contexts
[tuple(key
)]
518 if tuple(key
) not in self
.contexts
:
519 ctx
= Context(tuple(key
), [])
520 self
.contexts
[tuple(key
)] = ctx
522 def load_contexts(self
):
524 Parse the configuration and create contexts for each appropriate block
527 current_context_lines
= []
531 The end of a context is flagged via the 'end' keyword:
540 bgp router-id 10.0.0.1
541 bgp log-neighbor-changes
542 no bgp default ipv4-unicast
543 neighbor EBGP peer-group
544 neighbor EBGP advertisement-interval 1
545 neighbor EBGP timers connect 10
546 neighbor 2001:40:1:4::6 remote-as 40
547 neighbor 2001:40:1:8::a remote-as 40
551 neighbor IBGPv6 activate
552 neighbor 2001:10::2 peer-group IBGPv6
553 neighbor 2001:10::3 peer-group IBGPv6
558 neighbor LEAF activate
562 route-target import 10.1.1.1:10100
563 route-target export 10.1.1.1:10100
569 ospf router-id 10.0.0.1
570 log-adjacency-changes detail
571 timers throttle spf 0 50 5000
576 # The code assumes that its working on the output from the "vtysh -m"
577 # command. That provides the appropriate markers to signify end of
578 # a context. This routine uses that to build the contexts for the
581 # There are single line contexts such as "log file /media/node/zebra.log"
582 # and multi-line contexts such as "router ospf" and subcontexts
583 # within a context such as "address-family" within "router bgp"
584 # In each of these cases, the first line of the context becomes the
585 # key of the context. So "router bgp 10" is the key for the non-address
586 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
587 # the key for the subcontext and so on.
592 # the keywords that we know are single line contexts. bgp in this case
593 # is not the main router bgp block, but enabling multi-instance
594 oneline_ctx_keywords
= (
597 "allow-external-route-update",
619 "vrrp autoconfigure",
623 for line
in self
.lines
:
628 if line
.startswith("!") or line
.startswith("#"):
633 and ctx_keys
[0].startswith("bfd")
634 and ctx_keys
[1].startswith("profile ")
637 log
.debug("LINE %-50s: popping from sub context, %-50s", line
, ctx_keys
)
640 self
.save_contexts(ctx_keys
, current_context_lines
)
641 ctx_keys
= copy
.deepcopy(main_ctx_key
)
642 current_context_lines
= []
646 # there is one exception though: ldpd accepts a 'router-id' clause
647 # as part of its 'mpls ldp' config context. If we are processing
648 # ldp configuration and encounter a router-id we should NOT switch
652 and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
)
655 and ctx_keys
[0].startswith("mpls ldp")
656 and line
.startswith("router-id ")
659 self
.save_contexts(ctx_keys
, current_context_lines
)
661 # Start a new context
666 current_context_lines
= []
668 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
669 self
.save_contexts(ctx_keys
, current_context_lines
)
673 self
.save_contexts(ctx_keys
, current_context_lines
)
674 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
676 # Start a new context
680 current_context_lines
= []
682 elif line
== "exit" and ctx_keys
[0].startswith("rpki"):
683 self
.save_contexts(ctx_keys
, current_context_lines
)
684 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
686 # Start a new context
690 current_context_lines
= []
692 elif line
== "exit-vrf":
693 self
.save_contexts(ctx_keys
, current_context_lines
)
694 current_context_lines
.append(line
)
696 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
699 # Start a new context
703 current_context_lines
= []
707 and len(ctx_keys
) > 1
708 and ctx_keys
[0].startswith("segment-routing")
710 self
.save_contexts(ctx_keys
, current_context_lines
)
712 # Start a new context
713 ctx_keys
= ctx_keys
[:-1]
714 current_context_lines
= []
716 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
721 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
722 # if this exit is for address-family ipv4 unicast, ignore the pop
724 self
.save_contexts(ctx_keys
, current_context_lines
)
726 # Start a new context
727 ctx_keys
= copy
.deepcopy(main_ctx_key
)
728 current_context_lines
= []
730 "LINE %-50s: popping from subcontext to ctx%-50s",
735 elif line
in ["exit-vni", "exit-ldp-if"]:
737 self
.save_contexts(ctx_keys
, current_context_lines
)
739 # Start a new context
740 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
741 current_context_lines
= []
743 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
748 elif new_ctx
is True:
754 ctx_keys
= copy
.deepcopy(main_ctx_key
)
757 current_context_lines
= []
759 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
762 line
.startswith("address-family ")
763 or line
.startswith("vnc defaults")
764 or line
.startswith("vnc l2-group")
765 or line
.startswith("vnc nve-group")
766 or line
.startswith("peer")
767 or line
.startswith("key ")
768 or line
.startswith("member pseudowire")
772 # Save old context first
773 self
.save_contexts(ctx_keys
, current_context_lines
)
774 current_context_lines
= []
775 main_ctx_key
= copy
.deepcopy(ctx_keys
)
776 log
.debug("LINE %-50s: entering sub-context, append to ctx_keys", line
)
778 if line
== "address-family ipv6" and not ctx_keys
[0].startswith(
781 ctx_keys
.append("address-family ipv6 unicast")
782 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith(
785 ctx_keys
.append("address-family ipv4 unicast")
786 elif line
== "address-family evpn":
787 ctx_keys
.append("address-family l2vpn evpn")
789 ctx_keys
.append(line
)
792 line
.startswith("vni ")
793 and len(ctx_keys
) == 2
794 and ctx_keys
[0].startswith("router bgp")
795 and ctx_keys
[1] == "address-family l2vpn evpn"
798 # Save old context first
799 self
.save_contexts(ctx_keys
, current_context_lines
)
800 current_context_lines
= []
801 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
803 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
805 ctx_keys
.append(line
)
808 line
.startswith("interface ")
809 and len(ctx_keys
) == 2
810 and ctx_keys
[0].startswith("mpls ldp")
811 and ctx_keys
[1].startswith("address-family")
814 # Save old context first
815 self
.save_contexts(ctx_keys
, current_context_lines
)
816 current_context_lines
= []
817 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
819 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
821 ctx_keys
.append(line
)
824 line
.startswith("traffic-eng")
825 and len(ctx_keys
) == 1
826 and ctx_keys
[0].startswith("segment-routing")
829 # Save old context first
830 self
.save_contexts(ctx_keys
, current_context_lines
)
831 current_context_lines
= []
833 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
836 ctx_keys
.append(line
)
839 line
.startswith("segment-list ")
840 and len(ctx_keys
) == 2
841 and ctx_keys
[0].startswith("segment-routing")
842 and ctx_keys
[1].startswith("traffic-eng")
845 # Save old context first
846 self
.save_contexts(ctx_keys
, current_context_lines
)
847 current_context_lines
= []
849 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
852 ctx_keys
.append(line
)
855 line
.startswith("policy ")
856 and len(ctx_keys
) == 2
857 and ctx_keys
[0].startswith("segment-routing")
858 and ctx_keys
[1].startswith("traffic-eng")
861 # Save old context first
862 self
.save_contexts(ctx_keys
, current_context_lines
)
863 current_context_lines
= []
865 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
868 ctx_keys
.append(line
)
871 line
.startswith("candidate-path ")
872 and line
.endswith(" dynamic")
873 and len(ctx_keys
) == 3
874 and ctx_keys
[0].startswith("segment-routing")
875 and ctx_keys
[1].startswith("traffic-eng")
876 and ctx_keys
[2].startswith("policy")
879 # Save old context first
880 self
.save_contexts(ctx_keys
, current_context_lines
)
881 current_context_lines
= []
882 main_ctx_key
= copy
.deepcopy(ctx_keys
)
884 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
887 ctx_keys
.append(line
)
890 line
.startswith("pcep")
891 and len(ctx_keys
) == 2
892 and ctx_keys
[0].startswith("segment-routing")
893 and ctx_keys
[1].startswith("traffic-eng")
896 # Save old context first
897 self
.save_contexts(ctx_keys
, current_context_lines
)
898 current_context_lines
= []
899 main_ctx_key
= copy
.deepcopy(ctx_keys
)
901 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
903 ctx_keys
.append(line
)
906 line
.startswith("pce-config ")
907 and len(ctx_keys
) == 3
908 and ctx_keys
[0].startswith("segment-routing")
909 and ctx_keys
[1].startswith("traffic-eng")
910 and ctx_keys
[2].startswith("pcep")
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 pce-config sub-context, append to ctx_keys",
921 ctx_keys
.append(line
)
924 line
.startswith("pce ")
925 and len(ctx_keys
) == 3
926 and ctx_keys
[0].startswith("segment-routing")
927 and ctx_keys
[1].startswith("traffic-eng")
928 and ctx_keys
[2].startswith("pcep")
931 # Save old context first
932 self
.save_contexts(ctx_keys
, current_context_lines
)
933 current_context_lines
= []
934 main_ctx_key
= copy
.deepcopy(ctx_keys
)
936 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
938 ctx_keys
.append(line
)
941 line
.startswith("pcc")
942 and len(ctx_keys
) == 3
943 and ctx_keys
[0].startswith("segment-routing")
944 and ctx_keys
[1].startswith("traffic-eng")
945 and ctx_keys
[2].startswith("pcep")
948 # Save old context first
949 self
.save_contexts(ctx_keys
, current_context_lines
)
950 current_context_lines
= []
951 main_ctx_key
= copy
.deepcopy(ctx_keys
)
953 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
955 ctx_keys
.append(line
)
958 line
.startswith("profile ")
959 and len(ctx_keys
) == 1
960 and ctx_keys
[0].startswith("bfd")
963 # Save old context first
964 self
.save_contexts(ctx_keys
, current_context_lines
)
965 current_context_lines
= []
966 main_ctx_key
= copy
.deepcopy(ctx_keys
)
968 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
971 ctx_keys
.append(line
)
974 # Continuing in an existing context, add non-commented lines to it
975 current_context_lines
.append(line
)
977 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
980 # Save the context of the last one
981 self
.save_contexts(ctx_keys
, current_context_lines
)
984 def lines_to_config(ctx_keys
, line
, delete
):
986 Return the command as it would appear in frr.conf
991 for (i
, ctx_key
) in enumerate(ctx_keys
):
992 cmd
.append(" " * i
+ ctx_key
)
995 indent
= len(ctx_keys
) * " "
997 # There are some commands that are on by default so their "no" form will be
998 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
999 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
1000 # not by doing a "no no bgp default ipv4-unicast"
1002 if line
.startswith("no "):
1003 cmd
.append("%s%s" % (indent
, line
[3:]))
1005 cmd
.append("%sno %s" % (indent
, line
))
1008 cmd
.append(indent
+ line
)
1010 # If line is None then we are typically deleting an entire
1011 # context ('no router ospf' for example)
1013 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
1014 cmd
.append("%s%s" % (" " * i
, ctx_key
))
1016 # Only put the 'no' on the last sub-context
1018 if ctx_keys
[-1].startswith("no "):
1019 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
1021 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
1023 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
1028 def get_normalized_ipv6_line(line
):
1030 Return a normalized IPv6 line as produced by frr,
1031 with all letters in lower case and trailing and leading
1032 zeros removed, and only the network portion present if
1033 the IPv6 word is a network
1036 words
= line
.split(" ")
1042 if "ipaddress" not in sys
.modules
:
1043 v6word
= IPNetwork(word
)
1044 norm_word
= "%s/%s" % (v6word
.network
, v6word
.prefixlen
)
1046 v6word
= ip_network(word
, strict
=False)
1047 norm_word
= "%s/%s" % (
1048 str(v6word
.network_address
),
1055 norm_word
= "%s" % IPv6Address(word
)
1060 norm_line
= norm_line
+ " " + norm_word
1062 return norm_line
.strip()
1065 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
1066 for (ctx_keys
, line
) in lines
:
1067 if ctx_keys
== target_ctx_keys
:
1069 if line
== target_line
:
1072 if line
.startswith(target_line
):
1077 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
1079 # exit-vrf is a bit tricky. If the new config is missing it but we
1080 # have configs under a vrf, we need to add it at the end to do the
1081 # right context changes. If exit-vrf exists in both the running and
1082 # new config, we cannot delete it or it will break context changes.
1083 add_exit_vrf
= False
1086 for (ctx_keys
, line
) in lines_to_add
:
1087 if add_exit_vrf
== True:
1088 if ctx_keys
[0] != prior_ctx_key
:
1089 insert_key
= ((prior_ctx_key
),)
1090 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
1091 add_exit_vrf
= False
1093 if ctx_keys
[0].startswith("vrf") and line
:
1094 if line
is not "exit-vrf":
1096 prior_ctx_key
= ctx_keys
[0]
1098 add_exit_vrf
= False
1101 for (ctx_keys
, line
) in lines_to_del
:
1102 if line
== "exit-vrf":
1103 if line_exist(lines_to_add
, ctx_keys
, line
):
1104 lines_to_del
.remove((ctx_keys
, line
))
1106 return (lines_to_add
, lines_to_del
)
1109 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1111 # Quite possibly the most confusing (while accurate) variable names in history
1112 lines_to_add_to_del
= []
1113 lines_to_del_to_del
= []
1115 for (ctx_keys
, line
) in lines_to_del
:
1118 if ctx_keys
[0].startswith("router bgp") and line
:
1120 if line
.startswith("neighbor "):
1122 BGP changed how it displays swpX peers that are part of peer-group. Older
1123 versions of frr would display these on separate lines:
1124 neighbor swp1 interface
1125 neighbor swp1 peer-group FOO
1127 but today we display via a single line
1128 neighbor swp1 interface peer-group FOO
1130 This change confuses frr-reload.py so check to see if we are deleting
1131 neighbor swp1 interface peer-group FOO
1134 neighbor swp1 interface
1135 neighbor swp1 peer-group FOO
1137 If so then chop the del line and the corresponding add lines
1140 re_swpx_int_peergroup
= re
.search(
1141 "neighbor (\S+) interface peer-group (\S+)", line
1143 re_swpx_int_v6only_peergroup
= re
.search(
1144 "neighbor (\S+) interface v6only peer-group (\S+)", line
1147 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1148 swpx_interface
= None
1149 swpx_peergroup
= None
1151 if re_swpx_int_peergroup
:
1152 swpx
= re_swpx_int_peergroup
.group(1)
1153 peergroup
= re_swpx_int_peergroup
.group(2)
1154 swpx_interface
= "neighbor %s interface" % swpx
1155 elif re_swpx_int_v6only_peergroup
:
1156 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1157 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1158 swpx_interface
= "neighbor %s interface v6only" % swpx
1160 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1161 found_add_swpx_interface
= line_exist(
1162 lines_to_add
, ctx_keys
, swpx_interface
1164 found_add_swpx_peergroup
= line_exist(
1165 lines_to_add
, ctx_keys
, swpx_peergroup
1167 tmp_ctx_keys
= tuple(list(ctx_keys
))
1169 if not found_add_swpx_peergroup
:
1170 tmp_ctx_keys
= list(ctx_keys
)
1171 tmp_ctx_keys
.append("address-family ipv4 unicast")
1172 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1173 found_add_swpx_peergroup
= line_exist(
1174 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1177 if not found_add_swpx_peergroup
:
1178 tmp_ctx_keys
= list(ctx_keys
)
1179 tmp_ctx_keys
.append("address-family ipv6 unicast")
1180 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1181 found_add_swpx_peergroup
= line_exist(
1182 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1185 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1187 lines_to_del_to_del
.append((ctx_keys
, line
))
1188 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1189 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1192 Changing the bfd timers on neighbors is allowed without doing
1193 a delete/add process. Since doing a "no neighbor blah bfd ..."
1194 will cause the peer to bounce unnecessarily, just skip the delete
1195 and just do the add.
1197 re_nbr_bfd_timers
= re
.search(
1198 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1201 if re_nbr_bfd_timers
:
1202 nbr
= re_nbr_bfd_timers
.group(1)
1203 bfd_nbr
= "neighbor %s" % nbr
1204 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1206 for (ctx_keys
, add_line
) in lines_to_add
:
1207 if ctx_keys
[0].startswith("router bgp"):
1208 re_add_nbr_bfd_timers
= re
.search(
1209 bfd_search_string
, add_line
1212 if re_add_nbr_bfd_timers
:
1213 found_add_bfd_nbr
= line_exist(
1214 lines_to_add
, ctx_keys
, bfd_nbr
, False
1217 if found_add_bfd_nbr
:
1218 lines_to_del_to_del
.append((ctx_keys
, line
))
1221 We changed how we display the neighbor interface command. Older
1222 versions of frr would display the following:
1223 neighbor swp1 interface
1224 neighbor swp1 remote-as external
1225 neighbor swp1 capability extended-nexthop
1227 but today we display via a single line
1228 neighbor swp1 interface remote-as external
1230 and capability extended-nexthop is no longer needed because we
1231 automatically enable it when the neighbor is of type interface.
1233 This change confuses frr-reload.py so check to see if we are deleting
1234 neighbor swp1 interface remote-as (external|internal|ASNUM)
1237 neighbor swp1 interface
1238 neighbor swp1 remote-as (external|internal|ASNUM)
1239 neighbor swp1 capability extended-nexthop
1241 If so then chop the del line and the corresponding add lines
1243 re_swpx_int_remoteas
= re
.search(
1244 "neighbor (\S+) interface remote-as (\S+)", line
1246 re_swpx_int_v6only_remoteas
= re
.search(
1247 "neighbor (\S+) interface v6only remote-as (\S+)", line
1250 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1251 swpx_interface
= None
1252 swpx_remoteas
= None
1254 if re_swpx_int_remoteas
:
1255 swpx
= re_swpx_int_remoteas
.group(1)
1256 remoteas
= re_swpx_int_remoteas
.group(2)
1257 swpx_interface
= "neighbor %s interface" % swpx
1258 elif re_swpx_int_v6only_remoteas
:
1259 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1260 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1261 swpx_interface
= "neighbor %s interface v6only" % swpx
1263 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1264 found_add_swpx_interface
= line_exist(
1265 lines_to_add
, ctx_keys
, swpx_interface
1267 found_add_swpx_remoteas
= line_exist(
1268 lines_to_add
, ctx_keys
, swpx_remoteas
1270 tmp_ctx_keys
= tuple(list(ctx_keys
))
1272 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1274 lines_to_del_to_del
.append((ctx_keys
, line
))
1275 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1276 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1279 We made the 'bgp bestpath as-path multipath-relax' command
1280 automatically assume 'no-as-set' since the lack of this option caused
1281 weird routing problems. When the running config is shown in
1282 releases with this change, the no-as-set keyword is not shown as it
1283 is the default. This causes frr-reload to unnecessarily unapply
1284 this option only to apply it back again, causing unnecessary session
1287 if "multipath-relax" in line
:
1288 re_asrelax_new
= re
.search(
1289 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1291 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1292 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1294 if re_asrelax_new
and found_asrelax_old
:
1296 lines_to_del_to_del
.append((ctx_keys
, line
))
1297 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1300 If we are modifying the BGP table-map we need to avoid a del/add and
1301 instead modify the table-map in place via an add. This is needed to
1302 avoid installing all routes in the RIB the second the 'no table-map'
1305 if line
.startswith("table-map"):
1306 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1309 lines_to_del_to_del
.append((ctx_keys
, line
))
1312 More old-to-new config handling. ip import-table no longer accepts
1313 distance, but we honor the old syntax. But 'show running' shows only
1314 the new syntax. This causes an unnecessary 'no import-table' followed
1315 by the same old 'ip import-table' which causes perturbations in
1316 announced routes leading to traffic blackholes. Fix this issue.
1318 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1320 table_num
= re_importtbl
.group(1)
1321 for ctx
in lines_to_add
:
1322 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1323 lines_to_del_to_del
.append(
1324 (("ip import-table %s" % table_num
,), None)
1326 lines_to_add_to_del
.append((ctx
[0], None))
1329 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1330 However, the running config always adds 'seq x', where x is a number
1331 incremented by 5 for every element of the prefix/access list.
1332 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1333 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1334 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1335 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1336 access-list FOO seq 5 permit 2.2.2.2/32
1337 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1339 re_acl_pfxlst
= re
.search(
1340 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1346 re_acl_pfxlst
.group(1)
1347 + re_acl_pfxlst
.group(2)
1348 + re_acl_pfxlst
.group(3)
1349 + re_acl_pfxlst
.group(5)
1350 + re_acl_pfxlst
.group(6)
1352 for ctx
in lines_to_add
:
1353 if ctx
[0][0] == tmpline
:
1354 lines_to_del_to_del
.append((ctx_keys
, None))
1355 lines_to_add_to_del
.append(((tmpline
,), None))
1358 If prefix-lists or access-lists are being deleted and
1359 not added (see comment above), add command with 'no' to
1360 lines_to_add and remove from lines_to_del to improve
1361 scaling performance.
1364 add_cmd
= ("no " + ctx_keys
[0],)
1365 lines_to_add
.append((add_cmd
, None))
1366 lines_to_del_to_del
.append((ctx_keys
, None))
1370 and ctx_keys
[0].startswith("router bgp")
1371 and ctx_keys
[1] == "address-family l2vpn evpn"
1372 and ctx_keys
[2].startswith("vni")
1376 re
.search("^route-target import (.*)$", line
)
1382 rt
= re_route_target
.group(1).strip()
1383 route_target_import_line
= line
1384 route_target_export_line
= "route-target export %s" % rt
1385 route_target_both_line
= "route-target both %s" % rt
1387 found_route_target_export_line
= line_exist(
1388 lines_to_del
, ctx_keys
, route_target_export_line
1390 found_route_target_both_line
= line_exist(
1391 lines_to_add
, ctx_keys
, route_target_both_line
1395 If the running configs has
1396 route-target import 1:1
1397 route-target export 1:1
1399 and the config we are reloading against has
1400 route-target both 1:1
1402 then we can ignore deleting the import/export and ignore adding the 'both'
1404 if found_route_target_export_line
and found_route_target_both_line
:
1405 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1406 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1407 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1409 # Deleting static routes under a vrf can lead to time-outs if each is sent
1410 # as separate vtysh -c commands. Change them from being in lines_to_del and
1411 # put the "no" form in lines_to_add
1412 if ctx_keys
[0].startswith("vrf ") and line
:
1413 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1414 add_cmd
= "no " + line
1415 lines_to_add
.append((ctx_keys
, add_cmd
))
1416 lines_to_del_to_del
.append((ctx_keys
, line
))
1419 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1422 lines_to_del_to_del
.append((ctx_keys
, line
))
1423 lines_to_add_to_del
.append((ctx_keys
, line
))
1426 We have commands that used to be displayed in the global part
1427 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1431 neighbor ISL advertisement-interval 0
1437 address-family ipv4 unicast
1438 neighbor ISL advertisement-interval 0
1440 Look to see if we are deleting it in one format just to add it back in the other
1443 ctx_keys
[0].startswith("router bgp")
1444 and len(ctx_keys
) > 1
1445 and ctx_keys
[1] == "address-family ipv4 unicast"
1447 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1448 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1450 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1453 lines_to_del_to_del
.append((ctx_keys
, line
))
1454 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1456 for (ctx_keys
, line
) in lines_to_del_to_del
:
1457 lines_to_del
.remove((ctx_keys
, line
))
1459 for (ctx_keys
, line
) in lines_to_add_to_del
:
1460 lines_to_add
.remove((ctx_keys
, line
))
1462 return (lines_to_add
, lines_to_del
)
1465 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1467 There are certain commands that cannot be removed. Remove
1468 those commands from lines_to_del.
1470 lines_to_del_to_del
= []
1472 for (ctx_keys
, line
) in lines_to_del
:
1475 ctx_keys
[0].startswith("frr version")
1476 or ctx_keys
[0].startswith("frr defaults")
1477 or ctx_keys
[0].startswith("username")
1478 or ctx_keys
[0].startswith("password")
1479 or ctx_keys
[0].startswith("line vty")
1481 # This is technically "no"able but if we did so frr-reload would
1482 # stop working so do not let the user shoot themselves in the foot
1484 ctx_keys
[0].startswith("service integrated-vtysh-config")
1487 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1488 lines_to_del_to_del
.append((ctx_keys
, line
))
1490 for (ctx_keys
, line
) in lines_to_del_to_del
:
1491 lines_to_del
.remove((ctx_keys
, line
))
1493 return (lines_to_add
, lines_to_del
)
1496 def compare_context_objects(newconf
, running
):
1498 Create a context diff for the two specified contexts
1501 # Compare the two Config objects to find the lines that we need to add/del
1508 candidates_to_add
= []
1511 # Find contexts that are in newconf but not in running
1512 # Find contexts that are in running but not in newconf
1513 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1515 if running_ctx_keys
not in newconf
.contexts
:
1517 # We check that the len is 1 here so that we only look at ('router bgp 10')
1518 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1519 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1520 # running but not in newconf.
1521 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1523 lines_to_del
.append((running_ctx_keys
, None))
1525 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1526 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1528 ].startswith("vrf"):
1529 for line
in running_ctx
.lines
:
1530 lines_to_del
.append((running_ctx_keys
, line
))
1532 # If this is an address-family under 'router bgp' and we are already deleting the
1533 # entire 'router bgp' context then ignore this sub-context
1535 "router bgp" in running_ctx_keys
[0]
1536 and len(running_ctx_keys
) > 1
1541 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1543 "router bgp" in running_ctx_keys
[0]
1544 and len(running_ctx_keys
) > 2
1545 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1546 and running_ctx_keys
[2].startswith("vni ")
1548 lines_to_del
.append((running_ctx_keys
, None))
1551 "router bgp" in running_ctx_keys
[0]
1552 and len(running_ctx_keys
) > 1
1553 and running_ctx_keys
[1].startswith("address-family")
1555 # There's no 'no address-family' support and so we have to
1556 # delete each line individually again
1557 for line
in running_ctx
.lines
:
1558 lines_to_del
.append((running_ctx_keys
, line
))
1560 # Some commands can happen at higher counts that make
1561 # doing vtysh -c inefficient (and can time out.) For
1562 # these commands, instead of adding them to lines_to_del,
1563 # add the "no " version to lines_to_add.
1564 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1566 ].startswith("ipv6 route"):
1567 add_cmd
= ("no " + running_ctx_keys
[0],)
1568 lines_to_add
.append((add_cmd
, None))
1570 # if this an interface sub-subcontext in an address-family block in ldpd and
1571 # we are already deleting the whole context, then ignore this
1573 len(running_ctx_keys
) > 2
1574 and running_ctx_keys
[0].startswith("mpls ldp")
1575 and running_ctx_keys
[1].startswith("address-family")
1576 and (running_ctx_keys
[:2], None) in lines_to_del
1580 # same thing for a pseudowire sub-context inside an l2vpn context
1582 len(running_ctx_keys
) > 1
1583 and running_ctx_keys
[0].startswith("l2vpn")
1584 and running_ctx_keys
[1].startswith("member pseudowire")
1585 and (running_ctx_keys
[:1], None) in lines_to_del
1589 # Segment routing and traffic engineering never need to be deleted
1591 running_ctx_keys
[0].startswith("segment-routing")
1592 and len(running_ctx_keys
) < 3
1596 # Neither the pcep command
1598 len(running_ctx_keys
) == 3
1599 and running_ctx_keys
[0].startswith("segment-routing")
1600 and running_ctx_keys
[2].startswith("pcep")
1604 # Segment lists can only be deleted after we removed all the candidate paths that
1605 # use them, so add them to a separate array that is going to be appended at the end
1607 len(running_ctx_keys
) == 3
1608 and running_ctx_keys
[0].startswith("segment-routing")
1609 and running_ctx_keys
[2].startswith("segment-list")
1611 seglist_to_del
.append((running_ctx_keys
, None))
1613 # Policies must be deleted after there candidate path, to be sure
1614 # we add them to a separate array that is going to be appended at the end
1616 len(running_ctx_keys
) == 3
1617 and running_ctx_keys
[0].startswith("segment-routing")
1618 and running_ctx_keys
[2].startswith("policy")
1620 pollist_to_del
.append((running_ctx_keys
, None))
1622 # pce-config must be deleted after the pce, to be sure we add them
1623 # to a separate array that is going to be appended at the end
1625 len(running_ctx_keys
) >= 4
1626 and running_ctx_keys
[0].startswith("segment-routing")
1627 and running_ctx_keys
[3].startswith("pce-config")
1629 pceconf_to_del
.append((running_ctx_keys
, None))
1631 # pcc must be deleted after the pce and pce-config too
1633 len(running_ctx_keys
) >= 4
1634 and running_ctx_keys
[0].startswith("segment-routing")
1635 and running_ctx_keys
[3].startswith("pcc")
1637 pcclist_to_del
.append((running_ctx_keys
, None))
1639 # Non-global context
1640 elif running_ctx_keys
and not any(
1641 "address-family" in key
for key
in running_ctx_keys
1643 lines_to_del
.append((running_ctx_keys
, None))
1645 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1646 lines_to_del
.append((running_ctx_keys
, None))
1650 for line
in running_ctx
.lines
:
1651 lines_to_del
.append((running_ctx_keys
, line
))
1653 # if we have some policies commands to delete, append them to lines_to_del
1654 if len(pollist_to_del
) > 0:
1655 lines_to_del
.extend(pollist_to_del
)
1657 # if we have some segment list commands to delete, append them to lines_to_del
1658 if len(seglist_to_del
) > 0:
1659 lines_to_del
.extend(seglist_to_del
)
1661 # if we have some pce list commands to delete, append them to lines_to_del
1662 if len(pceconf_to_del
) > 0:
1663 lines_to_del
.extend(pceconf_to_del
)
1665 # if we have some pcc list commands to delete, append them to lines_to_del
1666 if len(pcclist_to_del
) > 0:
1667 lines_to_del
.extend(pcclist_to_del
)
1669 # Find the lines within each context to add
1670 # Find the lines within each context to del
1671 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1673 if newconf_ctx_keys
in running
.contexts
:
1674 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1676 for line
in newconf_ctx
.lines
:
1677 if line
not in running_ctx
.dlines
:
1679 # candidate paths can only be added after the policy and segment list,
1680 # so add them to a separate array that is going to be appended at the end
1682 len(newconf_ctx_keys
) == 3
1683 and newconf_ctx_keys
[0].startswith("segment-routing")
1684 and newconf_ctx_keys
[2].startswith("policy ")
1685 and line
.startswith("candidate-path ")
1687 candidates_to_add
.append((newconf_ctx_keys
, line
))
1690 lines_to_add
.append((newconf_ctx_keys
, line
))
1692 for line
in running_ctx
.lines
:
1693 if line
not in newconf_ctx
.dlines
:
1694 lines_to_del
.append((newconf_ctx_keys
, line
))
1696 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1698 if newconf_ctx_keys
not in running
.contexts
:
1700 # candidate paths can only be added after the policy and segment list,
1701 # so add them to a separate array that is going to be appended at the end
1703 len(newconf_ctx_keys
) == 4
1704 and newconf_ctx_keys
[0].startswith("segment-routing")
1705 and newconf_ctx_keys
[3].startswith("candidate-path")
1707 candidates_to_add
.append((newconf_ctx_keys
, None))
1708 for line
in newconf_ctx
.lines
:
1709 candidates_to_add
.append((newconf_ctx_keys
, line
))
1712 lines_to_add
.append((newconf_ctx_keys
, None))
1714 for line
in newconf_ctx
.lines
:
1715 lines_to_add
.append((newconf_ctx_keys
, line
))
1717 # if we have some candidate paths commands to add, append them to lines_to_add
1718 if len(candidates_to_add
) > 0:
1719 lines_to_add
.extend(candidates_to_add
)
1721 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1722 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1723 lines_to_add
, lines_to_del
1725 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1726 lines_to_add
, lines_to_del
1729 return (lines_to_add
, lines_to_del
)
1732 if __name__
== "__main__":
1733 # Command line options
1734 parser
= argparse
.ArgumentParser(
1735 description
="Dynamically apply diff in frr configs"
1737 parser
.add_argument(
1738 "--input", help='Read running config from file instead of "show running"'
1740 group
= parser
.add_mutually_exclusive_group(required
=True)
1742 "--reload", action
="store_true", help="Apply the deltas", default
=False
1745 "--test", action
="store_true", help="Show the deltas", default
=False
1747 level_group
= parser
.add_mutually_exclusive_group()
1748 level_group
.add_argument(
1750 action
="store_true",
1751 help="Enable debugs (synonym for --log-level=debug)",
1754 level_group
.add_argument(
1758 choices
=("critical", "error", "warning", "info", "debug"),
1760 parser
.add_argument(
1761 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1763 parser
.add_argument(
1767 help="Reload specified path/namespace",
1770 parser
.add_argument("filename", help="Location of new frr config file")
1771 parser
.add_argument(
1773 action
="store_true",
1774 help="Overwrite frr.conf with running config output",
1777 parser
.add_argument(
1778 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1780 parser
.add_argument(
1781 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1783 parser
.add_argument(
1784 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1786 parser
.add_argument(
1788 help="socket to be used by vtysh to connect to the daemons",
1791 parser
.add_argument(
1792 "--daemon", help="daemon for which want to replace the config", default
=""
1795 args
= parser
.parse_args()
1798 # For --test log to stdout
1799 # For --reload log to /var/log/frr/frr-reload.log
1800 if args
.test
or args
.stdout
:
1801 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1803 # Color the errors and warnings in red
1804 logging
.addLevelName(
1805 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1807 logging
.addLevelName(
1808 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1812 if not os
.path
.isdir("/var/log/frr/"):
1813 os
.makedirs("/var/log/frr/")
1815 logging
.basicConfig(
1816 filename
="/var/log/frr/frr-reload.log",
1817 format
="%(asctime)s %(levelname)5s: %(message)s",
1820 # argparse should prevent this from happening but just to be safe...
1822 raise Exception("Must specify --reload or --test")
1823 log
= logging
.getLogger(__name__
)
1826 log
.setLevel(logging
.DEBUG
)
1828 log
.setLevel(args
.log_level
.upper())
1830 if args
.reload and not args
.stdout
:
1831 # Additionally send errors and above to STDOUT, with no metadata,
1832 # when we are logging to a file. This specifically does not follow
1833 # args.log_level, and is analagous to behaviour in earlier versions
1834 # which additionally logged most errors using print().
1836 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
1837 stdout_hdlr
.setLevel(logging
.ERROR
)
1838 stdout_hdlr
.setFormatter(logging
.Formatter())
1839 log
.addHandler(stdout_hdlr
)
1841 # Verify the new config file is valid
1842 if not os
.path
.isfile(args
.filename
):
1843 log
.error("Filename %s does not exist" % args
.filename
)
1846 if not os
.path
.getsize(args
.filename
):
1847 log
.error("Filename %s is an empty file" % args
.filename
)
1850 # Verify that confdir is correct
1851 if not os
.path
.isdir(args
.confdir
):
1852 log
.error("Confdir %s is not a valid path" % args
.confdir
)
1855 # Verify that bindir is correct
1856 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
1857 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
1860 # verify that the vty_socket, if specified, is valid
1861 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
1862 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
1865 # verify that the daemon, if specified, is valid
1866 if args
.daemon
and args
.daemon
not in [
1884 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1889 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
1891 # Verify that 'service integrated-vtysh-config' is configured
1893 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
1895 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
1896 service_integrated_vtysh_config
= True
1898 if os
.path
.isfile(vtysh_filename
):
1899 with
open(vtysh_filename
, "r") as fh
:
1900 for line
in fh
.readlines():
1903 if line
== "no service integrated-vtysh-config":
1904 service_integrated_vtysh_config
= False
1907 if not service_integrated_vtysh_config
and not args
.daemon
:
1909 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1913 log
.info('Called via "%s"', str(args
))
1915 # Create a Config object from the config generated by newconf
1916 newconf
= Config(vtysh
)
1918 newconf
.load_from_file(args
.filename
)
1920 except VtyshException
as ve
:
1921 log
.error("vtysh failed to process new configuration: {}".format(ve
))
1926 # Create a Config object from the running config
1927 running
= Config(vtysh
)
1930 running
.load_from_file(args
.input)
1932 running
.load_from_show_running(args
.daemon
)
1934 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1935 lines_to_configure
= []
1938 print("\nLines To Delete")
1939 print("===============")
1941 for (ctx_keys
, line
) in lines_to_del
:
1946 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, True))
1947 lines_to_configure
.append(cmd
)
1951 print("\nLines To Add")
1952 print("============")
1954 for (ctx_keys
, line
) in lines_to_add
:
1959 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False))
1960 lines_to_configure
.append(cmd
)
1965 # We will not be able to do anything, go ahead and exit(1)
1966 if not vtysh
.is_config_available():
1969 log
.debug("New Frr Config\n%s", newconf
.get_lines())
1971 # This looks a little odd but we have to do this twice...here is why
1972 # If the user had this running bgp config:
1975 # neighbor 1.1.1.1 remote-as 50
1976 # neighbor 1.1.1.1 route-map FOO out
1978 # and this config in the newconf config file
1981 # neighbor 1.1.1.1 remote-as 999
1982 # neighbor 1.1.1.1 route-map FOO out
1985 # Then the script will do
1986 # - no neighbor 1.1.1.1 remote-as 50
1987 # - neighbor 1.1.1.1 remote-as 999
1989 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1990 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1991 # configs again to put this line back.
1993 # There are many keywords in FRR that can only appear one time under
1994 # a context, take "bgp router-id" for example. If the config that we are
1995 # reloading against has the following:
1998 # bgp router-id 1.1.1.1
1999 # bgp router-id 2.2.2.2
2001 # The final config needs to contain "bgp router-id 2.2.2.2". On the
2002 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
2003 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
2004 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
2005 # second pass to include all of the "adds" from the first pass.
2006 lines_to_add_first_pass
= []
2009 running
= Config(vtysh
)
2010 running
.load_from_show_running(args
.daemon
)
2011 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
2013 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
2016 lines_to_add_first_pass
= lines_to_add
2018 lines_to_add
.extend(lines_to_add_first_pass
)
2020 # Only do deletes on the first pass. The reason being if we
2021 # configure a bgp neighbor via "neighbor swp1 interface" FRR
2022 # will automatically add:
2025 # ipv6 nd ra-interval 10
2026 # no ipv6 nd suppress-ra
2029 # but those lines aren't in the config we are reloading against so
2030 # on the 2nd pass they will show up in lines_to_del. This could
2031 # apply to other scenarios as well where configuring FOO adds BAR
2033 if lines_to_del
and x
== 0:
2034 for (ctx_keys
, line
) in lines_to_del
:
2039 # 'no' commands are tricky, we can't just put them in a file and
2040 # vtysh -f that file. See the next comment for an explanation
2042 cmd
= lines_to_config(ctx_keys
, line
, True)
2045 # Some commands in frr are picky about taking a "no" of the entire line.
2046 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
2047 # only the beginning. If we hit one of these command an exception will be
2048 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2051 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2052 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2053 # % Unknown command.
2054 # frr(config-if)# no ip ospf authentication message-digest
2055 # % Unknown command.
2056 # frr(config-if)# no ip ospf authentication
2062 vtysh(["configure"] + cmd
, stdouts
)
2064 except VtyshException
:
2066 # - Pull the last entry from cmd (this would be
2067 # 'no ip ospf authentication message-digest 1.1.1.1' in
2069 # - Split that last entry by whitespace and drop the last word
2070 log
.info("Failed to execute %s", " ".join(cmd
))
2071 last_arg
= cmd
[-1].split(" ")
2073 if len(last_arg
) <= 2:
2075 '"%s" we failed to remove this command',
2076 " -- ".join(original_cmd
),
2078 # Log first error msg for original_cmd
2080 log
.error(stdouts
[0])
2084 new_last_arg
= last_arg
[0:-1]
2085 cmd
[-1] = " ".join(new_last_arg
)
2087 log
.info('Executed "%s"', " ".join(cmd
))
2091 lines_to_configure
= []
2093 for (ctx_keys
, line
) in lines_to_add
:
2098 # Don't run "no" commands twice since they can error
2099 # out the second time due to first deletion
2100 if x
== 1 and ctx_keys
[0].startswith("no "):
2103 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2104 lines_to_configure
.append(cmd
)
2106 if lines_to_configure
:
2107 random_string
= "".join(
2108 random
.SystemRandom().choice(
2109 string
.ascii_uppercase
+ string
.digits
2114 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2115 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2117 with
open(filename
, "w") as fh
:
2118 for line
in lines_to_configure
:
2119 fh
.write(line
+ "\n")
2122 vtysh
.exec_file(filename
)
2123 except VtyshException
as e
:
2124 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2128 # Make these changes persistent
2129 target
= str(args
.confdir
+ "/frr.conf")
2130 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):