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
)
282 self
.lines
.append(line
)
286 def load_from_show_running(self
, daemon
):
288 Read running configuration and slurp it into internal memory
289 The internal representation has been marked appropriately by passing it
290 through vtysh with the -m parameter
292 log
.info("Loading Config object from vtysh show running")
294 config_text
= self
.vtysh
.mark_show_run(daemon
)
296 for line
in config_text
.split("\n"):
300 line
== "Building configuration..."
301 or line
== "Current configuration:"
306 self
.lines
.append(line
)
312 Return the lines read in from the configuration
315 return "\n".join(self
.lines
)
317 def get_contexts(self
):
319 Return the parsed context as strings for display, log etc.
322 for (_
, ctx
) in sorted(iteritems(self
.contexts
)):
323 print(str(ctx
) + "\n")
325 def save_contexts(self
, key
, lines
):
327 Save the provided key and lines as a context
334 IP addresses specified in "network" statements, "ip prefix-lists"
335 etc. can differ in the host part of the specification the user
336 provides and what the running config displays. For example, user
337 can specify 11.1.1.1/24, and the running config displays this as
338 11.1.1.0/24. Ensure we don't do a needless operation for such
339 lines. IS-IS & OSPFv3 have no "network" support.
341 re_key_rt
= re
.match(r
"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0])
343 addr
= re_key_rt
.group(2)
346 if "ipaddress" not in sys
.modules
:
347 newaddr
= IPNetwork(addr
)
348 key
[0] = "%s route %s/%s%s" % (
355 newaddr
= ip_network(addr
, strict
=False)
356 key
[0] = "%s route %s/%s%s" % (
358 str(newaddr
.network_address
),
365 re_key_rt
= re
.match(
366 r
"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0]
369 addr
= re_key_rt
.group(4)
372 if "ipaddress" not in sys
.modules
:
373 newaddr
= "%s/%s" % (
374 IPNetwork(addr
).network
,
375 IPNetwork(addr
).prefixlen
,
378 network_addr
= ip_network(addr
, strict
=False)
379 newaddr
= "%s/%s" % (
380 str(network_addr
.network_address
),
381 network_addr
.prefixlen
,
388 legestr
= re_key_rt
.group(5)
389 re_lege
= re
.search(r
"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr
)
391 legestr
= "%sge %s le %s%s" % (
397 re_lege
= re
.search(r
"(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)", legestr
)
400 (re_key_rt
.group(1) == "ip" and re_lege
.group(3) == "32")
401 or (re_key_rt
.group(1) == "ipv6" and re_lege
.group(3) == "128")
403 legestr
= "%sge %s%s" % (
409 key
[0] = "%s prefix-list%s%s %s%s" % (
417 if lines
and key
[0].startswith("router bgp"):
420 re_net
= re
.match(r
"network\s+([A-Fa-f:.0-9/]+)(.*)$", line
)
422 addr
= re_net
.group(1)
423 if "/" not in addr
and key
[0].startswith("router bgp"):
424 # This is most likely an error because with no
425 # prefixlen, BGP treats the prefixlen as 8
429 if "ipaddress" not in sys
.modules
:
430 newaddr
= IPNetwork(addr
)
431 line
= "network %s/%s %s" % (
437 network_addr
= ip_network(addr
, strict
=False)
438 line
= "network %s/%s %s" % (
439 str(network_addr
.network_address
),
440 network_addr
.prefixlen
,
443 newlines
.append(line
)
445 # Really this should be an error. Whats a network
446 # without an IP Address following it ?
447 newlines
.append(line
)
449 newlines
.append(line
)
453 More fixups in user specification and what running config shows.
454 "null0" in routes must be replaced by Null0.
457 key
[0].startswith("ip route")
458 or key
[0].startswith("ipv6 route")
459 and "null0" in key
[0]
461 key
[0] = re
.sub(r
"\s+null0(\s*$)", " Null0", key
[0])
464 if tuple(key
) not in self
.contexts
:
465 ctx
= Context(tuple(key
), lines
)
466 self
.contexts
[tuple(key
)] = ctx
468 ctx
= self
.contexts
[tuple(key
)]
472 if tuple(key
) not in self
.contexts
:
473 ctx
= Context(tuple(key
), [])
474 self
.contexts
[tuple(key
)] = ctx
476 def load_contexts(self
):
478 Parse the configuration and create contexts for each appropriate block
481 current_context_lines
= []
485 The end of a context is flagged via the 'end' keyword:
494 bgp router-id 10.0.0.1
495 bgp log-neighbor-changes
496 no bgp default ipv4-unicast
497 neighbor EBGP peer-group
498 neighbor EBGP advertisement-interval 1
499 neighbor EBGP timers connect 10
500 neighbor 2001:40:1:4::6 remote-as 40
501 neighbor 2001:40:1:8::a remote-as 40
505 neighbor IBGPv6 activate
506 neighbor 2001:10::2 peer-group IBGPv6
507 neighbor 2001:10::3 peer-group IBGPv6
512 neighbor LEAF activate
516 route-target import 10.1.1.1:10100
517 route-target export 10.1.1.1:10100
523 ospf router-id 10.0.0.1
524 log-adjacency-changes detail
525 timers throttle spf 0 50 5000
530 # The code assumes that its working on the output from the "vtysh -m"
531 # command. That provides the appropriate markers to signify end of
532 # a context. This routine uses that to build the contexts for the
535 # There are single line contexts such as "log file /media/node/zebra.log"
536 # and multi-line contexts such as "router ospf" and subcontexts
537 # within a context such as "address-family" within "router bgp"
538 # In each of these cases, the first line of the context becomes the
539 # key of the context. So "router bgp 10" is the key for the non-address
540 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
541 # the key for the subcontext and so on.
546 # the keywords that we know are single line contexts. bgp in this case
547 # is not the main router bgp block, but enabling multi-instance
548 oneline_ctx_keywords
= (
551 "allow-external-route-update",
573 "vrrp autoconfigure",
577 for line
in self
.lines
:
582 if line
.startswith("!") or line
.startswith("#"):
585 if (len(ctx_keys
) == 2
586 and ctx_keys
[0].startswith('bfd')
587 and ctx_keys
[1].startswith('profile ')
589 log
.debug('LINE %-50s: popping from sub context, %-50s', line
, ctx_keys
)
592 self
.save_contexts(ctx_keys
, current_context_lines
)
593 ctx_keys
= copy
.deepcopy(main_ctx_key
)
594 current_context_lines
= []
598 # there is one exception though: ldpd accepts a 'router-id' clause
599 # as part of its 'mpls ldp' config context. If we are processing
600 # ldp configuration and encounter a router-id we should NOT switch
604 and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
)
607 and ctx_keys
[0].startswith("mpls ldp")
608 and line
.startswith("router-id ")
611 self
.save_contexts(ctx_keys
, current_context_lines
)
613 # Start a new context
618 current_context_lines
= []
620 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
621 self
.save_contexts(ctx_keys
, current_context_lines
)
625 self
.save_contexts(ctx_keys
, current_context_lines
)
626 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
628 # Start a new context
632 current_context_lines
= []
634 elif line
== "exit" and ctx_keys
[0].startswith("rpki"):
635 self
.save_contexts(ctx_keys
, current_context_lines
)
636 log
.debug("LINE %-50s: exiting old context, %-50s", line
, ctx_keys
)
638 # Start a new context
642 current_context_lines
= []
644 elif line
== "exit-vrf":
645 self
.save_contexts(ctx_keys
, current_context_lines
)
646 current_context_lines
.append(line
)
648 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
651 # Start a new context
655 current_context_lines
= []
659 and len(ctx_keys
) > 1
660 and ctx_keys
[0].startswith("segment-routing")
662 self
.save_contexts(ctx_keys
, current_context_lines
)
664 # Start a new context
665 ctx_keys
= ctx_keys
[:-1]
666 current_context_lines
= []
668 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
673 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
674 # if this exit is for address-family ipv4 unicast, ignore the pop
676 self
.save_contexts(ctx_keys
, current_context_lines
)
678 # Start a new context
679 ctx_keys
= copy
.deepcopy(main_ctx_key
)
680 current_context_lines
= []
682 "LINE %-50s: popping from subcontext to ctx%-50s",
687 elif line
in ["exit-vni", "exit-ldp-if"]:
689 self
.save_contexts(ctx_keys
, current_context_lines
)
691 # Start a new context
692 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
693 current_context_lines
= []
695 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
700 elif new_ctx
is True:
706 ctx_keys
= copy
.deepcopy(main_ctx_key
)
709 current_context_lines
= []
711 log
.debug("LINE %-50s: entering new context, %-50s", line
, ctx_keys
)
714 line
.startswith("address-family ")
715 or line
.startswith("vnc defaults")
716 or line
.startswith("vnc l2-group")
717 or line
.startswith("vnc nve-group")
718 or line
.startswith("peer")
719 or line
.startswith("key ")
720 or line
.startswith("member pseudowire")
724 # Save old context first
725 self
.save_contexts(ctx_keys
, current_context_lines
)
726 current_context_lines
= []
727 main_ctx_key
= copy
.deepcopy(ctx_keys
)
728 log
.debug("LINE %-50s: entering sub-context, append to ctx_keys", line
)
730 if line
== "address-family ipv6" and not ctx_keys
[0].startswith(
733 ctx_keys
.append("address-family ipv6 unicast")
734 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith(
737 ctx_keys
.append("address-family ipv4 unicast")
738 elif line
== "address-family evpn":
739 ctx_keys
.append("address-family l2vpn evpn")
741 ctx_keys
.append(line
)
744 line
.startswith("vni ")
745 and len(ctx_keys
) == 2
746 and ctx_keys
[0].startswith("router bgp")
747 and ctx_keys
[1] == "address-family l2vpn evpn"
750 # Save old context first
751 self
.save_contexts(ctx_keys
, current_context_lines
)
752 current_context_lines
= []
753 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
755 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
757 ctx_keys
.append(line
)
760 line
.startswith("interface ")
761 and len(ctx_keys
) == 2
762 and ctx_keys
[0].startswith("mpls ldp")
763 and ctx_keys
[1].startswith("address-family")
766 # Save old context first
767 self
.save_contexts(ctx_keys
, current_context_lines
)
768 current_context_lines
= []
769 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
771 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
773 ctx_keys
.append(line
)
776 line
.startswith("traffic-eng")
777 and len(ctx_keys
) == 1
778 and ctx_keys
[0].startswith("segment-routing")
781 # Save old context first
782 self
.save_contexts(ctx_keys
, current_context_lines
)
783 current_context_lines
= []
785 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
788 ctx_keys
.append(line
)
791 line
.startswith("segment-list ")
792 and len(ctx_keys
) == 2
793 and ctx_keys
[0].startswith("segment-routing")
794 and ctx_keys
[1].startswith("traffic-eng")
797 # Save old context first
798 self
.save_contexts(ctx_keys
, current_context_lines
)
799 current_context_lines
= []
801 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
804 ctx_keys
.append(line
)
807 line
.startswith("policy ")
808 and len(ctx_keys
) == 2
809 and ctx_keys
[0].startswith("segment-routing")
810 and ctx_keys
[1].startswith("traffic-eng")
813 # Save old context first
814 self
.save_contexts(ctx_keys
, current_context_lines
)
815 current_context_lines
= []
817 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
820 ctx_keys
.append(line
)
823 line
.startswith("candidate-path ")
824 and line
.endswith(" dynamic")
825 and len(ctx_keys
) == 3
826 and ctx_keys
[0].startswith("segment-routing")
827 and ctx_keys
[1].startswith("traffic-eng")
828 and ctx_keys
[2].startswith("policy")
831 # Save old context first
832 self
.save_contexts(ctx_keys
, current_context_lines
)
833 current_context_lines
= []
834 main_ctx_key
= copy
.deepcopy(ctx_keys
)
836 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
839 ctx_keys
.append(line
)
842 line
.startswith("pcep")
843 and len(ctx_keys
) == 2
844 and ctx_keys
[0].startswith("segment-routing")
845 and ctx_keys
[1].startswith("traffic-eng")
848 # Save old context first
849 self
.save_contexts(ctx_keys
, current_context_lines
)
850 current_context_lines
= []
851 main_ctx_key
= copy
.deepcopy(ctx_keys
)
853 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
855 ctx_keys
.append(line
)
858 line
.startswith("pce-config ")
859 and len(ctx_keys
) == 3
860 and ctx_keys
[0].startswith("segment-routing")
861 and ctx_keys
[1].startswith("traffic-eng")
862 and ctx_keys
[2].startswith("pcep")
865 # Save old context first
866 self
.save_contexts(ctx_keys
, current_context_lines
)
867 current_context_lines
= []
868 main_ctx_key
= copy
.deepcopy(ctx_keys
)
870 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
873 ctx_keys
.append(line
)
876 line
.startswith("pce ")
877 and len(ctx_keys
) == 3
878 and ctx_keys
[0].startswith("segment-routing")
879 and ctx_keys
[1].startswith("traffic-eng")
880 and ctx_keys
[2].startswith("pcep")
883 # Save old context first
884 self
.save_contexts(ctx_keys
, current_context_lines
)
885 current_context_lines
= []
886 main_ctx_key
= copy
.deepcopy(ctx_keys
)
888 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
890 ctx_keys
.append(line
)
893 line
.startswith("pcc")
894 and len(ctx_keys
) == 3
895 and ctx_keys
[0].startswith("segment-routing")
896 and ctx_keys
[1].startswith("traffic-eng")
897 and ctx_keys
[2].startswith("pcep")
900 # Save old context first
901 self
.save_contexts(ctx_keys
, current_context_lines
)
902 current_context_lines
= []
903 main_ctx_key
= copy
.deepcopy(ctx_keys
)
905 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
907 ctx_keys
.append(line
)
910 line
.startswith('profile ')
911 and len(ctx_keys
) == 1
912 and ctx_keys
[0].startswith('bfd')
915 # Save old context first
916 self
.save_contexts(ctx_keys
, current_context_lines
)
917 current_context_lines
= []
918 main_ctx_key
= copy
.deepcopy(ctx_keys
)
920 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
923 ctx_keys
.append(line
)
926 # Continuing in an existing context, add non-commented lines to it
927 current_context_lines
.append(line
)
929 "LINE %-50s: append to current_context_lines, %-50s", line
, ctx_keys
932 # Save the context of the last one
933 self
.save_contexts(ctx_keys
, current_context_lines
)
936 def lines_to_config(ctx_keys
, line
, delete
):
938 Return the command as it would appear in frr.conf
943 for (i
, ctx_key
) in enumerate(ctx_keys
):
944 cmd
.append(" " * i
+ ctx_key
)
947 indent
= len(ctx_keys
) * " "
949 # There are some commands that are on by default so their "no" form will be
950 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
951 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
952 # not by doing a "no no bgp default ipv4-unicast"
954 if line
.startswith("no "):
955 cmd
.append("%s%s" % (indent
, line
[3:]))
957 cmd
.append("%sno %s" % (indent
, line
))
960 cmd
.append(indent
+ line
)
962 # If line is None then we are typically deleting an entire
963 # context ('no router ospf' for example)
965 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
966 cmd
.append("%s%s" % (" " * i
, ctx_key
))
968 # Only put the 'no' on the last sub-context
970 if ctx_keys
[-1].startswith("no "):
971 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
973 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
975 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
980 def get_normalized_ipv6_line(line
):
982 Return a normalized IPv6 line as produced by frr,
983 with all letters in lower case and trailing and leading
984 zeros removed, and only the network portion present if
985 the IPv6 word is a network
988 words
= line
.split(" ")
994 if "ipaddress" not in sys
.modules
:
995 v6word
= IPNetwork(word
)
996 norm_word
= "%s/%s" % (v6word
.network
, v6word
.prefixlen
)
998 v6word
= ip_network(word
, strict
=False)
999 norm_word
= "%s/%s" % (
1000 str(v6word
.network_address
),
1007 norm_word
= "%s" % IPv6Address(word
)
1012 norm_line
= norm_line
+ " " + norm_word
1014 return norm_line
.strip()
1017 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
1018 for (ctx_keys
, line
) in lines
:
1019 if ctx_keys
== target_ctx_keys
:
1021 if line
== target_line
:
1024 if line
.startswith(target_line
):
1029 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
1031 # exit-vrf is a bit tricky. If the new config is missing it but we
1032 # have configs under a vrf, we need to add it at the end to do the
1033 # right context changes. If exit-vrf exists in both the running and
1034 # new config, we cannot delete it or it will break context changes.
1035 add_exit_vrf
= False
1038 for (ctx_keys
, line
) in lines_to_add
:
1039 if add_exit_vrf
== True:
1040 if ctx_keys
[0] != prior_ctx_key
:
1041 insert_key
= ((prior_ctx_key
),)
1042 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
1043 add_exit_vrf
= False
1045 if ctx_keys
[0].startswith("vrf") and line
:
1046 if line
is not "exit-vrf":
1048 prior_ctx_key
= ctx_keys
[0]
1050 add_exit_vrf
= False
1053 for (ctx_keys
, line
) in lines_to_del
:
1054 if line
== "exit-vrf":
1055 if line_exist(lines_to_add
, ctx_keys
, line
):
1056 lines_to_del
.remove((ctx_keys
, line
))
1058 return (lines_to_add
, lines_to_del
)
1061 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1063 # Quite possibly the most confusing (while accurate) variable names in history
1064 lines_to_add_to_del
= []
1065 lines_to_del_to_del
= []
1067 for (ctx_keys
, line
) in lines_to_del
:
1070 if ctx_keys
[0].startswith("router bgp") and line
:
1072 if line
.startswith("neighbor "):
1074 BGP changed how it displays swpX peers that are part of peer-group. Older
1075 versions of frr would display these on separate lines:
1076 neighbor swp1 interface
1077 neighbor swp1 peer-group FOO
1079 but today we display via a single line
1080 neighbor swp1 interface peer-group FOO
1082 This change confuses frr-reload.py so check to see if we are deleting
1083 neighbor swp1 interface peer-group FOO
1086 neighbor swp1 interface
1087 neighbor swp1 peer-group FOO
1089 If so then chop the del line and the corresponding add lines
1092 re_swpx_int_peergroup
= re
.search(
1093 "neighbor (\S+) interface peer-group (\S+)", line
1095 re_swpx_int_v6only_peergroup
= re
.search(
1096 "neighbor (\S+) interface v6only peer-group (\S+)", line
1099 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1100 swpx_interface
= None
1101 swpx_peergroup
= None
1103 if re_swpx_int_peergroup
:
1104 swpx
= re_swpx_int_peergroup
.group(1)
1105 peergroup
= re_swpx_int_peergroup
.group(2)
1106 swpx_interface
= "neighbor %s interface" % swpx
1107 elif re_swpx_int_v6only_peergroup
:
1108 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1109 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1110 swpx_interface
= "neighbor %s interface v6only" % swpx
1112 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1113 found_add_swpx_interface
= line_exist(
1114 lines_to_add
, ctx_keys
, swpx_interface
1116 found_add_swpx_peergroup
= line_exist(
1117 lines_to_add
, ctx_keys
, swpx_peergroup
1119 tmp_ctx_keys
= tuple(list(ctx_keys
))
1121 if not found_add_swpx_peergroup
:
1122 tmp_ctx_keys
= list(ctx_keys
)
1123 tmp_ctx_keys
.append("address-family ipv4 unicast")
1124 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1125 found_add_swpx_peergroup
= line_exist(
1126 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1129 if not found_add_swpx_peergroup
:
1130 tmp_ctx_keys
= list(ctx_keys
)
1131 tmp_ctx_keys
.append("address-family ipv6 unicast")
1132 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1133 found_add_swpx_peergroup
= line_exist(
1134 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1137 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1139 lines_to_del_to_del
.append((ctx_keys
, line
))
1140 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1141 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1144 Changing the bfd timers on neighbors is allowed without doing
1145 a delete/add process. Since doing a "no neighbor blah bfd ..."
1146 will cause the peer to bounce unnecessarily, just skip the delete
1147 and just do the add.
1149 re_nbr_bfd_timers
= re
.search(
1150 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1153 if re_nbr_bfd_timers
:
1154 nbr
= re_nbr_bfd_timers
.group(1)
1155 bfd_nbr
= "neighbor %s" % nbr
1156 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1158 for (ctx_keys
, add_line
) in lines_to_add
:
1159 if ctx_keys
[0].startswith("router bgp"):
1160 re_add_nbr_bfd_timers
= re
.search(
1161 bfd_search_string
, add_line
1164 if re_add_nbr_bfd_timers
:
1165 found_add_bfd_nbr
= line_exist(
1166 lines_to_add
, ctx_keys
, bfd_nbr
, False
1169 if found_add_bfd_nbr
:
1170 lines_to_del_to_del
.append((ctx_keys
, line
))
1173 We changed how we display the neighbor interface command. Older
1174 versions of frr would display the following:
1175 neighbor swp1 interface
1176 neighbor swp1 remote-as external
1177 neighbor swp1 capability extended-nexthop
1179 but today we display via a single line
1180 neighbor swp1 interface remote-as external
1182 and capability extended-nexthop is no longer needed because we
1183 automatically enable it when the neighbor is of type interface.
1185 This change confuses frr-reload.py so check to see if we are deleting
1186 neighbor swp1 interface remote-as (external|internal|ASNUM)
1189 neighbor swp1 interface
1190 neighbor swp1 remote-as (external|internal|ASNUM)
1191 neighbor swp1 capability extended-nexthop
1193 If so then chop the del line and the corresponding add lines
1195 re_swpx_int_remoteas
= re
.search(
1196 "neighbor (\S+) interface remote-as (\S+)", line
1198 re_swpx_int_v6only_remoteas
= re
.search(
1199 "neighbor (\S+) interface v6only remote-as (\S+)", line
1202 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1203 swpx_interface
= None
1204 swpx_remoteas
= None
1206 if re_swpx_int_remoteas
:
1207 swpx
= re_swpx_int_remoteas
.group(1)
1208 remoteas
= re_swpx_int_remoteas
.group(2)
1209 swpx_interface
= "neighbor %s interface" % swpx
1210 elif re_swpx_int_v6only_remoteas
:
1211 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1212 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1213 swpx_interface
= "neighbor %s interface v6only" % swpx
1215 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1216 found_add_swpx_interface
= line_exist(
1217 lines_to_add
, ctx_keys
, swpx_interface
1219 found_add_swpx_remoteas
= line_exist(
1220 lines_to_add
, ctx_keys
, swpx_remoteas
1222 tmp_ctx_keys
= tuple(list(ctx_keys
))
1224 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1226 lines_to_del_to_del
.append((ctx_keys
, line
))
1227 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1228 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1231 We made the 'bgp bestpath as-path multipath-relax' command
1232 automatically assume 'no-as-set' since the lack of this option caused
1233 weird routing problems. When the running config is shown in
1234 releases with this change, the no-as-set keyword is not shown as it
1235 is the default. This causes frr-reload to unnecessarily unapply
1236 this option only to apply it back again, causing unnecessary session
1239 if "multipath-relax" in line
:
1240 re_asrelax_new
= re
.search(
1241 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1243 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1244 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1246 if re_asrelax_new
and found_asrelax_old
:
1248 lines_to_del_to_del
.append((ctx_keys
, line
))
1249 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1252 If we are modifying the BGP table-map we need to avoid a del/add and
1253 instead modify the table-map in place via an add. This is needed to
1254 avoid installing all routes in the RIB the second the 'no table-map'
1257 if line
.startswith("table-map"):
1258 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1261 lines_to_del_to_del
.append((ctx_keys
, line
))
1264 More old-to-new config handling. ip import-table no longer accepts
1265 distance, but we honor the old syntax. But 'show running' shows only
1266 the new syntax. This causes an unnecessary 'no import-table' followed
1267 by the same old 'ip import-table' which causes perturbations in
1268 announced routes leading to traffic blackholes. Fix this issue.
1270 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1272 table_num
= re_importtbl
.group(1)
1273 for ctx
in lines_to_add
:
1274 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1275 lines_to_del_to_del
.append(
1276 (("ip import-table %s" % table_num
,), None)
1278 lines_to_add_to_del
.append((ctx
[0], None))
1281 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1282 However, the running config always adds 'seq x', where x is a number
1283 incremented by 5 for every element of the prefix/access list.
1284 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1285 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1286 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1287 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1288 access-list FOO seq 5 permit 2.2.2.2/32
1289 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1291 re_acl_pfxlst
= re
.search(
1292 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1298 re_acl_pfxlst
.group(1)
1299 + re_acl_pfxlst
.group(2)
1300 + re_acl_pfxlst
.group(3)
1301 + re_acl_pfxlst
.group(5)
1302 + re_acl_pfxlst
.group(6)
1304 for ctx
in lines_to_add
:
1305 if ctx
[0][0] == tmpline
:
1306 lines_to_del_to_del
.append((ctx_keys
, None))
1307 lines_to_add_to_del
.append(((tmpline
,), None))
1310 If prefix-lists or access-lists are being deleted and
1311 not added (see comment above), add command with 'no' to
1312 lines_to_add and remove from lines_to_del to improve
1313 scaling performance.
1316 add_cmd
= ("no " + ctx_keys
[0],)
1317 lines_to_add
.append((add_cmd
, None))
1318 lines_to_del_to_del
.append((ctx_keys
, None))
1322 and ctx_keys
[0].startswith("router bgp")
1323 and ctx_keys
[1] == "address-family l2vpn evpn"
1324 and ctx_keys
[2].startswith("vni")
1328 re
.search("^route-target import (.*)$", line
)
1334 rt
= re_route_target
.group(1).strip()
1335 route_target_import_line
= line
1336 route_target_export_line
= "route-target export %s" % rt
1337 route_target_both_line
= "route-target both %s" % rt
1339 found_route_target_export_line
= line_exist(
1340 lines_to_del
, ctx_keys
, route_target_export_line
1342 found_route_target_both_line
= line_exist(
1343 lines_to_add
, ctx_keys
, route_target_both_line
1347 If the running configs has
1348 route-target import 1:1
1349 route-target export 1:1
1351 and the config we are reloading against has
1352 route-target both 1:1
1354 then we can ignore deleting the import/export and ignore adding the 'both'
1356 if found_route_target_export_line
and found_route_target_both_line
:
1357 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1358 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1359 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1361 # Deleting static routes under a vrf can lead to time-outs if each is sent
1362 # as separate vtysh -c commands. Change them from being in lines_to_del and
1363 # put the "no" form in lines_to_add
1364 if ctx_keys
[0].startswith("vrf ") and line
:
1365 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1366 add_cmd
= "no " + line
1367 lines_to_add
.append((ctx_keys
, add_cmd
))
1368 lines_to_del_to_del
.append((ctx_keys
, line
))
1371 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1374 lines_to_del_to_del
.append((ctx_keys
, line
))
1375 lines_to_add_to_del
.append((ctx_keys
, line
))
1378 We have commands that used to be displayed in the global part
1379 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1383 neighbor ISL advertisement-interval 0
1389 address-family ipv4 unicast
1390 neighbor ISL advertisement-interval 0
1392 Look to see if we are deleting it in one format just to add it back in the other
1395 ctx_keys
[0].startswith("router bgp")
1396 and len(ctx_keys
) > 1
1397 and ctx_keys
[1] == "address-family ipv4 unicast"
1399 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1400 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1402 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1405 lines_to_del_to_del
.append((ctx_keys
, line
))
1406 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1408 for (ctx_keys
, line
) in lines_to_del_to_del
:
1409 lines_to_del
.remove((ctx_keys
, line
))
1411 for (ctx_keys
, line
) in lines_to_add_to_del
:
1412 lines_to_add
.remove((ctx_keys
, line
))
1414 return (lines_to_add
, lines_to_del
)
1417 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1419 There are certain commands that cannot be removed. Remove
1420 those commands from lines_to_del.
1422 lines_to_del_to_del
= []
1424 for (ctx_keys
, line
) in lines_to_del
:
1427 ctx_keys
[0].startswith("frr version")
1428 or ctx_keys
[0].startswith("frr defaults")
1429 or ctx_keys
[0].startswith("username")
1430 or ctx_keys
[0].startswith("password")
1431 or ctx_keys
[0].startswith("line vty")
1433 # This is technically "no"able but if we did so frr-reload would
1434 # stop working so do not let the user shoot themselves in the foot
1436 ctx_keys
[0].startswith("service integrated-vtysh-config")
1439 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1440 lines_to_del_to_del
.append((ctx_keys
, line
))
1442 for (ctx_keys
, line
) in lines_to_del_to_del
:
1443 lines_to_del
.remove((ctx_keys
, line
))
1445 return (lines_to_add
, lines_to_del
)
1448 def compare_context_objects(newconf
, running
):
1450 Create a context diff for the two specified contexts
1453 # Compare the two Config objects to find the lines that we need to add/del
1460 candidates_to_add
= []
1463 # Find contexts that are in newconf but not in running
1464 # Find contexts that are in running but not in newconf
1465 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1467 if running_ctx_keys
not in newconf
.contexts
:
1469 # We check that the len is 1 here so that we only look at ('router bgp 10')
1470 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1471 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1472 # running but not in newconf.
1473 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1475 lines_to_del
.append((running_ctx_keys
, None))
1477 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1478 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1480 ].startswith("vrf"):
1481 for line
in running_ctx
.lines
:
1482 lines_to_del
.append((running_ctx_keys
, line
))
1484 # If this is an address-family under 'router bgp' and we are already deleting the
1485 # entire 'router bgp' context then ignore this sub-context
1487 "router bgp" in running_ctx_keys
[0]
1488 and len(running_ctx_keys
) > 1
1493 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1495 "router bgp" in running_ctx_keys
[0]
1496 and len(running_ctx_keys
) > 2
1497 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1498 and running_ctx_keys
[2].startswith("vni ")
1500 lines_to_del
.append((running_ctx_keys
, None))
1503 "router bgp" in running_ctx_keys
[0]
1504 and len(running_ctx_keys
) > 1
1505 and running_ctx_keys
[1].startswith("address-family")
1507 # There's no 'no address-family' support and so we have to
1508 # delete each line individually again
1509 for line
in running_ctx
.lines
:
1510 lines_to_del
.append((running_ctx_keys
, line
))
1512 # Some commands can happen at higher counts that make
1513 # doing vtysh -c inefficient (and can time out.) For
1514 # these commands, instead of adding them to lines_to_del,
1515 # add the "no " version to lines_to_add.
1516 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1518 ].startswith("ipv6 route"):
1519 add_cmd
= ("no " + running_ctx_keys
[0],)
1520 lines_to_add
.append((add_cmd
, None))
1522 # if this an interface sub-subcontext in an address-family block in ldpd and
1523 # we are already deleting the whole context, then ignore this
1525 len(running_ctx_keys
) > 2
1526 and running_ctx_keys
[0].startswith("mpls ldp")
1527 and running_ctx_keys
[1].startswith("address-family")
1528 and (running_ctx_keys
[:2], None) in lines_to_del
1532 # same thing for a pseudowire sub-context inside an l2vpn context
1534 len(running_ctx_keys
) > 1
1535 and running_ctx_keys
[0].startswith("l2vpn")
1536 and running_ctx_keys
[1].startswith("member pseudowire")
1537 and (running_ctx_keys
[:1], None) in lines_to_del
1541 # Segment routing and traffic engineering never need to be deleted
1543 running_ctx_keys
[0].startswith("segment-routing")
1544 and len(running_ctx_keys
) < 3
1548 # Neither the pcep command
1550 len(running_ctx_keys
) == 3
1551 and running_ctx_keys
[0].startswith("segment-routing")
1552 and running_ctx_keys
[2].startswith("pcep")
1556 # Segment lists can only be deleted after we removed all the candidate paths that
1557 # use them, so add them to a separate array that is going to be appended at the end
1559 len(running_ctx_keys
) == 3
1560 and running_ctx_keys
[0].startswith("segment-routing")
1561 and running_ctx_keys
[2].startswith("segment-list")
1563 seglist_to_del
.append((running_ctx_keys
, None))
1565 # Policies must be deleted after there candidate path, to be sure
1566 # we add them to a separate array that is going to be appended at the end
1568 len(running_ctx_keys
) == 3
1569 and running_ctx_keys
[0].startswith("segment-routing")
1570 and running_ctx_keys
[2].startswith("policy")
1572 pollist_to_del
.append((running_ctx_keys
, None))
1574 # pce-config must be deleted after the pce, to be sure we add them
1575 # to a separate array that is going to be appended at the end
1577 len(running_ctx_keys
) >= 4
1578 and running_ctx_keys
[0].startswith("segment-routing")
1579 and running_ctx_keys
[3].startswith("pce-config")
1581 pceconf_to_del
.append((running_ctx_keys
, None))
1583 # pcc must be deleted after the pce and pce-config too
1585 len(running_ctx_keys
) >= 4
1586 and running_ctx_keys
[0].startswith("segment-routing")
1587 and running_ctx_keys
[3].startswith("pcc")
1589 pcclist_to_del
.append((running_ctx_keys
, None))
1591 # Non-global context
1592 elif running_ctx_keys
and not any(
1593 "address-family" in key
for key
in running_ctx_keys
1595 lines_to_del
.append((running_ctx_keys
, None))
1597 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1598 lines_to_del
.append((running_ctx_keys
, None))
1602 for line
in running_ctx
.lines
:
1603 lines_to_del
.append((running_ctx_keys
, line
))
1605 # if we have some policies commands to delete, append them to lines_to_del
1606 if len(pollist_to_del
) > 0:
1607 lines_to_del
.extend(pollist_to_del
)
1609 # if we have some segment list commands to delete, append them to lines_to_del
1610 if len(seglist_to_del
) > 0:
1611 lines_to_del
.extend(seglist_to_del
)
1613 # if we have some pce list commands to delete, append them to lines_to_del
1614 if len(pceconf_to_del
) > 0:
1615 lines_to_del
.extend(pceconf_to_del
)
1617 # if we have some pcc list commands to delete, append them to lines_to_del
1618 if len(pcclist_to_del
) > 0:
1619 lines_to_del
.extend(pcclist_to_del
)
1621 # Find the lines within each context to add
1622 # Find the lines within each context to del
1623 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1625 if newconf_ctx_keys
in running
.contexts
:
1626 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1628 for line
in newconf_ctx
.lines
:
1629 if line
not in running_ctx
.dlines
:
1631 # candidate paths can only be added after the policy and segment list,
1632 # so add them to a separate array that is going to be appended at the end
1634 len(newconf_ctx_keys
) == 3
1635 and newconf_ctx_keys
[0].startswith("segment-routing")
1636 and newconf_ctx_keys
[2].startswith("policy ")
1637 and line
.startswith("candidate-path ")
1639 candidates_to_add
.append((newconf_ctx_keys
, line
))
1642 lines_to_add
.append((newconf_ctx_keys
, line
))
1644 for line
in running_ctx
.lines
:
1645 if line
not in newconf_ctx
.dlines
:
1646 lines_to_del
.append((newconf_ctx_keys
, line
))
1648 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1650 if newconf_ctx_keys
not in running
.contexts
:
1652 # candidate paths can only be added after the policy and segment list,
1653 # so add them to a separate array that is going to be appended at the end
1655 len(newconf_ctx_keys
) == 4
1656 and newconf_ctx_keys
[0].startswith("segment-routing")
1657 and newconf_ctx_keys
[3].startswith("candidate-path")
1659 candidates_to_add
.append((newconf_ctx_keys
, None))
1660 for line
in newconf_ctx
.lines
:
1661 candidates_to_add
.append((newconf_ctx_keys
, line
))
1664 lines_to_add
.append((newconf_ctx_keys
, None))
1666 for line
in newconf_ctx
.lines
:
1667 lines_to_add
.append((newconf_ctx_keys
, line
))
1669 # if we have some candidate paths commands to add, append them to lines_to_add
1670 if len(candidates_to_add
) > 0:
1671 lines_to_add
.extend(candidates_to_add
)
1673 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1674 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1675 lines_to_add
, lines_to_del
1677 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1678 lines_to_add
, lines_to_del
1681 return (lines_to_add
, lines_to_del
)
1684 if __name__
== "__main__":
1685 # Command line options
1686 parser
= argparse
.ArgumentParser(
1687 description
="Dynamically apply diff in frr configs"
1689 parser
.add_argument(
1690 "--input", help='Read running config from file instead of "show running"'
1692 group
= parser
.add_mutually_exclusive_group(required
=True)
1694 "--reload", action
="store_true", help="Apply the deltas", default
=False
1697 "--test", action
="store_true", help="Show the deltas", default
=False
1699 level_group
= parser
.add_mutually_exclusive_group()
1700 level_group
.add_argument(
1702 action
="store_true",
1703 help="Enable debugs (synonym for --log-level=debug)",
1706 level_group
.add_argument(
1710 choices
=("critical", "error", "warning", "info", "debug"),
1712 parser
.add_argument(
1713 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1715 parser
.add_argument(
1719 help="Reload specified path/namespace",
1722 parser
.add_argument("filename", help="Location of new frr config file")
1723 parser
.add_argument(
1725 action
="store_true",
1726 help="Overwrite frr.conf with running config output",
1729 parser
.add_argument(
1730 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1732 parser
.add_argument(
1733 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1735 parser
.add_argument(
1736 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1738 parser
.add_argument(
1740 help="socket to be used by vtysh to connect to the daemons",
1743 parser
.add_argument(
1744 "--daemon", help="daemon for which want to replace the config", default
=""
1747 args
= parser
.parse_args()
1750 # For --test log to stdout
1751 # For --reload log to /var/log/frr/frr-reload.log
1752 if args
.test
or args
.stdout
:
1753 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1755 # Color the errors and warnings in red
1756 logging
.addLevelName(
1757 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1759 logging
.addLevelName(
1760 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1764 if not os
.path
.isdir("/var/log/frr/"):
1765 os
.makedirs("/var/log/frr/")
1767 logging
.basicConfig(
1768 filename
="/var/log/frr/frr-reload.log",
1769 format
="%(asctime)s %(levelname)5s: %(message)s",
1772 # argparse should prevent this from happening but just to be safe...
1774 raise Exception("Must specify --reload or --test")
1775 log
= logging
.getLogger(__name__
)
1778 log
.setLevel(logging
.DEBUG
)
1780 log
.setLevel(args
.log_level
.upper())
1782 if args
.reload and not args
.stdout
:
1783 # Additionally send errors and above to STDOUT, with no metadata,
1784 # when we are logging to a file. This specifically does not follow
1785 # args.log_level, and is analagous to behaviour in earlier versions
1786 # which additionally logged most errors using print().
1788 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
1789 stdout_hdlr
.setLevel(logging
.ERROR
)
1790 stdout_hdlr
.setFormatter(logging
.Formatter())
1791 log
.addHandler(stdout_hdlr
)
1793 # Verify the new config file is valid
1794 if not os
.path
.isfile(args
.filename
):
1795 log
.error("Filename %s does not exist" % args
.filename
)
1798 if not os
.path
.getsize(args
.filename
):
1799 log
.error("Filename %s is an empty file" % args
.filename
)
1802 # Verify that confdir is correct
1803 if not os
.path
.isdir(args
.confdir
):
1804 log
.error("Confdir %s is not a valid path" % args
.confdir
)
1807 # Verify that bindir is correct
1808 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
1809 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
1812 # verify that the vty_socket, if specified, is valid
1813 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
1814 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
1817 # verify that the daemon, if specified, is valid
1818 if args
.daemon
and args
.daemon
not in [
1836 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1841 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
1843 # Verify that 'service integrated-vtysh-config' is configured
1845 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
1847 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
1848 service_integrated_vtysh_config
= True
1850 if os
.path
.isfile(vtysh_filename
):
1851 with
open(vtysh_filename
, "r") as fh
:
1852 for line
in fh
.readlines():
1855 if line
== "no service integrated-vtysh-config":
1856 service_integrated_vtysh_config
= False
1859 if not service_integrated_vtysh_config
and not args
.daemon
:
1861 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1865 log
.info('Called via "%s"', str(args
))
1867 # Create a Config object from the config generated by newconf
1868 newconf
= Config(vtysh
)
1870 newconf
.load_from_file(args
.filename
)
1872 except VtyshException
as ve
:
1873 log
.error("vtysh failed to process new configuration: {}".format(ve
))
1878 # Create a Config object from the running config
1879 running
= Config(vtysh
)
1882 running
.load_from_file(args
.input)
1884 running
.load_from_show_running(args
.daemon
)
1886 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1887 lines_to_configure
= []
1890 print("\nLines To Delete")
1891 print("===============")
1893 for (ctx_keys
, line
) in lines_to_del
:
1898 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, True))
1899 lines_to_configure
.append(cmd
)
1903 print("\nLines To Add")
1904 print("============")
1906 for (ctx_keys
, line
) in lines_to_add
:
1911 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False))
1912 lines_to_configure
.append(cmd
)
1917 # We will not be able to do anything, go ahead and exit(1)
1918 if not vtysh
.is_config_available():
1921 log
.debug("New Frr Config\n%s", newconf
.get_lines())
1923 # This looks a little odd but we have to do this twice...here is why
1924 # If the user had this running bgp config:
1927 # neighbor 1.1.1.1 remote-as 50
1928 # neighbor 1.1.1.1 route-map FOO out
1930 # and this config in the newconf config file
1933 # neighbor 1.1.1.1 remote-as 999
1934 # neighbor 1.1.1.1 route-map FOO out
1937 # Then the script will do
1938 # - no neighbor 1.1.1.1 remote-as 50
1939 # - neighbor 1.1.1.1 remote-as 999
1941 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1942 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1943 # configs again to put this line back.
1945 # There are many keywords in FRR that can only appear one time under
1946 # a context, take "bgp router-id" for example. If the config that we are
1947 # reloading against has the following:
1950 # bgp router-id 1.1.1.1
1951 # bgp router-id 2.2.2.2
1953 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1954 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1955 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1956 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1957 # second pass to include all of the "adds" from the first pass.
1958 lines_to_add_first_pass
= []
1961 running
= Config(vtysh
)
1962 running
.load_from_show_running(args
.daemon
)
1963 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
1965 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1968 lines_to_add_first_pass
= lines_to_add
1970 lines_to_add
.extend(lines_to_add_first_pass
)
1972 # Only do deletes on the first pass. The reason being if we
1973 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1974 # will automatically add:
1977 # ipv6 nd ra-interval 10
1978 # no ipv6 nd suppress-ra
1981 # but those lines aren't in the config we are reloading against so
1982 # on the 2nd pass they will show up in lines_to_del. This could
1983 # apply to other scenarios as well where configuring FOO adds BAR
1985 if lines_to_del
and x
== 0:
1986 for (ctx_keys
, line
) in lines_to_del
:
1991 # 'no' commands are tricky, we can't just put them in a file and
1992 # vtysh -f that file. See the next comment for an explanation
1994 cmd
= lines_to_config(ctx_keys
, line
, True)
1997 # Some commands in frr are picky about taking a "no" of the entire line.
1998 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1999 # only the beginning. If we hit one of these command an exception will be
2000 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2003 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2004 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2005 # % Unknown command.
2006 # frr(config-if)# no ip ospf authentication message-digest
2007 # % Unknown command.
2008 # frr(config-if)# no ip ospf authentication
2014 vtysh(["configure"] + cmd
, stdouts
)
2016 except VtyshException
:
2018 # - Pull the last entry from cmd (this would be
2019 # 'no ip ospf authentication message-digest 1.1.1.1' in
2021 # - Split that last entry by whitespace and drop the last word
2022 log
.info("Failed to execute %s", " ".join(cmd
))
2023 last_arg
= cmd
[-1].split(" ")
2025 if len(last_arg
) <= 2:
2027 '"%s" we failed to remove this command',
2028 " -- ".join(original_cmd
),
2030 # Log first error msg for original_cmd
2032 log
.error(stdouts
[0])
2036 new_last_arg
= last_arg
[0:-1]
2037 cmd
[-1] = " ".join(new_last_arg
)
2039 log
.info('Executed "%s"', " ".join(cmd
))
2043 lines_to_configure
= []
2045 for (ctx_keys
, line
) in lines_to_add
:
2050 # Don't run "no" commands twice since they can error
2051 # out the second time due to first deletion
2052 if x
== 1 and ctx_keys
[0].startswith("no "):
2055 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2056 lines_to_configure
.append(cmd
)
2058 if lines_to_configure
:
2059 random_string
= "".join(
2060 random
.SystemRandom().choice(
2061 string
.ascii_uppercase
+ string
.digits
2066 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2067 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2069 with
open(filename
, "w") as fh
:
2070 for line
in lines_to_configure
:
2071 fh
.write(line
+ "\n")
2074 vtysh
.exec_file(filename
)
2075 except VtyshException
as e
:
2076 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2080 # Make these changes persistent
2081 target
= str(args
.confdir
+ "/frr.conf")
2082 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):