]> git.proxmox.com Git - mirror_frr.git/blob - tools/frr-reload.py
Merge pull request #8405 from mjstapp/skip_backup_lsps
[mirror_frr.git] / tools / frr-reload.py
1 #!/usr/bin/python
2 # Frr Reloader
3 # Copyright (C) 2014 Cumulus Networks, Inc.
4 #
5 # This file is part of Frr.
6 #
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
10 # later version.
11 #
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.
16 #
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
20 # 02111-1307, USA.
21 #
22 """
23 This program
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
28 text file
29 """
30
31 from __future__ import print_function, unicode_literals
32 import argparse
33 import copy
34 import logging
35 import os, os.path
36 import random
37 import re
38 import string
39 import subprocess
40 import sys
41 from collections import OrderedDict
42
43 try:
44 from ipaddress import IPv6Address, ip_network
45 except ImportError:
46 from ipaddr import IPv6Address, IPNetwork
47 from pprint import pformat
48
49 try:
50 dict.iteritems
51 except AttributeError:
52 # Python 3
53 def iteritems(d):
54 return iter(d.items())
55
56
57 else:
58 # Python 2
59 def iteritems(d):
60 return d.iteritems()
61
62
63 log = logging.getLogger(__name__)
64
65
66 class VtyshException(Exception):
67 pass
68
69
70 class Vtysh(object):
71 def __init__(self, bindir=None, confdir=None, sockdir=None, pathspace=None):
72 self.bindir = bindir
73 self.confdir = confdir
74 self.pathspace = pathspace
75 self.common_args = [os.path.join(bindir or "", "vtysh")]
76 if confdir:
77 self.common_args.extend(["--config_dir", confdir])
78 if sockdir:
79 self.common_args.extend(["--vty_socket", sockdir])
80 if pathspace:
81 self.common_args.extend(["-N", pathspace])
82
83 def _call(self, args, stdin=None, stdout=None, stderr=None):
84 kwargs = {}
85 if stdin is not 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)
92
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]]
96 else:
97 args = ["-c", command]
98 return self._call(args, stdin, stdout, stderr)
99
100 def __call__(self, command, stdouts=None):
101 """
102 Call a CLI command (e.g. "show running-config")
103
104 Output text is automatically redirected, decoded and returned.
105 Multiple commands may be passed as list.
106 """
107 proc = self._call_cmd(command, stdout=subprocess.PIPE)
108 stdout, stderr = proc.communicate()
109 if proc.wait() != 0:
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)
114 )
115 return stdout.decode("UTF-8")
116
117 def is_config_available(self):
118 """
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.
122 """
123
124 output = self("configure")
125
126 if "VTY configuration is locked by other VTY" in output:
127 log.error("vtysh 'configure' returned\n%s\n" % (output))
128 return False
129
130 return True
131
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)
137 )
138
139 def mark_file(self, filename, stdin=None):
140 child = self._call(
141 ["-m", "-f", filename],
142 stdout=subprocess.PIPE,
143 stdin=subprocess.PIPE,
144 stderr=subprocess.PIPE,
145 )
146 try:
147 stdout, stderr = child.communicate()
148 except subprocess.TimeoutExpired:
149 child.kill()
150 stdout, stderr = child.communicate()
151 raise VtyshException("vtysh call timed out!")
152
153 if child.wait() != 0:
154 raise VtyshException(
155 "vtysh (mark file) exited with status %d:\n%s"
156 % (child.returncode, stderr)
157 )
158
159 return stdout.decode("UTF-8")
160
161 def mark_show_run(self, daemon=None):
162 cmd = "show running-config"
163 if daemon:
164 cmd += " %s" % daemon
165 cmd += " no-header"
166 show_run = self._call_cmd(cmd, stdout=subprocess.PIPE)
167 mark = self._call(
168 ["-m", "-f", "-"], stdin=show_run.stdout, stdout=subprocess.PIPE
169 )
170
171 show_run.wait()
172 stdout, stderr = mark.communicate()
173 mark.wait()
174
175 if show_run.returncode != 0:
176 raise VtyshException(
177 "vtysh (show running-config) exited with status %d:"
178 % (show_run.returncode)
179 )
180 if mark.returncode != 0:
181 raise VtyshException(
182 "vtysh (mark running-config) exited with status %d" % (mark.returncode)
183 )
184
185 return stdout.decode("UTF-8")
186
187
188 class Context(object):
189
190 """
191 A Context object represents a section of frr configuration such as:
192 !
193 interface swp3
194 description swp3 -> r8's swp1
195 ipv6 nd suppress-ra
196 link-detect
197 !
198
199 or a single line context object such as this:
200
201 ip forwarding
202
203 """
204
205 def __init__(self, keys, lines):
206 self.keys = keys
207 self.lines = lines
208
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()
212
213 for ligne in lines:
214 self.dlines[ligne] = True
215
216 def add_lines(self, lines):
217 """
218 Add lines to specified context
219 """
220
221 self.lines.extend(lines)
222
223 for ligne in lines:
224 self.dlines[ligne] = True
225
226
227 def get_normalized_es_id(line):
228 """
229 The es-id or es-sys-mac need to be converted to lower case
230 """
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)
234 if obj:
235 line = "%s %s" % (sub_str, obj.group("esi").lower())
236 break
237 return line
238
239
240 def get_normalized_mac_ip_line(line):
241 if line.startswith("evpn mh es"):
242 return get_normalized_es_id(line)
243
244 if not "ipv6 add" in line:
245 return get_normalized_ipv6_line(line)
246
247 return line
248
249
250 class Config(object):
251
252 """
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.
256 """
257
258 def __init__(self, vtysh):
259 self.lines = []
260 self.contexts = OrderedDict()
261 self.vtysh = vtysh
262
263 def load_from_file(self, filename):
264 """
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
268 """
269 log.info("Loading Config object from file %s", filename)
270
271 file_output = self.vtysh.mark_file(filename)
272
273 for line in file_output.split("\n"):
274 line = line.strip()
275
276 # Compress duplicate whitespaces
277 line = " ".join(line.split())
278
279 if ":" in line:
280 line = get_normalized_mac_ip_line(line)
281
282 """
283 vrf static routes can be added in two ways. The old way is:
284
285 "ip route x.x.x.x/x y.y.y.y vrf <vrfname>"
286
287 but it's rendered in the configuration as the new way::
288
289 vrf <vrf-name>
290 ip route x.x.x.x/x y.y.y.y
291 exit-vrf
292
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.
297 """
298 if (
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")
309 line = "end"
310
311 self.lines.append(line)
312
313 self.load_contexts()
314
315 def load_from_show_running(self, daemon):
316 """
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
320 """
321 log.info("Loading Config object from vtysh show running")
322
323 config_text = self.vtysh.mark_show_run(daemon)
324
325 for line in config_text.split("\n"):
326 line = line.strip()
327
328 if (
329 line == "Building configuration..."
330 or line == "Current configuration:"
331 or not line
332 ):
333 continue
334
335 self.lines.append(line)
336
337 self.load_contexts()
338
339 def get_lines(self):
340 """
341 Return the lines read in from the configuration
342 """
343
344 return "\n".join(self.lines)
345
346 def get_contexts(self):
347 """
348 Return the parsed context as strings for display, log etc.
349 """
350
351 for (_, ctx) in sorted(iteritems(self.contexts)):
352 print(str(ctx) + "\n")
353
354 def save_contexts(self, key, lines):
355 """
356 Save the provided key and lines as a context
357 """
358
359 if not key:
360 return
361
362 """
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.
369 """
370 re_key_rt = re.match(r"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key[0])
371 if re_key_rt:
372 addr = re_key_rt.group(2)
373 if "/" in addr:
374 try:
375 if "ipaddress" not in sys.modules:
376 newaddr = IPNetwork(addr)
377 key[0] = "%s route %s/%s%s" % (
378 re_key_rt.group(1),
379 newaddr.network,
380 newaddr.prefixlen,
381 re_key_rt.group(3),
382 )
383 else:
384 newaddr = ip_network(addr, strict=False)
385 key[0] = "%s route %s/%s%s" % (
386 re_key_rt.group(1),
387 str(newaddr.network_address),
388 newaddr.prefixlen,
389 re_key_rt.group(3),
390 )
391 except ValueError:
392 pass
393
394 re_key_rt = re.match(
395 r"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key[0]
396 )
397 if re_key_rt:
398 addr = re_key_rt.group(4)
399 if "/" in addr:
400 try:
401 if "ipaddress" not in sys.modules:
402 newaddr = "%s/%s" % (
403 IPNetwork(addr).network,
404 IPNetwork(addr).prefixlen,
405 )
406 else:
407 network_addr = ip_network(addr, strict=False)
408 newaddr = "%s/%s" % (
409 str(network_addr.network_address),
410 network_addr.prefixlen,
411 )
412 except ValueError:
413 newaddr = addr
414 else:
415 newaddr = addr
416
417 legestr = re_key_rt.group(5)
418 re_lege = re.search(r"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr)
419 if re_lege:
420 legestr = "%sge %s le %s%s" % (
421 re_lege.group(1),
422 re_lege.group(3),
423 re_lege.group(2),
424 re_lege.group(4),
425 )
426 re_lege = re.search(r"(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)", legestr)
427
428 if re_lege and (
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")
431 ):
432 legestr = "%sge %s%s" % (
433 re_lege.group(1),
434 re_lege.group(2),
435 re_lege.group(4),
436 )
437
438 key[0] = "%s prefix-list%s%s %s%s" % (
439 re_key_rt.group(1),
440 re_key_rt.group(2),
441 re_key_rt.group(3),
442 newaddr,
443 legestr,
444 )
445
446 if lines and key[0].startswith("router bgp"):
447 newlines = []
448 for line in lines:
449 re_net = re.match(r"network\s+([A-Fa-f:.0-9/]+)(.*)$", line)
450 if re_net:
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
455 addr = addr + "/8"
456
457 try:
458 if "ipaddress" not in sys.modules:
459 newaddr = IPNetwork(addr)
460 line = "network %s/%s %s" % (
461 newaddr.network,
462 newaddr.prefixlen,
463 re_net.group(2),
464 )
465 else:
466 network_addr = ip_network(addr, strict=False)
467 line = "network %s/%s %s" % (
468 str(network_addr.network_address),
469 network_addr.prefixlen,
470 re_net.group(2),
471 )
472 newlines.append(line)
473 except ValueError:
474 # Really this should be an error. Whats a network
475 # without an IP Address following it ?
476 newlines.append(line)
477 else:
478 newlines.append(line)
479 lines = newlines
480
481 """
482 More fixups in user specification and what running config shows.
483 "null0" in routes must be replaced by Null0.
484 """
485 if (
486 key[0].startswith("ip route")
487 or key[0].startswith("ipv6 route")
488 and "null0" in key[0]
489 ):
490 key[0] = re.sub(r"\s+null0(\s*$)", " Null0", key[0])
491
492 """
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
495 """
496 if lines and key[0].startswith("vrf "):
497 newlines = []
498 for line in lines:
499 if line.startswith("ip route ") or line.startswith("ipv6 route "):
500 if "null0" in line:
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)
505 else:
506 newlines.append(line)
507 lines = newlines
508
509 if lines:
510 if tuple(key) not in self.contexts:
511 ctx = Context(tuple(key), lines)
512 self.contexts[tuple(key)] = ctx
513 else:
514 ctx = self.contexts[tuple(key)]
515 ctx.add_lines(lines)
516
517 else:
518 if tuple(key) not in self.contexts:
519 ctx = Context(tuple(key), [])
520 self.contexts[tuple(key)] = ctx
521
522 def load_contexts(self):
523 """
524 Parse the configuration and create contexts for each appropriate block
525 """
526
527 current_context_lines = []
528 ctx_keys = []
529
530 """
531 The end of a context is flagged via the 'end' keyword:
532
533 !
534 interface swp52
535 ipv6 nd suppress-ra
536 link-detect
537 !
538 end
539 router bgp 10
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
548 !
549 end
550 address-family ipv6
551 neighbor IBGPv6 activate
552 neighbor 2001:10::2 peer-group IBGPv6
553 neighbor 2001:10::3 peer-group IBGPv6
554 exit-address-family
555 !
556 end
557 address-family evpn
558 neighbor LEAF activate
559 advertise-all-vni
560 vni 10100
561 rd 65000:10100
562 route-target import 10.1.1.1:10100
563 route-target export 10.1.1.1:10100
564 exit-vni
565 exit-address-family
566 !
567 end
568 router ospf
569 ospf router-id 10.0.0.1
570 log-adjacency-changes detail
571 timers throttle spf 0 50 5000
572 !
573 end
574 """
575
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
579 # config.
580 #
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.
588 ctx_keys = []
589 main_ctx_key = []
590 new_ctx = True
591
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 = (
595 "access-list ",
596 "agentx",
597 "allow-external-route-update",
598 "bgp ",
599 "debug ",
600 "domainname ",
601 "dump ",
602 "enable ",
603 "frr ",
604 "fpm ",
605 "hostname ",
606 "ip ",
607 "ipv6 ",
608 "log ",
609 "mpls lsp",
610 "mpls label",
611 "no ",
612 "password ",
613 "ptm-enable",
614 "router-id ",
615 "service ",
616 "table ",
617 "username ",
618 "zebra ",
619 "vrrp autoconfigure",
620 "evpn mh",
621 )
622
623 for line in self.lines:
624
625 if not line:
626 continue
627
628 if line.startswith("!") or line.startswith("#"):
629 continue
630
631 if (
632 len(ctx_keys) == 2
633 and ctx_keys[0].startswith("bfd")
634 and ctx_keys[1].startswith("profile ")
635 and line == "end"
636 ):
637 log.debug("LINE %-50s: popping from sub context, %-50s", line, ctx_keys)
638
639 if main_ctx_key:
640 self.save_contexts(ctx_keys, current_context_lines)
641 ctx_keys = copy.deepcopy(main_ctx_key)
642 current_context_lines = []
643 continue
644
645 # one line contexts
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
649 # to a new context
650 if (
651 new_ctx is True
652 and any(line.startswith(keyword) for keyword in oneline_ctx_keywords)
653 and not (
654 ctx_keys
655 and ctx_keys[0].startswith("mpls ldp")
656 and line.startswith("router-id ")
657 )
658 ):
659 self.save_contexts(ctx_keys, current_context_lines)
660
661 # Start a new context
662 main_ctx_key = []
663 ctx_keys = [
664 line,
665 ]
666 current_context_lines = []
667
668 log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys)
669 self.save_contexts(ctx_keys, current_context_lines)
670 new_ctx = True
671
672 elif line == "end":
673 self.save_contexts(ctx_keys, current_context_lines)
674 log.debug("LINE %-50s: exiting old context, %-50s", line, ctx_keys)
675
676 # Start a new context
677 new_ctx = True
678 main_ctx_key = []
679 ctx_keys = []
680 current_context_lines = []
681
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)
685
686 # Start a new context
687 new_ctx = True
688 main_ctx_key = []
689 ctx_keys = []
690 current_context_lines = []
691
692 elif line == "exit-vrf":
693 self.save_contexts(ctx_keys, current_context_lines)
694 current_context_lines.append(line)
695 log.debug(
696 "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys
697 )
698
699 # Start a new context
700 new_ctx = True
701 main_ctx_key = []
702 ctx_keys = []
703 current_context_lines = []
704
705 elif (
706 line == "exit"
707 and len(ctx_keys) > 1
708 and ctx_keys[0].startswith("segment-routing")
709 ):
710 self.save_contexts(ctx_keys, current_context_lines)
711
712 # Start a new context
713 ctx_keys = ctx_keys[:-1]
714 current_context_lines = []
715 log.debug(
716 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
717 line,
718 ctx_keys,
719 )
720
721 elif line in ["exit-address-family", "exit", "exit-vnc"]:
722 # if this exit is for address-family ipv4 unicast, ignore the pop
723 if main_ctx_key:
724 self.save_contexts(ctx_keys, current_context_lines)
725
726 # Start a new context
727 ctx_keys = copy.deepcopy(main_ctx_key)
728 current_context_lines = []
729 log.debug(
730 "LINE %-50s: popping from subcontext to ctx%-50s",
731 line,
732 ctx_keys,
733 )
734
735 elif line in ["exit-vni", "exit-ldp-if"]:
736 if sub_main_ctx_key:
737 self.save_contexts(ctx_keys, current_context_lines)
738
739 # Start a new context
740 ctx_keys = copy.deepcopy(sub_main_ctx_key)
741 current_context_lines = []
742 log.debug(
743 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
744 line,
745 ctx_keys,
746 )
747
748 elif new_ctx is True:
749 if not main_ctx_key:
750 ctx_keys = [
751 line,
752 ]
753 else:
754 ctx_keys = copy.deepcopy(main_ctx_key)
755 main_ctx_key = []
756
757 current_context_lines = []
758 new_ctx = False
759 log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys)
760
761 elif (
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")
769 ):
770 main_ctx_key = []
771
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)
777
778 if line == "address-family ipv6" and not ctx_keys[0].startswith(
779 "mpls ldp"
780 ):
781 ctx_keys.append("address-family ipv6 unicast")
782 elif line == "address-family ipv4" and not ctx_keys[0].startswith(
783 "mpls ldp"
784 ):
785 ctx_keys.append("address-family ipv4 unicast")
786 elif line == "address-family evpn":
787 ctx_keys.append("address-family l2vpn evpn")
788 else:
789 ctx_keys.append(line)
790
791 elif (
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"
796 ):
797
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)
802 log.debug(
803 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
804 )
805 ctx_keys.append(line)
806
807 elif (
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")
812 ):
813
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)
818 log.debug(
819 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
820 )
821 ctx_keys.append(line)
822
823 elif (
824 line.startswith("traffic-eng")
825 and len(ctx_keys) == 1
826 and ctx_keys[0].startswith("segment-routing")
827 ):
828
829 # Save old context first
830 self.save_contexts(ctx_keys, current_context_lines)
831 current_context_lines = []
832 log.debug(
833 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
834 line,
835 )
836 ctx_keys.append(line)
837
838 elif (
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")
843 ):
844
845 # Save old context first
846 self.save_contexts(ctx_keys, current_context_lines)
847 current_context_lines = []
848 log.debug(
849 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
850 line,
851 )
852 ctx_keys.append(line)
853
854 elif (
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")
859 ):
860
861 # Save old context first
862 self.save_contexts(ctx_keys, current_context_lines)
863 current_context_lines = []
864 log.debug(
865 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
866 line,
867 )
868 ctx_keys.append(line)
869
870 elif (
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")
877 ):
878
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)
883 log.debug(
884 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
885 line,
886 )
887 ctx_keys.append(line)
888
889 elif (
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")
894 ):
895
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)
900 log.debug(
901 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
902 )
903 ctx_keys.append(line)
904
905 elif (
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")
911 ):
912
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)
917 log.debug(
918 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
919 line,
920 )
921 ctx_keys.append(line)
922
923 elif (
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")
929 ):
930
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)
935 log.debug(
936 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
937 )
938 ctx_keys.append(line)
939
940 elif (
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")
946 ):
947
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)
952 log.debug(
953 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
954 )
955 ctx_keys.append(line)
956
957 elif (
958 line.startswith("profile ")
959 and len(ctx_keys) == 1
960 and ctx_keys[0].startswith("bfd")
961 ):
962
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)
967 log.debug(
968 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
969 line,
970 )
971 ctx_keys.append(line)
972
973 else:
974 # Continuing in an existing context, add non-commented lines to it
975 current_context_lines.append(line)
976 log.debug(
977 "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys
978 )
979
980 # Save the context of the last one
981 self.save_contexts(ctx_keys, current_context_lines)
982
983
984 def lines_to_config(ctx_keys, line, delete):
985 """
986 Return the command as it would appear in frr.conf
987 """
988 cmd = []
989
990 if line:
991 for (i, ctx_key) in enumerate(ctx_keys):
992 cmd.append(" " * i + ctx_key)
993
994 line = line.lstrip()
995 indent = len(ctx_keys) * " "
996
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"
1001 if delete:
1002 if line.startswith("no "):
1003 cmd.append("%s%s" % (indent, line[3:]))
1004 else:
1005 cmd.append("%sno %s" % (indent, line))
1006
1007 else:
1008 cmd.append(indent + line)
1009
1010 # If line is None then we are typically deleting an entire
1011 # context ('no router ospf' for example)
1012 else:
1013 for i, ctx_key in enumerate(ctx_keys[:-1]):
1014 cmd.append("%s%s" % (" " * i, ctx_key))
1015
1016 # Only put the 'no' on the last sub-context
1017 if delete:
1018 if ctx_keys[-1].startswith("no "):
1019 cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1][3:]))
1020 else:
1021 cmd.append("%sno %s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1]))
1022 else:
1023 cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1]))
1024
1025 return cmd
1026
1027
1028 def get_normalized_ipv6_line(line):
1029 """
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
1034 """
1035 norm_line = ""
1036 words = line.split(" ")
1037 for word in words:
1038 if ":" in word:
1039 norm_word = None
1040 if "/" in word:
1041 try:
1042 if "ipaddress" not in sys.modules:
1043 v6word = IPNetwork(word)
1044 norm_word = "%s/%s" % (v6word.network, v6word.prefixlen)
1045 else:
1046 v6word = ip_network(word, strict=False)
1047 norm_word = "%s/%s" % (
1048 str(v6word.network_address),
1049 v6word.prefixlen,
1050 )
1051 except ValueError:
1052 pass
1053 if not norm_word:
1054 try:
1055 norm_word = "%s" % IPv6Address(word)
1056 except ValueError:
1057 norm_word = word
1058 else:
1059 norm_word = word
1060 norm_line = norm_line + " " + norm_word
1061
1062 return norm_line.strip()
1063
1064
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:
1068 if exact_match:
1069 if line == target_line:
1070 return True
1071 else:
1072 if line.startswith(target_line):
1073 return True
1074 return False
1075
1076
1077 def check_for_exit_vrf(lines_to_add, lines_to_del):
1078
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
1084 index = 0
1085
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
1092
1093 if ctx_keys[0].startswith("vrf") and line:
1094 if line is not "exit-vrf":
1095 add_exit_vrf = True
1096 prior_ctx_key = ctx_keys[0]
1097 else:
1098 add_exit_vrf = False
1099 index += 1
1100
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))
1105
1106 return (lines_to_add, lines_to_del)
1107
1108
1109 def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
1110
1111 # Quite possibly the most confusing (while accurate) variable names in history
1112 lines_to_add_to_del = []
1113 lines_to_del_to_del = []
1114
1115 for (ctx_keys, line) in lines_to_del:
1116 deleted = False
1117
1118 # If there is a change in the segment routing block ranges, do it
1119 # in-place, to avoid requesting spurious label chunks which might fail
1120 if line and "segment-routing global-block" in line:
1121 for (add_key, add_line) in lines_to_add:
1122 if ctx_keys[0] == add_key[0] and add_line and "segment-routing global-block" in add_line:
1123 lines_to_del_to_del.append((ctx_keys, line))
1124 break
1125 continue
1126
1127
1128 if ctx_keys[0].startswith("router bgp") and line:
1129
1130 if line.startswith("neighbor "):
1131 """
1132 BGP changed how it displays swpX peers that are part of peer-group. Older
1133 versions of frr would display these on separate lines:
1134 neighbor swp1 interface
1135 neighbor swp1 peer-group FOO
1136
1137 but today we display via a single line
1138 neighbor swp1 interface peer-group FOO
1139
1140 This change confuses frr-reload.py so check to see if we are deleting
1141 neighbor swp1 interface peer-group FOO
1142
1143 and adding
1144 neighbor swp1 interface
1145 neighbor swp1 peer-group FOO
1146
1147 If so then chop the del line and the corresponding add lines
1148 """
1149
1150 re_swpx_int_peergroup = re.search(
1151 "neighbor (\S+) interface peer-group (\S+)", line
1152 )
1153 re_swpx_int_v6only_peergroup = re.search(
1154 "neighbor (\S+) interface v6only peer-group (\S+)", line
1155 )
1156
1157 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
1158 swpx_interface = None
1159 swpx_peergroup = None
1160
1161 if re_swpx_int_peergroup:
1162 swpx = re_swpx_int_peergroup.group(1)
1163 peergroup = re_swpx_int_peergroup.group(2)
1164 swpx_interface = "neighbor %s interface" % swpx
1165 elif re_swpx_int_v6only_peergroup:
1166 swpx = re_swpx_int_v6only_peergroup.group(1)
1167 peergroup = re_swpx_int_v6only_peergroup.group(2)
1168 swpx_interface = "neighbor %s interface v6only" % swpx
1169
1170 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
1171 found_add_swpx_interface = line_exist(
1172 lines_to_add, ctx_keys, swpx_interface
1173 )
1174 found_add_swpx_peergroup = line_exist(
1175 lines_to_add, ctx_keys, swpx_peergroup
1176 )
1177 tmp_ctx_keys = tuple(list(ctx_keys))
1178
1179 if not found_add_swpx_peergroup:
1180 tmp_ctx_keys = list(ctx_keys)
1181 tmp_ctx_keys.append("address-family ipv4 unicast")
1182 tmp_ctx_keys = tuple(tmp_ctx_keys)
1183 found_add_swpx_peergroup = line_exist(
1184 lines_to_add, tmp_ctx_keys, swpx_peergroup
1185 )
1186
1187 if not found_add_swpx_peergroup:
1188 tmp_ctx_keys = list(ctx_keys)
1189 tmp_ctx_keys.append("address-family ipv6 unicast")
1190 tmp_ctx_keys = tuple(tmp_ctx_keys)
1191 found_add_swpx_peergroup = line_exist(
1192 lines_to_add, tmp_ctx_keys, swpx_peergroup
1193 )
1194
1195 if found_add_swpx_interface and found_add_swpx_peergroup:
1196 deleted = True
1197 lines_to_del_to_del.append((ctx_keys, line))
1198 lines_to_add_to_del.append((ctx_keys, swpx_interface))
1199 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
1200
1201 """
1202 Changing the bfd timers on neighbors is allowed without doing
1203 a delete/add process. Since doing a "no neighbor blah bfd ..."
1204 will cause the peer to bounce unnecessarily, just skip the delete
1205 and just do the add.
1206 """
1207 re_nbr_bfd_timers = re.search(
1208 r"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1209 )
1210
1211 if re_nbr_bfd_timers:
1212 nbr = re_nbr_bfd_timers.group(1)
1213 bfd_nbr = "neighbor %s" % nbr
1214 bfd_search_string = bfd_nbr + r" bfd (\S+) (\S+) (\S+)"
1215
1216 for (ctx_keys, add_line) in lines_to_add:
1217 if ctx_keys[0].startswith("router bgp"):
1218 re_add_nbr_bfd_timers = re.search(
1219 bfd_search_string, add_line
1220 )
1221
1222 if re_add_nbr_bfd_timers:
1223 found_add_bfd_nbr = line_exist(
1224 lines_to_add, ctx_keys, bfd_nbr, False
1225 )
1226
1227 if found_add_bfd_nbr:
1228 lines_to_del_to_del.append((ctx_keys, line))
1229
1230 """
1231 We changed how we display the neighbor interface command. Older
1232 versions of frr would display the following:
1233 neighbor swp1 interface
1234 neighbor swp1 remote-as external
1235 neighbor swp1 capability extended-nexthop
1236
1237 but today we display via a single line
1238 neighbor swp1 interface remote-as external
1239
1240 and capability extended-nexthop is no longer needed because we
1241 automatically enable it when the neighbor is of type interface.
1242
1243 This change confuses frr-reload.py so check to see if we are deleting
1244 neighbor swp1 interface remote-as (external|internal|ASNUM)
1245
1246 and adding
1247 neighbor swp1 interface
1248 neighbor swp1 remote-as (external|internal|ASNUM)
1249 neighbor swp1 capability extended-nexthop
1250
1251 If so then chop the del line and the corresponding add lines
1252 """
1253 re_swpx_int_remoteas = re.search(
1254 "neighbor (\S+) interface remote-as (\S+)", line
1255 )
1256 re_swpx_int_v6only_remoteas = re.search(
1257 "neighbor (\S+) interface v6only remote-as (\S+)", line
1258 )
1259
1260 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
1261 swpx_interface = None
1262 swpx_remoteas = None
1263
1264 if re_swpx_int_remoteas:
1265 swpx = re_swpx_int_remoteas.group(1)
1266 remoteas = re_swpx_int_remoteas.group(2)
1267 swpx_interface = "neighbor %s interface" % swpx
1268 elif re_swpx_int_v6only_remoteas:
1269 swpx = re_swpx_int_v6only_remoteas.group(1)
1270 remoteas = re_swpx_int_v6only_remoteas.group(2)
1271 swpx_interface = "neighbor %s interface v6only" % swpx
1272
1273 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
1274 found_add_swpx_interface = line_exist(
1275 lines_to_add, ctx_keys, swpx_interface
1276 )
1277 found_add_swpx_remoteas = line_exist(
1278 lines_to_add, ctx_keys, swpx_remoteas
1279 )
1280 tmp_ctx_keys = tuple(list(ctx_keys))
1281
1282 if found_add_swpx_interface and found_add_swpx_remoteas:
1283 deleted = True
1284 lines_to_del_to_del.append((ctx_keys, line))
1285 lines_to_add_to_del.append((ctx_keys, swpx_interface))
1286 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
1287
1288 """
1289 We made the 'bgp bestpath as-path multipath-relax' command
1290 automatically assume 'no-as-set' since the lack of this option caused
1291 weird routing problems. When the running config is shown in
1292 releases with this change, the no-as-set keyword is not shown as it
1293 is the default. This causes frr-reload to unnecessarily unapply
1294 this option only to apply it back again, causing unnecessary session
1295 resets.
1296 """
1297 if "multipath-relax" in line:
1298 re_asrelax_new = re.search(
1299 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1300 )
1301 old_asrelax_cmd = "bgp bestpath as-path multipath-relax no-as-set"
1302 found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
1303
1304 if re_asrelax_new and found_asrelax_old:
1305 deleted = True
1306 lines_to_del_to_del.append((ctx_keys, line))
1307 lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
1308
1309 """
1310 If we are modifying the BGP table-map we need to avoid a del/add and
1311 instead modify the table-map in place via an add. This is needed to
1312 avoid installing all routes in the RIB the second the 'no table-map'
1313 is issued.
1314 """
1315 if line.startswith("table-map"):
1316 found_table_map = line_exist(lines_to_add, ctx_keys, "table-map", False)
1317
1318 if found_table_map:
1319 lines_to_del_to_del.append((ctx_keys, line))
1320
1321 """
1322 More old-to-new config handling. ip import-table no longer accepts
1323 distance, but we honor the old syntax. But 'show running' shows only
1324 the new syntax. This causes an unnecessary 'no import-table' followed
1325 by the same old 'ip import-table' which causes perturbations in
1326 announced routes leading to traffic blackholes. Fix this issue.
1327 """
1328 re_importtbl = re.search("^ip\s+import-table\s+(\d+)$", ctx_keys[0])
1329 if re_importtbl:
1330 table_num = re_importtbl.group(1)
1331 for ctx in lines_to_add:
1332 if ctx[0][0].startswith("ip import-table %s distance" % table_num):
1333 lines_to_del_to_del.append(
1334 (("ip import-table %s" % table_num,), None)
1335 )
1336 lines_to_add_to_del.append((ctx[0], None))
1337
1338 """
1339 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1340 However, the running config always adds 'seq x', where x is a number
1341 incremented by 5 for every element of the prefix/access list.
1342 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1343 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1344 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1345 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1346 access-list FOO seq 5 permit 2.2.2.2/32
1347 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1348 """
1349 re_acl_pfxlst = re.search(
1350 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1351 ctx_keys[0],
1352 )
1353 if re_acl_pfxlst:
1354 found = False
1355 tmpline = (
1356 re_acl_pfxlst.group(1)
1357 + re_acl_pfxlst.group(2)
1358 + re_acl_pfxlst.group(3)
1359 + re_acl_pfxlst.group(5)
1360 + re_acl_pfxlst.group(6)
1361 )
1362 for ctx in lines_to_add:
1363 if ctx[0][0] == tmpline:
1364 lines_to_del_to_del.append((ctx_keys, None))
1365 lines_to_add_to_del.append(((tmpline,), None))
1366 found = True
1367 """
1368 If prefix-lists or access-lists are being deleted and
1369 not added (see comment above), add command with 'no' to
1370 lines_to_add and remove from lines_to_del to improve
1371 scaling performance.
1372 """
1373 if found is False:
1374 add_cmd = ("no " + ctx_keys[0],)
1375 lines_to_add.append((add_cmd, None))
1376 lines_to_del_to_del.append((ctx_keys, None))
1377
1378 if (
1379 len(ctx_keys) == 3
1380 and ctx_keys[0].startswith("router bgp")
1381 and ctx_keys[1] == "address-family l2vpn evpn"
1382 and ctx_keys[2].startswith("vni")
1383 ):
1384
1385 re_route_target = (
1386 re.search("^route-target import (.*)$", line)
1387 if line is not None
1388 else False
1389 )
1390
1391 if re_route_target:
1392 rt = re_route_target.group(1).strip()
1393 route_target_import_line = line
1394 route_target_export_line = "route-target export %s" % rt
1395 route_target_both_line = "route-target both %s" % rt
1396
1397 found_route_target_export_line = line_exist(
1398 lines_to_del, ctx_keys, route_target_export_line
1399 )
1400 found_route_target_both_line = line_exist(
1401 lines_to_add, ctx_keys, route_target_both_line
1402 )
1403
1404 """
1405 If the running configs has
1406 route-target import 1:1
1407 route-target export 1:1
1408
1409 and the config we are reloading against has
1410 route-target both 1:1
1411
1412 then we can ignore deleting the import/export and ignore adding the 'both'
1413 """
1414 if found_route_target_export_line and found_route_target_both_line:
1415 lines_to_del_to_del.append((ctx_keys, route_target_import_line))
1416 lines_to_del_to_del.append((ctx_keys, route_target_export_line))
1417 lines_to_add_to_del.append((ctx_keys, route_target_both_line))
1418
1419 # Deleting static routes under a vrf can lead to time-outs if each is sent
1420 # as separate vtysh -c commands. Change them from being in lines_to_del and
1421 # put the "no" form in lines_to_add
1422 if ctx_keys[0].startswith("vrf ") and line:
1423 if line.startswith("ip route") or line.startswith("ipv6 route"):
1424 add_cmd = "no " + line
1425 lines_to_add.append((ctx_keys, add_cmd))
1426 lines_to_del_to_del.append((ctx_keys, line))
1427
1428 if not deleted:
1429 found_add_line = line_exist(lines_to_add, ctx_keys, line)
1430
1431 if found_add_line:
1432 lines_to_del_to_del.append((ctx_keys, line))
1433 lines_to_add_to_del.append((ctx_keys, line))
1434 else:
1435 """
1436 We have commands that used to be displayed in the global part
1437 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1438
1439 # old way
1440 router bgp 64900
1441 neighbor ISL advertisement-interval 0
1442
1443 vs.
1444
1445 # new way
1446 router bgp 64900
1447 address-family ipv4 unicast
1448 neighbor ISL advertisement-interval 0
1449
1450 Look to see if we are deleting it in one format just to add it back in the other
1451 """
1452 if (
1453 ctx_keys[0].startswith("router bgp")
1454 and len(ctx_keys) > 1
1455 and ctx_keys[1] == "address-family ipv4 unicast"
1456 ):
1457 tmp_ctx_keys = list(ctx_keys)[:-1]
1458 tmp_ctx_keys = tuple(tmp_ctx_keys)
1459
1460 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
1461
1462 if found_add_line:
1463 lines_to_del_to_del.append((ctx_keys, line))
1464 lines_to_add_to_del.append((tmp_ctx_keys, line))
1465
1466 for (ctx_keys, line) in lines_to_del_to_del:
1467 lines_to_del.remove((ctx_keys, line))
1468
1469 for (ctx_keys, line) in lines_to_add_to_del:
1470 lines_to_add.remove((ctx_keys, line))
1471
1472 return (lines_to_add, lines_to_del)
1473
1474
1475 def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
1476 """
1477 There are certain commands that cannot be removed. Remove
1478 those commands from lines_to_del.
1479 """
1480 lines_to_del_to_del = []
1481
1482 for (ctx_keys, line) in lines_to_del:
1483
1484 if (
1485 ctx_keys[0].startswith("frr version")
1486 or ctx_keys[0].startswith("frr defaults")
1487 or ctx_keys[0].startswith("username")
1488 or ctx_keys[0].startswith("password")
1489 or ctx_keys[0].startswith("line vty")
1490 or
1491 # This is technically "no"able but if we did so frr-reload would
1492 # stop working so do not let the user shoot themselves in the foot
1493 # by removing this.
1494 ctx_keys[0].startswith("service integrated-vtysh-config")
1495 ):
1496
1497 log.info('"%s" cannot be removed' % (ctx_keys[-1],))
1498 lines_to_del_to_del.append((ctx_keys, line))
1499
1500 for (ctx_keys, line) in lines_to_del_to_del:
1501 lines_to_del.remove((ctx_keys, line))
1502
1503 return (lines_to_add, lines_to_del)
1504
1505
1506 def compare_context_objects(newconf, running):
1507 """
1508 Create a context diff for the two specified contexts
1509 """
1510
1511 # Compare the two Config objects to find the lines that we need to add/del
1512 lines_to_add = []
1513 lines_to_del = []
1514 pollist_to_del = []
1515 seglist_to_del = []
1516 pceconf_to_del = []
1517 pcclist_to_del = []
1518 candidates_to_add = []
1519 delete_bgpd = False
1520
1521 # Find contexts that are in newconf but not in running
1522 # Find contexts that are in running but not in newconf
1523 for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
1524
1525 if running_ctx_keys not in newconf.contexts:
1526
1527 # We check that the len is 1 here so that we only look at ('router bgp 10')
1528 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1529 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1530 # running but not in newconf.
1531 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
1532 delete_bgpd = True
1533 lines_to_del.append((running_ctx_keys, None))
1534
1535 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1536 elif running_ctx_keys[0].startswith("interface") or running_ctx_keys[
1537 0
1538 ].startswith("vrf"):
1539 for line in running_ctx.lines:
1540 lines_to_del.append((running_ctx_keys, line))
1541
1542 # If this is an address-family under 'router bgp' and we are already deleting the
1543 # entire 'router bgp' context then ignore this sub-context
1544 elif (
1545 "router bgp" in running_ctx_keys[0]
1546 and len(running_ctx_keys) > 1
1547 and delete_bgpd
1548 ):
1549 continue
1550
1551 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1552 elif (
1553 "router bgp" in running_ctx_keys[0]
1554 and len(running_ctx_keys) > 2
1555 and running_ctx_keys[1].startswith("address-family l2vpn evpn")
1556 and running_ctx_keys[2].startswith("vni ")
1557 ):
1558 lines_to_del.append((running_ctx_keys, None))
1559
1560 elif (
1561 "router bgp" in running_ctx_keys[0]
1562 and len(running_ctx_keys) > 1
1563 and running_ctx_keys[1].startswith("address-family")
1564 ):
1565 # There's no 'no address-family' support and so we have to
1566 # delete each line individually again
1567 for line in running_ctx.lines:
1568 lines_to_del.append((running_ctx_keys, line))
1569
1570 # Some commands can happen at higher counts that make
1571 # doing vtysh -c inefficient (and can time out.) For
1572 # these commands, instead of adding them to lines_to_del,
1573 # add the "no " version to lines_to_add.
1574 elif running_ctx_keys[0].startswith("ip route") or running_ctx_keys[
1575 0
1576 ].startswith("ipv6 route"):
1577 add_cmd = ("no " + running_ctx_keys[0],)
1578 lines_to_add.append((add_cmd, None))
1579
1580 # if this an interface sub-subcontext in an address-family block in ldpd and
1581 # we are already deleting the whole context, then ignore this
1582 elif (
1583 len(running_ctx_keys) > 2
1584 and running_ctx_keys[0].startswith("mpls ldp")
1585 and running_ctx_keys[1].startswith("address-family")
1586 and (running_ctx_keys[:2], None) in lines_to_del
1587 ):
1588 continue
1589
1590 # same thing for a pseudowire sub-context inside an l2vpn context
1591 elif (
1592 len(running_ctx_keys) > 1
1593 and running_ctx_keys[0].startswith("l2vpn")
1594 and running_ctx_keys[1].startswith("member pseudowire")
1595 and (running_ctx_keys[:1], None) in lines_to_del
1596 ):
1597 continue
1598
1599 # Segment routing and traffic engineering never need to be deleted
1600 elif (
1601 running_ctx_keys[0].startswith("segment-routing")
1602 and len(running_ctx_keys) < 3
1603 ):
1604 continue
1605
1606 # Neither the pcep command
1607 elif (
1608 len(running_ctx_keys) == 3
1609 and running_ctx_keys[0].startswith("segment-routing")
1610 and running_ctx_keys[2].startswith("pcep")
1611 ):
1612 continue
1613
1614 # Segment lists can only be deleted after we removed all the candidate paths that
1615 # use them, so add them to a separate array that is going to be appended at the end
1616 elif (
1617 len(running_ctx_keys) == 3
1618 and running_ctx_keys[0].startswith("segment-routing")
1619 and running_ctx_keys[2].startswith("segment-list")
1620 ):
1621 seglist_to_del.append((running_ctx_keys, None))
1622
1623 # Policies must be deleted after there candidate path, to be sure
1624 # we add them to a separate array that is going to be appended at the end
1625 elif (
1626 len(running_ctx_keys) == 3
1627 and running_ctx_keys[0].startswith("segment-routing")
1628 and running_ctx_keys[2].startswith("policy")
1629 ):
1630 pollist_to_del.append((running_ctx_keys, None))
1631
1632 # pce-config must be deleted after the pce, to be sure we add them
1633 # to a separate array that is going to be appended at the end
1634 elif (
1635 len(running_ctx_keys) >= 4
1636 and running_ctx_keys[0].startswith("segment-routing")
1637 and running_ctx_keys[3].startswith("pce-config")
1638 ):
1639 pceconf_to_del.append((running_ctx_keys, None))
1640
1641 # pcc must be deleted after the pce and pce-config too
1642 elif (
1643 len(running_ctx_keys) >= 4
1644 and running_ctx_keys[0].startswith("segment-routing")
1645 and running_ctx_keys[3].startswith("pcc")
1646 ):
1647 pcclist_to_del.append((running_ctx_keys, None))
1648
1649 # Non-global context
1650 elif running_ctx_keys and not any(
1651 "address-family" in key for key in running_ctx_keys
1652 ):
1653 lines_to_del.append((running_ctx_keys, None))
1654
1655 elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys):
1656 lines_to_del.append((running_ctx_keys, None))
1657
1658 # Global context
1659 else:
1660 for line in running_ctx.lines:
1661 lines_to_del.append((running_ctx_keys, line))
1662
1663 # if we have some policies commands to delete, append them to lines_to_del
1664 if len(pollist_to_del) > 0:
1665 lines_to_del.extend(pollist_to_del)
1666
1667 # if we have some segment list commands to delete, append them to lines_to_del
1668 if len(seglist_to_del) > 0:
1669 lines_to_del.extend(seglist_to_del)
1670
1671 # if we have some pce list commands to delete, append them to lines_to_del
1672 if len(pceconf_to_del) > 0:
1673 lines_to_del.extend(pceconf_to_del)
1674
1675 # if we have some pcc list commands to delete, append them to lines_to_del
1676 if len(pcclist_to_del) > 0:
1677 lines_to_del.extend(pcclist_to_del)
1678
1679 # Find the lines within each context to add
1680 # Find the lines within each context to del
1681 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
1682
1683 if newconf_ctx_keys in running.contexts:
1684 running_ctx = running.contexts[newconf_ctx_keys]
1685
1686 for line in newconf_ctx.lines:
1687 if line not in running_ctx.dlines:
1688
1689 # candidate paths can only be added after the policy and segment list,
1690 # so add them to a separate array that is going to be appended at the end
1691 if (
1692 len(newconf_ctx_keys) == 3
1693 and newconf_ctx_keys[0].startswith("segment-routing")
1694 and newconf_ctx_keys[2].startswith("policy ")
1695 and line.startswith("candidate-path ")
1696 ):
1697 candidates_to_add.append((newconf_ctx_keys, line))
1698
1699 else:
1700 lines_to_add.append((newconf_ctx_keys, line))
1701
1702 for line in running_ctx.lines:
1703 if line not in newconf_ctx.dlines:
1704 lines_to_del.append((newconf_ctx_keys, line))
1705
1706 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
1707
1708 if newconf_ctx_keys not in running.contexts:
1709
1710 # candidate paths can only be added after the policy and segment list,
1711 # so add them to a separate array that is going to be appended at the end
1712 if (
1713 len(newconf_ctx_keys) == 4
1714 and newconf_ctx_keys[0].startswith("segment-routing")
1715 and newconf_ctx_keys[3].startswith("candidate-path")
1716 ):
1717 candidates_to_add.append((newconf_ctx_keys, None))
1718 for line in newconf_ctx.lines:
1719 candidates_to_add.append((newconf_ctx_keys, line))
1720
1721 else:
1722 lines_to_add.append((newconf_ctx_keys, None))
1723
1724 for line in newconf_ctx.lines:
1725 lines_to_add.append((newconf_ctx_keys, line))
1726
1727 # if we have some candidate paths commands to add, append them to lines_to_add
1728 if len(candidates_to_add) > 0:
1729 lines_to_add.extend(candidates_to_add)
1730
1731 (lines_to_add, lines_to_del) = check_for_exit_vrf(lines_to_add, lines_to_del)
1732 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(
1733 lines_to_add, lines_to_del
1734 )
1735 (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(
1736 lines_to_add, lines_to_del
1737 )
1738
1739 return (lines_to_add, lines_to_del)
1740
1741
1742 if __name__ == "__main__":
1743 # Command line options
1744 parser = argparse.ArgumentParser(
1745 description="Dynamically apply diff in frr configs"
1746 )
1747 parser.add_argument(
1748 "--input", help='Read running config from file instead of "show running"'
1749 )
1750 group = parser.add_mutually_exclusive_group(required=True)
1751 group.add_argument(
1752 "--reload", action="store_true", help="Apply the deltas", default=False
1753 )
1754 group.add_argument(
1755 "--test", action="store_true", help="Show the deltas", default=False
1756 )
1757 level_group = parser.add_mutually_exclusive_group()
1758 level_group.add_argument(
1759 "--debug",
1760 action="store_true",
1761 help="Enable debugs (synonym for --log-level=debug)",
1762 default=False,
1763 )
1764 level_group.add_argument(
1765 "--log-level",
1766 help="Log level",
1767 default="info",
1768 choices=("critical", "error", "warning", "info", "debug"),
1769 )
1770 parser.add_argument(
1771 "--stdout", action="store_true", help="Log to STDOUT", default=False
1772 )
1773 parser.add_argument(
1774 "--pathspace",
1775 "-N",
1776 metavar="NAME",
1777 help="Reload specified path/namespace",
1778 default=None,
1779 )
1780 parser.add_argument("filename", help="Location of new frr config file")
1781 parser.add_argument(
1782 "--overwrite",
1783 action="store_true",
1784 help="Overwrite frr.conf with running config output",
1785 default=False,
1786 )
1787 parser.add_argument(
1788 "--bindir", help="path to the vtysh executable", default="/usr/bin"
1789 )
1790 parser.add_argument(
1791 "--confdir", help="path to the daemon config files", default="/etc/frr"
1792 )
1793 parser.add_argument(
1794 "--rundir", help="path for the temp config file", default="/var/run/frr"
1795 )
1796 parser.add_argument(
1797 "--vty_socket",
1798 help="socket to be used by vtysh to connect to the daemons",
1799 default=None,
1800 )
1801 parser.add_argument(
1802 "--daemon", help="daemon for which want to replace the config", default=""
1803 )
1804
1805 args = parser.parse_args()
1806
1807 # Logging
1808 # For --test log to stdout
1809 # For --reload log to /var/log/frr/frr-reload.log
1810 if args.test or args.stdout:
1811 logging.basicConfig(format="%(asctime)s %(levelname)5s: %(message)s")
1812
1813 # Color the errors and warnings in red
1814 logging.addLevelName(
1815 logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR)
1816 )
1817 logging.addLevelName(
1818 logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING)
1819 )
1820
1821 elif args.reload:
1822 if not os.path.isdir("/var/log/frr/"):
1823 os.makedirs("/var/log/frr/")
1824
1825 logging.basicConfig(
1826 filename="/var/log/frr/frr-reload.log",
1827 format="%(asctime)s %(levelname)5s: %(message)s",
1828 )
1829
1830 # argparse should prevent this from happening but just to be safe...
1831 else:
1832 raise Exception("Must specify --reload or --test")
1833 log = logging.getLogger(__name__)
1834
1835 if args.debug:
1836 log.setLevel(logging.DEBUG)
1837 else:
1838 log.setLevel(args.log_level.upper())
1839
1840 if args.reload and not args.stdout:
1841 # Additionally send errors and above to STDOUT, with no metadata,
1842 # when we are logging to a file. This specifically does not follow
1843 # args.log_level, and is analagous to behaviour in earlier versions
1844 # which additionally logged most errors using print().
1845
1846 stdout_hdlr = logging.StreamHandler(sys.stdout)
1847 stdout_hdlr.setLevel(logging.ERROR)
1848 stdout_hdlr.setFormatter(logging.Formatter())
1849 log.addHandler(stdout_hdlr)
1850
1851 # Verify the new config file is valid
1852 if not os.path.isfile(args.filename):
1853 log.error("Filename %s does not exist" % args.filename)
1854 sys.exit(1)
1855
1856 if not os.path.getsize(args.filename):
1857 log.error("Filename %s is an empty file" % args.filename)
1858 sys.exit(1)
1859
1860 # Verify that confdir is correct
1861 if not os.path.isdir(args.confdir):
1862 log.error("Confdir %s is not a valid path" % args.confdir)
1863 sys.exit(1)
1864
1865 # Verify that bindir is correct
1866 if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + "/vtysh"):
1867 log.error("Bindir %s is not a valid path to vtysh" % args.bindir)
1868 sys.exit(1)
1869
1870 # verify that the vty_socket, if specified, is valid
1871 if args.vty_socket and not os.path.isdir(args.vty_socket):
1872 log.error("vty_socket %s is not a valid path" % args.vty_socket)
1873 sys.exit(1)
1874
1875 # verify that the daemon, if specified, is valid
1876 if args.daemon and args.daemon not in [
1877 "zebra",
1878 "bgpd",
1879 "fabricd",
1880 "isisd",
1881 "ospf6d",
1882 "ospfd",
1883 "pbrd",
1884 "pimd",
1885 "ripd",
1886 "ripngd",
1887 "sharpd",
1888 "staticd",
1889 "vrrpd",
1890 "ldpd",
1891 "pathd",
1892 "bfdd",
1893 ]:
1894 msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon
1895 print(msg)
1896 log.error(msg)
1897 sys.exit(1)
1898
1899 vtysh = Vtysh(args.bindir, args.confdir, args.vty_socket, args.pathspace)
1900
1901 # Verify that 'service integrated-vtysh-config' is configured
1902 if args.pathspace:
1903 vtysh_filename = args.confdir + "/" + args.pathspace + "/vtysh.conf"
1904 else:
1905 vtysh_filename = args.confdir + "/vtysh.conf"
1906 service_integrated_vtysh_config = True
1907
1908 if os.path.isfile(vtysh_filename):
1909 with open(vtysh_filename, "r") as fh:
1910 for line in fh.readlines():
1911 line = line.strip()
1912
1913 if line == "no service integrated-vtysh-config":
1914 service_integrated_vtysh_config = False
1915 break
1916
1917 if not service_integrated_vtysh_config and not args.daemon:
1918 log.error(
1919 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1920 )
1921 sys.exit(1)
1922
1923 log.info('Called via "%s"', str(args))
1924
1925 # Create a Config object from the config generated by newconf
1926 newconf = Config(vtysh)
1927 try:
1928 newconf.load_from_file(args.filename)
1929 reload_ok = True
1930 except VtyshException as ve:
1931 log.error("vtysh failed to process new configuration: {}".format(ve))
1932 reload_ok = False
1933
1934 if args.test:
1935
1936 # Create a Config object from the running config
1937 running = Config(vtysh)
1938
1939 if args.input:
1940 running.load_from_file(args.input)
1941 else:
1942 running.load_from_show_running(args.daemon)
1943
1944 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
1945 lines_to_configure = []
1946
1947 if lines_to_del:
1948 print("\nLines To Delete")
1949 print("===============")
1950
1951 for (ctx_keys, line) in lines_to_del:
1952
1953 if line == "!":
1954 continue
1955
1956 cmd = "\n".join(lines_to_config(ctx_keys, line, True))
1957 lines_to_configure.append(cmd)
1958 print(cmd)
1959
1960 if lines_to_add:
1961 print("\nLines To Add")
1962 print("============")
1963
1964 for (ctx_keys, line) in lines_to_add:
1965
1966 if line == "!":
1967 continue
1968
1969 cmd = "\n".join(lines_to_config(ctx_keys, line, False))
1970 lines_to_configure.append(cmd)
1971 print(cmd)
1972
1973 elif args.reload:
1974
1975 # We will not be able to do anything, go ahead and exit(1)
1976 if not vtysh.is_config_available():
1977 sys.exit(1)
1978
1979 log.debug("New Frr Config\n%s", newconf.get_lines())
1980
1981 # This looks a little odd but we have to do this twice...here is why
1982 # If the user had this running bgp config:
1983 #
1984 # router bgp 10
1985 # neighbor 1.1.1.1 remote-as 50
1986 # neighbor 1.1.1.1 route-map FOO out
1987 #
1988 # and this config in the newconf config file
1989 #
1990 # router bgp 10
1991 # neighbor 1.1.1.1 remote-as 999
1992 # neighbor 1.1.1.1 route-map FOO out
1993 #
1994 #
1995 # Then the script will do
1996 # - no neighbor 1.1.1.1 remote-as 50
1997 # - neighbor 1.1.1.1 remote-as 999
1998 #
1999 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
2000 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
2001 # configs again to put this line back.
2002
2003 # There are many keywords in FRR that can only appear one time under
2004 # a context, take "bgp router-id" for example. If the config that we are
2005 # reloading against has the following:
2006 #
2007 # router bgp 10
2008 # bgp router-id 1.1.1.1
2009 # bgp router-id 2.2.2.2
2010 #
2011 # The final config needs to contain "bgp router-id 2.2.2.2". On the
2012 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
2013 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
2014 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
2015 # second pass to include all of the "adds" from the first pass.
2016 lines_to_add_first_pass = []
2017
2018 for x in range(2):
2019 running = Config(vtysh)
2020 running.load_from_show_running(args.daemon)
2021 log.debug("Running Frr Config (Pass #%d)\n%s", x, running.get_lines())
2022
2023 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2024
2025 if x == 0:
2026 lines_to_add_first_pass = lines_to_add
2027 else:
2028 lines_to_add.extend(lines_to_add_first_pass)
2029
2030 # Only do deletes on the first pass. The reason being if we
2031 # configure a bgp neighbor via "neighbor swp1 interface" FRR
2032 # will automatically add:
2033 #
2034 # interface swp1
2035 # ipv6 nd ra-interval 10
2036 # no ipv6 nd suppress-ra
2037 # !
2038 #
2039 # but those lines aren't in the config we are reloading against so
2040 # on the 2nd pass they will show up in lines_to_del. This could
2041 # apply to other scenarios as well where configuring FOO adds BAR
2042 # to the config.
2043 if lines_to_del and x == 0:
2044 for (ctx_keys, line) in lines_to_del:
2045
2046 if line == "!":
2047 continue
2048
2049 # 'no' commands are tricky, we can't just put them in a file and
2050 # vtysh -f that file. See the next comment for an explanation
2051 # of their quirks
2052 cmd = lines_to_config(ctx_keys, line, True)
2053 original_cmd = cmd
2054
2055 # Some commands in frr are picky about taking a "no" of the entire line.
2056 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
2057 # only the beginning. If we hit one of these command an exception will be
2058 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2059 #
2060 # Example:
2061 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2062 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2063 # % Unknown command.
2064 # frr(config-if)# no ip ospf authentication message-digest
2065 # % Unknown command.
2066 # frr(config-if)# no ip ospf authentication
2067 # frr(config-if)#
2068
2069 stdouts = []
2070 while True:
2071 try:
2072 vtysh(["configure"] + cmd, stdouts)
2073
2074 except VtyshException:
2075
2076 # - Pull the last entry from cmd (this would be
2077 # 'no ip ospf authentication message-digest 1.1.1.1' in
2078 # our example above
2079 # - Split that last entry by whitespace and drop the last word
2080 log.info("Failed to execute %s", " ".join(cmd))
2081 last_arg = cmd[-1].split(" ")
2082
2083 if len(last_arg) <= 2:
2084 log.error(
2085 '"%s" we failed to remove this command',
2086 " -- ".join(original_cmd),
2087 )
2088 # Log first error msg for original_cmd
2089 if stdouts:
2090 log.error(stdouts[0])
2091 reload_ok = False
2092 break
2093
2094 new_last_arg = last_arg[0:-1]
2095 cmd[-1] = " ".join(new_last_arg)
2096 else:
2097 log.info('Executed "%s"', " ".join(cmd))
2098 break
2099
2100 if lines_to_add:
2101 lines_to_configure = []
2102
2103 for (ctx_keys, line) in lines_to_add:
2104
2105 if line == "!":
2106 continue
2107
2108 # Don't run "no" commands twice since they can error
2109 # out the second time due to first deletion
2110 if x == 1 and ctx_keys[0].startswith("no "):
2111 continue
2112
2113 cmd = "\n".join(lines_to_config(ctx_keys, line, False)) + "\n"
2114 lines_to_configure.append(cmd)
2115
2116 if lines_to_configure:
2117 random_string = "".join(
2118 random.SystemRandom().choice(
2119 string.ascii_uppercase + string.digits
2120 )
2121 for _ in range(6)
2122 )
2123
2124 filename = args.rundir + "/reload-%s.txt" % random_string
2125 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
2126
2127 with open(filename, "w") as fh:
2128 for line in lines_to_configure:
2129 fh.write(line + "\n")
2130
2131 try:
2132 vtysh.exec_file(filename)
2133 except VtyshException as e:
2134 log.warning("frr-reload.py failed due to\n%s" % e.args)
2135 reload_ok = False
2136 os.unlink(filename)
2137
2138 # Make these changes persistent
2139 target = str(args.confdir + "/frr.conf")
2140 if args.overwrite or (not args.daemon and args.filename != target):
2141 vtysh("write")
2142
2143 if not reload_ok:
2144 sys.exit(1)