]> git.proxmox.com Git - mirror_frr.git/blob - tools/frr-reload.py
tools: reload recognizes `pbr table range` as single-line ctx
[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
427 key[0] = "%s prefix-list%s%s %s%s" % (
428 re_key_rt.group(1),
429 re_key_rt.group(2),
430 re_key_rt.group(3),
431 newaddr,
432 legestr,
433 )
434
435 if lines and key[0].startswith("router bgp"):
436 newlines = []
437 for line in lines:
438 re_net = re.match(r"network\s+([A-Fa-f:.0-9/]+)(.*)$", line)
439 if re_net:
440 addr = re_net.group(1)
441 if "/" not in addr and key[0].startswith("router bgp"):
442 # This is most likely an error because with no
443 # prefixlen, BGP treats the prefixlen as 8
444 addr = addr + "/8"
445
446 try:
447 if "ipaddress" not in sys.modules:
448 newaddr = IPNetwork(addr)
449 line = "network %s/%s %s" % (
450 newaddr.network,
451 newaddr.prefixlen,
452 re_net.group(2),
453 )
454 else:
455 network_addr = ip_network(addr, strict=False)
456 line = "network %s/%s %s" % (
457 str(network_addr.network_address),
458 network_addr.prefixlen,
459 re_net.group(2),
460 )
461 newlines.append(line)
462 except ValueError:
463 # Really this should be an error. Whats a network
464 # without an IP Address following it ?
465 newlines.append(line)
466 else:
467 newlines.append(line)
468 lines = newlines
469
470 """
471 More fixups in user specification and what running config shows.
472 "null0" in routes must be replaced by Null0.
473 """
474 if (
475 key[0].startswith("ip route")
476 or key[0].startswith("ipv6 route")
477 and "null0" in key[0]
478 ):
479 key[0] = re.sub(r"\s+null0(\s*$)", " Null0", key[0])
480
481 """
482 Similar to above, but when the static is in a vrf, it turns into a
483 blackhole nexthop for both null0 and Null0. Fix it accordingly
484 """
485 if lines and key[0].startswith("vrf "):
486 newlines = []
487 for line in lines:
488 if line.startswith("ip route ") or line.startswith("ipv6 route "):
489 if "null0" in line:
490 line = re.sub(r"\s+null0(\s*$)", " blackhole", line)
491 elif "Null0" in line:
492 line = re.sub(r"\s+Null0(\s*$)", " blackhole", line)
493 newlines.append(line)
494 else:
495 newlines.append(line)
496 lines = newlines
497
498 if lines:
499 if tuple(key) not in self.contexts:
500 ctx = Context(tuple(key), lines)
501 self.contexts[tuple(key)] = ctx
502 else:
503 ctx = self.contexts[tuple(key)]
504 ctx.add_lines(lines)
505
506 else:
507 if tuple(key) not in self.contexts:
508 ctx = Context(tuple(key), [])
509 self.contexts[tuple(key)] = ctx
510
511 def load_contexts(self):
512 """
513 Parse the configuration and create contexts for each appropriate block
514 """
515
516 current_context_lines = []
517 ctx_keys = []
518
519 """
520 The end of a context is flagged via the 'end' keyword:
521
522 !
523 interface swp52
524 ipv6 nd suppress-ra
525 link-detect
526 !
527 end
528 router bgp 10
529 bgp router-id 10.0.0.1
530 bgp log-neighbor-changes
531 no bgp default ipv4-unicast
532 neighbor EBGP peer-group
533 neighbor EBGP advertisement-interval 1
534 neighbor EBGP timers connect 10
535 neighbor 2001:40:1:4::6 remote-as 40
536 neighbor 2001:40:1:8::a remote-as 40
537 !
538 end
539 address-family ipv6
540 neighbor IBGPv6 activate
541 neighbor 2001:10::2 peer-group IBGPv6
542 neighbor 2001:10::3 peer-group IBGPv6
543 exit-address-family
544 !
545 end
546 address-family evpn
547 neighbor LEAF activate
548 advertise-all-vni
549 vni 10100
550 rd 65000:10100
551 route-target import 10.1.1.1:10100
552 route-target export 10.1.1.1:10100
553 exit-vni
554 exit-address-family
555 !
556 end
557 router ospf
558 ospf router-id 10.0.0.1
559 log-adjacency-changes detail
560 timers throttle spf 0 50 5000
561 !
562 end
563 """
564
565 # The code assumes that its working on the output from the "vtysh -m"
566 # command. That provides the appropriate markers to signify end of
567 # a context. This routine uses that to build the contexts for the
568 # config.
569 #
570 # There are single line contexts such as "log file /media/node/zebra.log"
571 # and multi-line contexts such as "router ospf" and subcontexts
572 # within a context such as "address-family" within "router bgp"
573 # In each of these cases, the first line of the context becomes the
574 # key of the context. So "router bgp 10" is the key for the non-address
575 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
576 # the key for the subcontext and so on.
577 ctx_keys = []
578 main_ctx_key = []
579 new_ctx = True
580
581 # the keywords that we know are single line contexts. bgp in this case
582 # is not the main router bgp block, but enabling multi-instance
583 oneline_ctx_keywords = (
584 "access-list ",
585 "agentx",
586 "allow-external-route-update",
587 "bgp ",
588 "debug ",
589 "domainname ",
590 "dump ",
591 "enable ",
592 "frr ",
593 "fpm ",
594 "hostname ",
595 "ip ",
596 "ipv6 ",
597 "log ",
598 "mpls lsp",
599 "mpls label",
600 "no ",
601 "password ",
602 "pbr ",
603 "ptm-enable",
604 "router-id ",
605 "service ",
606 "table ",
607 "username ",
608 "zebra ",
609 "vrrp autoconfigure",
610 "evpn mh",
611 )
612
613 for line in self.lines:
614
615 if not line:
616 continue
617
618 if line.startswith("!") or line.startswith("#"):
619 continue
620
621 if (
622 len(ctx_keys) == 2
623 and ctx_keys[0].startswith("bfd")
624 and ctx_keys[1].startswith("profile ")
625 and line == "end"
626 ):
627 log.debug("LINE %-50s: popping from sub context, %-50s", line, ctx_keys)
628
629 if main_ctx_key:
630 self.save_contexts(ctx_keys, current_context_lines)
631 ctx_keys = copy.deepcopy(main_ctx_key)
632 current_context_lines = []
633 continue
634
635 # one line contexts
636 # there is one exception though: ldpd accepts a 'router-id' clause
637 # as part of its 'mpls ldp' config context. If we are processing
638 # ldp configuration and encounter a router-id we should NOT switch
639 # to a new context
640 if (
641 new_ctx is True
642 and any(line.startswith(keyword) for keyword in oneline_ctx_keywords)
643 and not (
644 ctx_keys
645 and ctx_keys[0].startswith("mpls ldp")
646 and line.startswith("router-id ")
647 )
648 ):
649 self.save_contexts(ctx_keys, current_context_lines)
650
651 # Start a new context
652 main_ctx_key = []
653 ctx_keys = [
654 line,
655 ]
656 current_context_lines = []
657
658 log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys)
659 self.save_contexts(ctx_keys, current_context_lines)
660 new_ctx = True
661
662 elif line == "end":
663 self.save_contexts(ctx_keys, current_context_lines)
664 log.debug("LINE %-50s: exiting old context, %-50s", line, ctx_keys)
665
666 # Start a new context
667 new_ctx = True
668 main_ctx_key = []
669 ctx_keys = []
670 current_context_lines = []
671
672 elif line == "exit" and ctx_keys[0].startswith("rpki"):
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-vrf":
683 self.save_contexts(ctx_keys, current_context_lines)
684 current_context_lines.append(line)
685 log.debug(
686 "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys
687 )
688
689 # Start a new context
690 new_ctx = True
691 main_ctx_key = []
692 ctx_keys = []
693 current_context_lines = []
694
695 elif (
696 line == "exit"
697 and len(ctx_keys) > 1
698 and ctx_keys[0].startswith("segment-routing")
699 ):
700 self.save_contexts(ctx_keys, current_context_lines)
701
702 # Start a new context
703 ctx_keys = ctx_keys[:-1]
704 current_context_lines = []
705 log.debug(
706 "LINE %-50s: popping segment routing sub-context to ctx%-50s",
707 line,
708 ctx_keys,
709 )
710
711 elif line in ["exit-address-family", "exit", "exit-vnc"]:
712 # if this exit is for address-family ipv4 unicast, ignore the pop
713 if main_ctx_key:
714 self.save_contexts(ctx_keys, current_context_lines)
715
716 # Start a new context
717 ctx_keys = copy.deepcopy(main_ctx_key)
718 current_context_lines = []
719 log.debug(
720 "LINE %-50s: popping from subcontext to ctx%-50s",
721 line,
722 ctx_keys,
723 )
724
725 elif line in ["exit-vni", "exit-ldp-if"]:
726 if sub_main_ctx_key:
727 self.save_contexts(ctx_keys, current_context_lines)
728
729 # Start a new context
730 ctx_keys = copy.deepcopy(sub_main_ctx_key)
731 current_context_lines = []
732 log.debug(
733 "LINE %-50s: popping from sub-subcontext to ctx%-50s",
734 line,
735 ctx_keys,
736 )
737
738 elif new_ctx is True:
739 if not main_ctx_key:
740 ctx_keys = [
741 line,
742 ]
743 else:
744 ctx_keys = copy.deepcopy(main_ctx_key)
745 main_ctx_key = []
746
747 current_context_lines = []
748 new_ctx = False
749 log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys)
750
751 elif (
752 line.startswith("address-family ")
753 or line.startswith("vnc defaults")
754 or line.startswith("vnc l2-group")
755 or line.startswith("vnc nve-group")
756 or line.startswith("peer")
757 or line.startswith("key ")
758 or line.startswith("member pseudowire")
759 ):
760 main_ctx_key = []
761
762 # Save old context first
763 self.save_contexts(ctx_keys, current_context_lines)
764 current_context_lines = []
765 main_ctx_key = copy.deepcopy(ctx_keys)
766 log.debug("LINE %-50s: entering sub-context, append to ctx_keys", line)
767
768 if line == "address-family ipv6" and not ctx_keys[0].startswith(
769 "mpls ldp"
770 ):
771 ctx_keys.append("address-family ipv6 unicast")
772 elif line == "address-family ipv4" and not ctx_keys[0].startswith(
773 "mpls ldp"
774 ):
775 ctx_keys.append("address-family ipv4 unicast")
776 elif line == "address-family evpn":
777 ctx_keys.append("address-family l2vpn evpn")
778 else:
779 ctx_keys.append(line)
780
781 elif (
782 line.startswith("vni ")
783 and len(ctx_keys) == 2
784 and ctx_keys[0].startswith("router bgp")
785 and ctx_keys[1] == "address-family l2vpn evpn"
786 ):
787
788 # Save old context first
789 self.save_contexts(ctx_keys, current_context_lines)
790 current_context_lines = []
791 sub_main_ctx_key = copy.deepcopy(ctx_keys)
792 log.debug(
793 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
794 )
795 ctx_keys.append(line)
796
797 elif (
798 line.startswith("interface ")
799 and len(ctx_keys) == 2
800 and ctx_keys[0].startswith("mpls ldp")
801 and ctx_keys[1].startswith("address-family")
802 ):
803
804 # Save old context first
805 self.save_contexts(ctx_keys, current_context_lines)
806 current_context_lines = []
807 sub_main_ctx_key = copy.deepcopy(ctx_keys)
808 log.debug(
809 "LINE %-50s: entering sub-sub-context, append to ctx_keys", line
810 )
811 ctx_keys.append(line)
812
813 elif (
814 line.startswith("traffic-eng")
815 and len(ctx_keys) == 1
816 and ctx_keys[0].startswith("segment-routing")
817 ):
818
819 # Save old context first
820 self.save_contexts(ctx_keys, current_context_lines)
821 current_context_lines = []
822 log.debug(
823 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
824 line,
825 )
826 ctx_keys.append(line)
827
828 elif (
829 line.startswith("segment-list ")
830 and len(ctx_keys) == 2
831 and ctx_keys[0].startswith("segment-routing")
832 and ctx_keys[1].startswith("traffic-eng")
833 ):
834
835 # Save old context first
836 self.save_contexts(ctx_keys, current_context_lines)
837 current_context_lines = []
838 log.debug(
839 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
840 line,
841 )
842 ctx_keys.append(line)
843
844 elif (
845 line.startswith("policy ")
846 and len(ctx_keys) == 2
847 and ctx_keys[0].startswith("segment-routing")
848 and ctx_keys[1].startswith("traffic-eng")
849 ):
850
851 # Save old context first
852 self.save_contexts(ctx_keys, current_context_lines)
853 current_context_lines = []
854 log.debug(
855 "LINE %-50s: entering segment routing sub-context, append to ctx_keys",
856 line,
857 )
858 ctx_keys.append(line)
859
860 elif (
861 line.startswith("candidate-path ")
862 and line.endswith(" dynamic")
863 and len(ctx_keys) == 3
864 and ctx_keys[0].startswith("segment-routing")
865 and ctx_keys[1].startswith("traffic-eng")
866 and ctx_keys[2].startswith("policy")
867 ):
868
869 # Save old context first
870 self.save_contexts(ctx_keys, current_context_lines)
871 current_context_lines = []
872 main_ctx_key = copy.deepcopy(ctx_keys)
873 log.debug(
874 "LINE %-50s: entering candidate-path sub-context, append to ctx_keys",
875 line,
876 )
877 ctx_keys.append(line)
878
879 elif (
880 line.startswith("pcep")
881 and len(ctx_keys) == 2
882 and ctx_keys[0].startswith("segment-routing")
883 and ctx_keys[1].startswith("traffic-eng")
884 ):
885
886 # Save old context first
887 self.save_contexts(ctx_keys, current_context_lines)
888 current_context_lines = []
889 main_ctx_key = copy.deepcopy(ctx_keys)
890 log.debug(
891 "LINE %-50s: entering pcep sub-context, append to ctx_keys", line
892 )
893 ctx_keys.append(line)
894
895 elif (
896 line.startswith("pce-config ")
897 and len(ctx_keys) == 3
898 and ctx_keys[0].startswith("segment-routing")
899 and ctx_keys[1].startswith("traffic-eng")
900 and ctx_keys[2].startswith("pcep")
901 ):
902
903 # Save old context first
904 self.save_contexts(ctx_keys, current_context_lines)
905 current_context_lines = []
906 main_ctx_key = copy.deepcopy(ctx_keys)
907 log.debug(
908 "LINE %-50s: entering pce-config sub-context, append to ctx_keys",
909 line,
910 )
911 ctx_keys.append(line)
912
913 elif (
914 line.startswith("pce ")
915 and len(ctx_keys) == 3
916 and ctx_keys[0].startswith("segment-routing")
917 and ctx_keys[1].startswith("traffic-eng")
918 and ctx_keys[2].startswith("pcep")
919 ):
920
921 # Save old context first
922 self.save_contexts(ctx_keys, current_context_lines)
923 current_context_lines = []
924 main_ctx_key = copy.deepcopy(ctx_keys)
925 log.debug(
926 "LINE %-50s: entering pce sub-context, append to ctx_keys", line
927 )
928 ctx_keys.append(line)
929
930 elif (
931 line.startswith("pcc")
932 and len(ctx_keys) == 3
933 and ctx_keys[0].startswith("segment-routing")
934 and ctx_keys[1].startswith("traffic-eng")
935 and ctx_keys[2].startswith("pcep")
936 ):
937
938 # Save old context first
939 self.save_contexts(ctx_keys, current_context_lines)
940 current_context_lines = []
941 main_ctx_key = copy.deepcopy(ctx_keys)
942 log.debug(
943 "LINE %-50s: entering pcc sub-context, append to ctx_keys", line
944 )
945 ctx_keys.append(line)
946
947 elif (
948 line.startswith("profile ")
949 and len(ctx_keys) == 1
950 and ctx_keys[0].startswith("bfd")
951 ):
952
953 # Save old context first
954 self.save_contexts(ctx_keys, current_context_lines)
955 current_context_lines = []
956 main_ctx_key = copy.deepcopy(ctx_keys)
957 log.debug(
958 "LINE %-50s: entering BFD profile sub-context, append to ctx_keys",
959 line,
960 )
961 ctx_keys.append(line)
962
963 else:
964 # Continuing in an existing context, add non-commented lines to it
965 current_context_lines.append(line)
966 log.debug(
967 "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys
968 )
969
970 # Save the context of the last one
971 self.save_contexts(ctx_keys, current_context_lines)
972
973
974 def lines_to_config(ctx_keys, line, delete):
975 """
976 Return the command as it would appear in frr.conf
977 """
978 cmd = []
979
980 if line:
981 for (i, ctx_key) in enumerate(ctx_keys):
982 cmd.append(" " * i + ctx_key)
983
984 line = line.lstrip()
985 indent = len(ctx_keys) * " "
986
987 # There are some commands that are on by default so their "no" form will be
988 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
989 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
990 # not by doing a "no no bgp default ipv4-unicast"
991 if delete:
992 if line.startswith("no "):
993 cmd.append("%s%s" % (indent, line[3:]))
994 else:
995 cmd.append("%sno %s" % (indent, line))
996
997 else:
998 cmd.append(indent + line)
999
1000 # If line is None then we are typically deleting an entire
1001 # context ('no router ospf' for example)
1002 else:
1003 for i, ctx_key in enumerate(ctx_keys[:-1]):
1004 cmd.append("%s%s" % (" " * i, ctx_key))
1005
1006 # Only put the 'no' on the last sub-context
1007 if delete:
1008 if ctx_keys[-1].startswith("no "):
1009 cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1][3:]))
1010 else:
1011 cmd.append("%sno %s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1]))
1012 else:
1013 cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1]))
1014
1015 return cmd
1016
1017
1018 def get_normalized_ipv6_line(line):
1019 """
1020 Return a normalized IPv6 line as produced by frr,
1021 with all letters in lower case and trailing and leading
1022 zeros removed, and only the network portion present if
1023 the IPv6 word is a network
1024 """
1025 norm_line = ""
1026 words = line.split(" ")
1027 for word in words:
1028 if ":" in word:
1029 norm_word = None
1030 if "/" in word:
1031 try:
1032 if "ipaddress" not in sys.modules:
1033 v6word = IPNetwork(word)
1034 norm_word = "%s/%s" % (v6word.network, v6word.prefixlen)
1035 else:
1036 v6word = ip_network(word, strict=False)
1037 norm_word = "%s/%s" % (
1038 str(v6word.network_address),
1039 v6word.prefixlen,
1040 )
1041 except ValueError:
1042 pass
1043 if not norm_word:
1044 try:
1045 norm_word = "%s" % IPv6Address(word)
1046 except ValueError:
1047 norm_word = word
1048 else:
1049 norm_word = word
1050 norm_line = norm_line + " " + norm_word
1051
1052 return norm_line.strip()
1053
1054
1055 def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
1056 for (ctx_keys, line) in lines:
1057 if ctx_keys == target_ctx_keys:
1058 if exact_match:
1059 if line == target_line:
1060 return True
1061 else:
1062 if line.startswith(target_line):
1063 return True
1064 return False
1065
1066
1067 def check_for_exit_vrf(lines_to_add, lines_to_del):
1068
1069 # exit-vrf is a bit tricky. If the new config is missing it but we
1070 # have configs under a vrf, we need to add it at the end to do the
1071 # right context changes. If exit-vrf exists in both the running and
1072 # new config, we cannot delete it or it will break context changes.
1073 add_exit_vrf = False
1074 index = 0
1075
1076 for (ctx_keys, line) in lines_to_add:
1077 if add_exit_vrf == True:
1078 if ctx_keys[0] != prior_ctx_key:
1079 insert_key = ((prior_ctx_key),)
1080 lines_to_add.insert(index, ((insert_key, "exit-vrf")))
1081 add_exit_vrf = False
1082
1083 if ctx_keys[0].startswith("vrf") and line:
1084 if line != "exit-vrf":
1085 add_exit_vrf = True
1086 prior_ctx_key = ctx_keys[0]
1087 else:
1088 add_exit_vrf = False
1089 index += 1
1090
1091 for (ctx_keys, line) in lines_to_del:
1092 if line == "exit-vrf":
1093 if line_exist(lines_to_add, ctx_keys, line):
1094 lines_to_del.remove((ctx_keys, line))
1095
1096 return (lines_to_add, lines_to_del)
1097
1098
1099 """
1100 This method handles deletion of bgp peer group config.
1101 The objective is to delete config lines related to peers
1102 associated with the peer-group and move the peer-group
1103 config line to the end of the lines_to_del list.
1104 """
1105
1106
1107 def delete_move_lines(lines_to_add, lines_to_del):
1108
1109 del_dict = dict()
1110 # Stores the lines to move to the end of the pending list.
1111 lines_to_del_to_del = []
1112 # Stores the lines to move to end of the pending list.
1113 lines_to_del_to_app = []
1114 found_pg_del_cmd = False
1115
1116 """
1117 When "neighbor <pg_name> peer-group" under a bgp instance is removed,
1118 it also deletes the associated peer config. Any config line below no form of
1119 peer-group related to a peer are errored out as the peer no longer exists.
1120 To cleanup peer-group and associated peer(s) configs:
1121 - Remove all the peers config lines from the pending list (lines_to_del list).
1122 - Move peer-group deletion line to the end of the pending list, to allow
1123 removal of any of the peer-group specific configs.
1124
1125 Create a dictionary of config context (i.e. router bgp vrf x).
1126 Under each context node, create a dictionary of a peer-group name.
1127 Append a peer associated to the peer-group into a list under a peer-group node.
1128 Remove all of the peer associated config lines from the pending list.
1129 Append peer-group deletion line to end of the pending list.
1130
1131 Example:
1132 neighbor underlay peer-group
1133 neighbor underlay remote-as external
1134 neighbor underlay advertisement-interval 0
1135 neighbor underlay timers 3 9
1136 neighbor underlay timers connect 10
1137 neighbor swp1 interface peer-group underlay
1138 neighbor swp1 advertisement-interval 0
1139 neighbor swp1 timers 3 9
1140 neighbor swp1 timers connect 10
1141 neighbor swp2 interface peer-group underlay
1142 neighbor swp2 advertisement-interval 0
1143 neighbor swp2 timers 3 9
1144 neighbor swp2 timers connect 10
1145 neighbor swp3 interface peer-group underlay
1146 neighbor uplink1 interface remote-as internal
1147 neighbor uplink1 advertisement-interval 0
1148 neighbor uplink1 timers 3 9
1149 neighbor uplink1 timers connect 10
1150
1151 New order:
1152 "router bgp 200 no bgp bestpath as-path multipath-relax"
1153 "router bgp 200 no neighbor underlay advertisement-interval 0"
1154 "router bgp 200 no neighbor underlay timers 3 9"
1155 "router bgp 200 no neighbor underlay timers connect 10"
1156 "router bgp 200 no neighbor uplink1 advertisement-interval 0"
1157 "router bgp 200 no neighbor uplink1 timers 3 9"
1158 "router bgp 200 no neighbor uplink1 timers connect 10"
1159 "router bgp 200 no neighbor underlay remote-as external"
1160 "router bgp 200 no neighbor uplink1 interface remote-as internal"
1161 "router bgp 200 no neighbor underlay peer-group"
1162
1163 """
1164
1165 for (ctx_keys, line) in lines_to_del:
1166 if (
1167 ctx_keys[0].startswith("router bgp")
1168 and line
1169 and line.startswith("neighbor ")
1170 ):
1171 """
1172 When 'neighbor <peer> remote-as <>' is removed it deletes the peer,
1173 there might be a peer associated config which also needs to be removed
1174 prior to peer.
1175 Append the 'neighbor <peer> remote-as <>' to the lines_to_del.
1176 Example:
1177
1178 neighbor uplink1 interface remote-as internal
1179 neighbor uplink1 advertisement-interval 0
1180 neighbor uplink1 timers 3 9
1181 neighbor uplink1 timers connect 10
1182
1183 Move to end:
1184 neighbor uplink1 advertisement-interval 0
1185 neighbor uplink1 timers 3 9
1186 neighbor uplink1 timers connect 10
1187 ...
1188
1189 neighbor uplink1 interface remote-as internal
1190
1191 """
1192 # 'no neighbor peer [interface] remote-as <>'
1193 nb_remoteas = "neighbor (\S+) .*remote-as (\S+)"
1194 re_nb_remoteas = re.search(nb_remoteas, line)
1195 if re_nb_remoteas:
1196 lines_to_del_to_app.append((ctx_keys, line))
1197
1198 """
1199 {'router bgp 65001': {'PG': [], 'PG1': []},
1200 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}}
1201 """
1202 if ctx_keys[0] not in del_dict:
1203 del_dict[ctx_keys[0]] = dict()
1204 # find 'no neighbor <pg_name> peer-group'
1205 re_pg = re.match("neighbor (\S+) peer-group$", line)
1206 if re_pg and re_pg.group(1) not in del_dict[ctx_keys[0]]:
1207 del_dict[ctx_keys[0]][re_pg.group(1)] = list()
1208
1209 for (ctx_keys, line) in lines_to_del_to_app:
1210 lines_to_del.remove((ctx_keys, line))
1211 lines_to_del.append((ctx_keys, line))
1212
1213 if found_pg_del_cmd == False:
1214 return (lines_to_add, lines_to_del)
1215
1216 """
1217 {'router bgp 65001': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']},
1218 'router bgp 65001 vrf vrf1': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}}
1219 """
1220 for (ctx_keys, line) in lines_to_del:
1221 if (
1222 ctx_keys[0].startswith("router bgp")
1223 and line
1224 and line.startswith("neighbor ")
1225 ):
1226 if ctx_keys[0] in del_dict:
1227 for pg_key in del_dict[ctx_keys[0]]:
1228 # 'neighbor <peer> [interface] peer-group <pg_name>'
1229 nb_pg = "neighbor (\S+) .*peer-group %s$" % pg_key
1230 re_nbr_pg = re.search(nb_pg, line)
1231 if (
1232 re_nbr_pg
1233 and re_nbr_pg.group(1) not in del_dict[ctx_keys[0]][pg_key]
1234 ):
1235 del_dict[ctx_keys[0]][pg_key].append(re_nbr_pg.group(1))
1236
1237 lines_to_del_to_app = []
1238 for (ctx_keys, line) in lines_to_del:
1239 if (
1240 ctx_keys[0].startswith("router bgp")
1241 and line
1242 and line.startswith("neighbor ")
1243 ):
1244 if ctx_keys[0] in del_dict:
1245 for pg in del_dict[ctx_keys[0]]:
1246 for nbr in del_dict[ctx_keys[0]][pg]:
1247 nb_exp = "neighbor %s .*" % nbr
1248 re_nb = re.search(nb_exp, line)
1249 # add peer configs to delete list.
1250 if re_nb and line not in lines_to_del_to_del:
1251 lines_to_del_to_del.append((ctx_keys, line))
1252
1253 pg_exp = "neighbor %s peer-group$" % pg
1254 re_pg = re.match(pg_exp, line)
1255 if re_pg:
1256 lines_to_del_to_app.append((ctx_keys, line))
1257
1258 for (ctx_keys, line) in lines_to_del_to_del:
1259 lines_to_del.remove((ctx_keys, line))
1260
1261 for (ctx_keys, line) in lines_to_del_to_app:
1262 lines_to_del.remove((ctx_keys, line))
1263 lines_to_del.append((ctx_keys, line))
1264
1265 return (lines_to_add, lines_to_del)
1266
1267
1268 def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
1269
1270 # Quite possibly the most confusing (while accurate) variable names in history
1271 lines_to_add_to_del = []
1272 lines_to_del_to_del = []
1273
1274 for (ctx_keys, line) in lines_to_del:
1275 deleted = False
1276
1277 # If there is a change in the segment routing block ranges, do it
1278 # in-place, to avoid requesting spurious label chunks which might fail
1279 if line and "segment-routing global-block" in line:
1280 for (add_key, add_line) in lines_to_add:
1281 if (
1282 ctx_keys[0] == add_key[0]
1283 and add_line
1284 and "segment-routing global-block" in add_line
1285 ):
1286 lines_to_del_to_del.append((ctx_keys, line))
1287 break
1288 continue
1289
1290 if ctx_keys[0].startswith("router bgp") and line:
1291
1292 if line.startswith("neighbor "):
1293 """
1294 BGP changed how it displays swpX peers that are part of peer-group. Older
1295 versions of frr would display these on separate lines:
1296 neighbor swp1 interface
1297 neighbor swp1 peer-group FOO
1298
1299 but today we display via a single line
1300 neighbor swp1 interface peer-group FOO
1301
1302 This change confuses frr-reload.py so check to see if we are deleting
1303 neighbor swp1 interface peer-group FOO
1304
1305 and adding
1306 neighbor swp1 interface
1307 neighbor swp1 peer-group FOO
1308
1309 If so then chop the del line and the corresponding add lines
1310 """
1311
1312 re_swpx_int_peergroup = re.search(
1313 "neighbor (\S+) interface peer-group (\S+)", line
1314 )
1315 re_swpx_int_v6only_peergroup = re.search(
1316 "neighbor (\S+) interface v6only peer-group (\S+)", line
1317 )
1318
1319 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
1320 swpx_interface = None
1321 swpx_peergroup = None
1322
1323 if re_swpx_int_peergroup:
1324 swpx = re_swpx_int_peergroup.group(1)
1325 peergroup = re_swpx_int_peergroup.group(2)
1326 swpx_interface = "neighbor %s interface" % swpx
1327 elif re_swpx_int_v6only_peergroup:
1328 swpx = re_swpx_int_v6only_peergroup.group(1)
1329 peergroup = re_swpx_int_v6only_peergroup.group(2)
1330 swpx_interface = "neighbor %s interface v6only" % swpx
1331
1332 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
1333 found_add_swpx_interface = line_exist(
1334 lines_to_add, ctx_keys, swpx_interface
1335 )
1336 found_add_swpx_peergroup = line_exist(
1337 lines_to_add, ctx_keys, swpx_peergroup
1338 )
1339 tmp_ctx_keys = tuple(list(ctx_keys))
1340
1341 if not found_add_swpx_peergroup:
1342 tmp_ctx_keys = list(ctx_keys)
1343 tmp_ctx_keys.append("address-family ipv4 unicast")
1344 tmp_ctx_keys = tuple(tmp_ctx_keys)
1345 found_add_swpx_peergroup = line_exist(
1346 lines_to_add, tmp_ctx_keys, swpx_peergroup
1347 )
1348
1349 if not found_add_swpx_peergroup:
1350 tmp_ctx_keys = list(ctx_keys)
1351 tmp_ctx_keys.append("address-family ipv6 unicast")
1352 tmp_ctx_keys = tuple(tmp_ctx_keys)
1353 found_add_swpx_peergroup = line_exist(
1354 lines_to_add, tmp_ctx_keys, swpx_peergroup
1355 )
1356
1357 if found_add_swpx_interface and found_add_swpx_peergroup:
1358 deleted = True
1359 lines_to_del_to_del.append((ctx_keys, line))
1360 lines_to_add_to_del.append((ctx_keys, swpx_interface))
1361 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
1362
1363 """
1364 Changing the bfd timers on neighbors is allowed without doing
1365 a delete/add process. Since doing a "no neighbor blah bfd ..."
1366 will cause the peer to bounce unnecessarily, just skip the delete
1367 and just do the add.
1368 """
1369 re_nbr_bfd_timers = re.search(
1370 r"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1371 )
1372
1373 if re_nbr_bfd_timers:
1374 nbr = re_nbr_bfd_timers.group(1)
1375 bfd_nbr = "neighbor %s" % nbr
1376 bfd_search_string = bfd_nbr + r" bfd (\S+) (\S+) (\S+)"
1377
1378 for (ctx_keys, add_line) in lines_to_add:
1379 if ctx_keys[0].startswith("router bgp"):
1380 re_add_nbr_bfd_timers = re.search(
1381 bfd_search_string, add_line
1382 )
1383
1384 if re_add_nbr_bfd_timers:
1385 found_add_bfd_nbr = line_exist(
1386 lines_to_add, ctx_keys, bfd_nbr, False
1387 )
1388
1389 if found_add_bfd_nbr:
1390 lines_to_del_to_del.append((ctx_keys, line))
1391
1392 """
1393 We changed how we display the neighbor interface command. Older
1394 versions of frr would display the following:
1395 neighbor swp1 interface
1396 neighbor swp1 remote-as external
1397 neighbor swp1 capability extended-nexthop
1398
1399 but today we display via a single line
1400 neighbor swp1 interface remote-as external
1401
1402 and capability extended-nexthop is no longer needed because we
1403 automatically enable it when the neighbor is of type interface.
1404
1405 This change confuses frr-reload.py so check to see if we are deleting
1406 neighbor swp1 interface remote-as (external|internal|ASNUM)
1407
1408 and adding
1409 neighbor swp1 interface
1410 neighbor swp1 remote-as (external|internal|ASNUM)
1411 neighbor swp1 capability extended-nexthop
1412
1413 If so then chop the del line and the corresponding add lines
1414 """
1415 re_swpx_int_remoteas = re.search(
1416 "neighbor (\S+) interface remote-as (\S+)", line
1417 )
1418 re_swpx_int_v6only_remoteas = re.search(
1419 "neighbor (\S+) interface v6only remote-as (\S+)", line
1420 )
1421
1422 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
1423 swpx_interface = None
1424 swpx_remoteas = None
1425
1426 if re_swpx_int_remoteas:
1427 swpx = re_swpx_int_remoteas.group(1)
1428 remoteas = re_swpx_int_remoteas.group(2)
1429 swpx_interface = "neighbor %s interface" % swpx
1430 elif re_swpx_int_v6only_remoteas:
1431 swpx = re_swpx_int_v6only_remoteas.group(1)
1432 remoteas = re_swpx_int_v6only_remoteas.group(2)
1433 swpx_interface = "neighbor %s interface v6only" % swpx
1434
1435 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
1436 found_add_swpx_interface = line_exist(
1437 lines_to_add, ctx_keys, swpx_interface
1438 )
1439 found_add_swpx_remoteas = line_exist(
1440 lines_to_add, ctx_keys, swpx_remoteas
1441 )
1442 tmp_ctx_keys = tuple(list(ctx_keys))
1443
1444 if found_add_swpx_interface and found_add_swpx_remoteas:
1445 deleted = True
1446 lines_to_del_to_del.append((ctx_keys, line))
1447 lines_to_add_to_del.append((ctx_keys, swpx_interface))
1448 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
1449
1450 """
1451 We made the 'bgp bestpath as-path multipath-relax' command
1452 automatically assume 'no-as-set' since the lack of this option caused
1453 weird routing problems. When the running config is shown in
1454 releases with this change, the no-as-set keyword is not shown as it
1455 is the default. This causes frr-reload to unnecessarily unapply
1456 this option only to apply it back again, causing unnecessary session
1457 resets.
1458 """
1459 if "multipath-relax" in line:
1460 re_asrelax_new = re.search(
1461 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1462 )
1463 old_asrelax_cmd = "bgp bestpath as-path multipath-relax no-as-set"
1464 found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
1465
1466 if re_asrelax_new and found_asrelax_old:
1467 deleted = True
1468 lines_to_del_to_del.append((ctx_keys, line))
1469 lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
1470
1471 """
1472 If we are modifying the BGP table-map we need to avoid a del/add and
1473 instead modify the table-map in place via an add. This is needed to
1474 avoid installing all routes in the RIB the second the 'no table-map'
1475 is issued.
1476 """
1477 if line.startswith("table-map"):
1478 found_table_map = line_exist(lines_to_add, ctx_keys, "table-map", False)
1479
1480 if found_table_map:
1481 lines_to_del_to_del.append((ctx_keys, line))
1482
1483 """
1484 More old-to-new config handling. ip import-table no longer accepts
1485 distance, but we honor the old syntax. But 'show running' shows only
1486 the new syntax. This causes an unnecessary 'no import-table' followed
1487 by the same old 'ip import-table' which causes perturbations in
1488 announced routes leading to traffic blackholes. Fix this issue.
1489 """
1490 re_importtbl = re.search("^ip\s+import-table\s+(\d+)$", ctx_keys[0])
1491 if re_importtbl:
1492 table_num = re_importtbl.group(1)
1493 for ctx in lines_to_add:
1494 if ctx[0][0].startswith("ip import-table %s distance" % table_num):
1495 lines_to_del_to_del.append(
1496 (("ip import-table %s" % table_num,), None)
1497 )
1498 lines_to_add_to_del.append((ctx[0], None))
1499
1500 """
1501 ip/ipv6 prefix-lists and access-lists can be specified without a seq number.
1502 However, the running config always adds 'seq x', where x is a number
1503 incremented by 5 for every element of the prefix/access list.
1504 So, ignore such lines as well. Sample prefix-list and acces-list lines:
1505 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1506 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1507 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1508 access-list FOO seq 5 permit 2.2.2.2/32
1509 ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1510 """
1511 re_acl_pfxlst = re.search(
1512 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1513 ctx_keys[0],
1514 )
1515 if re_acl_pfxlst:
1516 found = False
1517 tmpline = (
1518 re_acl_pfxlst.group(1)
1519 + re_acl_pfxlst.group(2)
1520 + re_acl_pfxlst.group(3)
1521 + re_acl_pfxlst.group(5)
1522 + re_acl_pfxlst.group(6)
1523 )
1524 for ctx in lines_to_add:
1525 if ctx[0][0] == tmpline:
1526 lines_to_del_to_del.append((ctx_keys, None))
1527 lines_to_add_to_del.append(((tmpline,), None))
1528 found = True
1529 """
1530 If prefix-lists or access-lists are being deleted and
1531 not added (see comment above), add command with 'no' to
1532 lines_to_add and remove from lines_to_del to improve
1533 scaling performance.
1534 """
1535 if found is False:
1536 add_cmd = ("no " + ctx_keys[0],)
1537 lines_to_add.append((add_cmd, None))
1538 lines_to_del_to_del.append((ctx_keys, None))
1539
1540 if (
1541 len(ctx_keys) == 3
1542 and ctx_keys[0].startswith("router bgp")
1543 and ctx_keys[1] == "address-family l2vpn evpn"
1544 and ctx_keys[2].startswith("vni")
1545 ):
1546
1547 re_route_target = (
1548 re.search("^route-target import (.*)$", line)
1549 if line is not None
1550 else False
1551 )
1552
1553 if re_route_target:
1554 rt = re_route_target.group(1).strip()
1555 route_target_import_line = line
1556 route_target_export_line = "route-target export %s" % rt
1557 route_target_both_line = "route-target both %s" % rt
1558
1559 found_route_target_export_line = line_exist(
1560 lines_to_del, ctx_keys, route_target_export_line
1561 )
1562 found_route_target_both_line = line_exist(
1563 lines_to_add, ctx_keys, route_target_both_line
1564 )
1565
1566 """
1567 If the running configs has
1568 route-target import 1:1
1569 route-target export 1:1
1570
1571 and the config we are reloading against has
1572 route-target both 1:1
1573
1574 then we can ignore deleting the import/export and ignore adding the 'both'
1575 """
1576 if found_route_target_export_line and found_route_target_both_line:
1577 lines_to_del_to_del.append((ctx_keys, route_target_import_line))
1578 lines_to_del_to_del.append((ctx_keys, route_target_export_line))
1579 lines_to_add_to_del.append((ctx_keys, route_target_both_line))
1580
1581 # Deleting static routes under a vrf can lead to time-outs if each is sent
1582 # as separate vtysh -c commands. Change them from being in lines_to_del and
1583 # put the "no" form in lines_to_add
1584 if ctx_keys[0].startswith("vrf ") and line:
1585 if line.startswith("ip route") or line.startswith("ipv6 route"):
1586 add_cmd = "no " + line
1587 lines_to_add.append((ctx_keys, add_cmd))
1588 lines_to_del_to_del.append((ctx_keys, line))
1589
1590 if not deleted:
1591 found_add_line = line_exist(lines_to_add, ctx_keys, line)
1592
1593 if found_add_line:
1594 lines_to_del_to_del.append((ctx_keys, line))
1595 lines_to_add_to_del.append((ctx_keys, line))
1596 else:
1597 """
1598 We have commands that used to be displayed in the global part
1599 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1600
1601 # old way
1602 router bgp 64900
1603 neighbor ISL advertisement-interval 0
1604
1605 vs.
1606
1607 # new way
1608 router bgp 64900
1609 address-family ipv4 unicast
1610 neighbor ISL advertisement-interval 0
1611
1612 Look to see if we are deleting it in one format just to add it back in the other
1613 """
1614 if (
1615 ctx_keys[0].startswith("router bgp")
1616 and len(ctx_keys) > 1
1617 and ctx_keys[1] == "address-family ipv4 unicast"
1618 ):
1619 tmp_ctx_keys = list(ctx_keys)[:-1]
1620 tmp_ctx_keys = tuple(tmp_ctx_keys)
1621
1622 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
1623
1624 if found_add_line:
1625 lines_to_del_to_del.append((ctx_keys, line))
1626 lines_to_add_to_del.append((tmp_ctx_keys, line))
1627
1628 for (ctx_keys, line) in lines_to_del_to_del:
1629 lines_to_del.remove((ctx_keys, line))
1630
1631 for (ctx_keys, line) in lines_to_add_to_del:
1632 lines_to_add.remove((ctx_keys, line))
1633
1634 return (lines_to_add, lines_to_del)
1635
1636
1637 def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
1638 """
1639 There are certain commands that cannot be removed. Remove
1640 those commands from lines_to_del.
1641 """
1642 lines_to_del_to_del = []
1643
1644 for (ctx_keys, line) in lines_to_del:
1645
1646 if (
1647 ctx_keys[0].startswith("frr version")
1648 or ctx_keys[0].startswith("frr defaults")
1649 or ctx_keys[0].startswith("username")
1650 or ctx_keys[0].startswith("password")
1651 or ctx_keys[0].startswith("line vty")
1652 or
1653 # This is technically "no"able but if we did so frr-reload would
1654 # stop working so do not let the user shoot themselves in the foot
1655 # by removing this.
1656 ctx_keys[0].startswith("service integrated-vtysh-config")
1657 ):
1658
1659 log.info('"%s" cannot be removed' % (ctx_keys[-1],))
1660 lines_to_del_to_del.append((ctx_keys, line))
1661
1662 for (ctx_keys, line) in lines_to_del_to_del:
1663 lines_to_del.remove((ctx_keys, line))
1664
1665 return (lines_to_add, lines_to_del)
1666
1667
1668 def compare_context_objects(newconf, running):
1669 """
1670 Create a context diff for the two specified contexts
1671 """
1672
1673 # Compare the two Config objects to find the lines that we need to add/del
1674 lines_to_add = []
1675 lines_to_del = []
1676 pollist_to_del = []
1677 seglist_to_del = []
1678 pceconf_to_del = []
1679 pcclist_to_del = []
1680 candidates_to_add = []
1681 delete_bgpd = False
1682
1683 # Find contexts that are in newconf but not in running
1684 # Find contexts that are in running but not in newconf
1685 for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
1686
1687 if running_ctx_keys not in newconf.contexts:
1688
1689 # We check that the len is 1 here so that we only look at ('router bgp 10')
1690 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1691 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1692 # running but not in newconf.
1693 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
1694 delete_bgpd = True
1695 lines_to_del.append((running_ctx_keys, None))
1696
1697 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1698 elif running_ctx_keys[0].startswith("interface") or running_ctx_keys[
1699 0
1700 ].startswith("vrf"):
1701 for line in running_ctx.lines:
1702 lines_to_del.append((running_ctx_keys, line))
1703
1704 # If this is an address-family under 'router bgp' and we are already deleting the
1705 # entire 'router bgp' context then ignore this sub-context
1706 elif (
1707 "router bgp" in running_ctx_keys[0]
1708 and len(running_ctx_keys) > 1
1709 and delete_bgpd
1710 ):
1711 continue
1712
1713 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1714 elif (
1715 "router bgp" in running_ctx_keys[0]
1716 and len(running_ctx_keys) > 2
1717 and running_ctx_keys[1].startswith("address-family l2vpn evpn")
1718 and running_ctx_keys[2].startswith("vni ")
1719 ):
1720 lines_to_del.append((running_ctx_keys, None))
1721
1722 elif (
1723 "router bgp" in running_ctx_keys[0]
1724 and len(running_ctx_keys) > 1
1725 and running_ctx_keys[1].startswith("address-family")
1726 ):
1727 # There's no 'no address-family' support and so we have to
1728 # delete each line individually again
1729 for line in running_ctx.lines:
1730 lines_to_del.append((running_ctx_keys, line))
1731
1732 # Some commands can happen at higher counts that make
1733 # doing vtysh -c inefficient (and can time out.) For
1734 # these commands, instead of adding them to lines_to_del,
1735 # add the "no " version to lines_to_add.
1736 elif running_ctx_keys[0].startswith("ip route") or running_ctx_keys[
1737 0
1738 ].startswith("ipv6 route"):
1739 add_cmd = ("no " + running_ctx_keys[0],)
1740 lines_to_add.append((add_cmd, None))
1741
1742 # if this an interface sub-subcontext in an address-family block in ldpd and
1743 # we are already deleting the whole context, then ignore this
1744 elif (
1745 len(running_ctx_keys) > 2
1746 and running_ctx_keys[0].startswith("mpls ldp")
1747 and running_ctx_keys[1].startswith("address-family")
1748 and (running_ctx_keys[:2], None) in lines_to_del
1749 ):
1750 continue
1751
1752 # same thing for a pseudowire sub-context inside an l2vpn context
1753 elif (
1754 len(running_ctx_keys) > 1
1755 and running_ctx_keys[0].startswith("l2vpn")
1756 and running_ctx_keys[1].startswith("member pseudowire")
1757 and (running_ctx_keys[:1], None) in lines_to_del
1758 ):
1759 continue
1760
1761 # Segment routing and traffic engineering never need to be deleted
1762 elif (
1763 running_ctx_keys[0].startswith("segment-routing")
1764 and len(running_ctx_keys) < 3
1765 ):
1766 continue
1767
1768 # Neither the pcep command
1769 elif (
1770 len(running_ctx_keys) == 3
1771 and running_ctx_keys[0].startswith("segment-routing")
1772 and running_ctx_keys[2].startswith("pcep")
1773 ):
1774 continue
1775
1776 # Segment lists can only be deleted after we removed all the candidate paths that
1777 # use them, so add them to a separate array that is going to be appended at the end
1778 elif (
1779 len(running_ctx_keys) == 3
1780 and running_ctx_keys[0].startswith("segment-routing")
1781 and running_ctx_keys[2].startswith("segment-list")
1782 ):
1783 seglist_to_del.append((running_ctx_keys, None))
1784
1785 # Policies must be deleted after there candidate path, to be sure
1786 # we add them to a separate array that is going to be appended at the end
1787 elif (
1788 len(running_ctx_keys) == 3
1789 and running_ctx_keys[0].startswith("segment-routing")
1790 and running_ctx_keys[2].startswith("policy")
1791 ):
1792 pollist_to_del.append((running_ctx_keys, None))
1793
1794 # pce-config must be deleted after the pce, to be sure we add them
1795 # to a separate array that is going to be appended at the end
1796 elif (
1797 len(running_ctx_keys) >= 4
1798 and running_ctx_keys[0].startswith("segment-routing")
1799 and running_ctx_keys[3].startswith("pce-config")
1800 ):
1801 pceconf_to_del.append((running_ctx_keys, None))
1802
1803 # pcc must be deleted after the pce and pce-config too
1804 elif (
1805 len(running_ctx_keys) >= 4
1806 and running_ctx_keys[0].startswith("segment-routing")
1807 and running_ctx_keys[3].startswith("pcc")
1808 ):
1809 pcclist_to_del.append((running_ctx_keys, None))
1810
1811 # Non-global context
1812 elif running_ctx_keys and not any(
1813 "address-family" in key for key in running_ctx_keys
1814 ):
1815 lines_to_del.append((running_ctx_keys, None))
1816
1817 elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys):
1818 lines_to_del.append((running_ctx_keys, None))
1819
1820 # Global context
1821 else:
1822 for line in running_ctx.lines:
1823 lines_to_del.append((running_ctx_keys, line))
1824
1825 # if we have some policies commands to delete, append them to lines_to_del
1826 if len(pollist_to_del) > 0:
1827 lines_to_del.extend(pollist_to_del)
1828
1829 # if we have some segment list commands to delete, append them to lines_to_del
1830 if len(seglist_to_del) > 0:
1831 lines_to_del.extend(seglist_to_del)
1832
1833 # if we have some pce list commands to delete, append them to lines_to_del
1834 if len(pceconf_to_del) > 0:
1835 lines_to_del.extend(pceconf_to_del)
1836
1837 # if we have some pcc list commands to delete, append them to lines_to_del
1838 if len(pcclist_to_del) > 0:
1839 lines_to_del.extend(pcclist_to_del)
1840
1841 # Find the lines within each context to add
1842 # Find the lines within each context to del
1843 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
1844
1845 if newconf_ctx_keys in running.contexts:
1846 running_ctx = running.contexts[newconf_ctx_keys]
1847
1848 for line in newconf_ctx.lines:
1849 if line not in running_ctx.dlines:
1850
1851 # candidate paths can only be added after the policy and segment list,
1852 # so add them to a separate array that is going to be appended at the end
1853 if (
1854 len(newconf_ctx_keys) == 3
1855 and newconf_ctx_keys[0].startswith("segment-routing")
1856 and newconf_ctx_keys[2].startswith("policy ")
1857 and line.startswith("candidate-path ")
1858 ):
1859 candidates_to_add.append((newconf_ctx_keys, line))
1860
1861 else:
1862 lines_to_add.append((newconf_ctx_keys, line))
1863
1864 for line in running_ctx.lines:
1865 if line not in newconf_ctx.dlines:
1866 lines_to_del.append((newconf_ctx_keys, line))
1867
1868 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
1869
1870 if newconf_ctx_keys not in running.contexts:
1871
1872 # candidate paths can only be added after the policy and segment list,
1873 # so add them to a separate array that is going to be appended at the end
1874 if (
1875 len(newconf_ctx_keys) == 4
1876 and newconf_ctx_keys[0].startswith("segment-routing")
1877 and newconf_ctx_keys[3].startswith("candidate-path")
1878 ):
1879 candidates_to_add.append((newconf_ctx_keys, None))
1880 for line in newconf_ctx.lines:
1881 candidates_to_add.append((newconf_ctx_keys, line))
1882
1883 else:
1884 lines_to_add.append((newconf_ctx_keys, None))
1885
1886 for line in newconf_ctx.lines:
1887 lines_to_add.append((newconf_ctx_keys, line))
1888
1889 # if we have some candidate paths commands to add, append them to lines_to_add
1890 if len(candidates_to_add) > 0:
1891 lines_to_add.extend(candidates_to_add)
1892
1893 (lines_to_add, lines_to_del) = check_for_exit_vrf(lines_to_add, lines_to_del)
1894 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(
1895 lines_to_add, lines_to_del
1896 )
1897 (lines_to_add, lines_to_del) = delete_move_lines(lines_to_add, lines_to_del)
1898 (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(
1899 lines_to_add, lines_to_del
1900 )
1901
1902 return (lines_to_add, lines_to_del)
1903
1904
1905 if __name__ == "__main__":
1906 # Command line options
1907 parser = argparse.ArgumentParser(
1908 description="Dynamically apply diff in frr configs"
1909 )
1910 parser.add_argument(
1911 "--input", help='Read running config from file instead of "show running"'
1912 )
1913 group = parser.add_mutually_exclusive_group(required=True)
1914 group.add_argument(
1915 "--reload", action="store_true", help="Apply the deltas", default=False
1916 )
1917 group.add_argument(
1918 "--test", action="store_true", help="Show the deltas", default=False
1919 )
1920 level_group = parser.add_mutually_exclusive_group()
1921 level_group.add_argument(
1922 "--debug",
1923 action="store_true",
1924 help="Enable debugs (synonym for --log-level=debug)",
1925 default=False,
1926 )
1927 level_group.add_argument(
1928 "--log-level",
1929 help="Log level",
1930 default="info",
1931 choices=("critical", "error", "warning", "info", "debug"),
1932 )
1933 parser.add_argument(
1934 "--stdout", action="store_true", help="Log to STDOUT", default=False
1935 )
1936 parser.add_argument(
1937 "--pathspace",
1938 "-N",
1939 metavar="NAME",
1940 help="Reload specified path/namespace",
1941 default=None,
1942 )
1943 parser.add_argument("filename", help="Location of new frr config file")
1944 parser.add_argument(
1945 "--overwrite",
1946 action="store_true",
1947 help="Overwrite frr.conf with running config output",
1948 default=False,
1949 )
1950 parser.add_argument(
1951 "--bindir", help="path to the vtysh executable", default="/usr/bin"
1952 )
1953 parser.add_argument(
1954 "--confdir", help="path to the daemon config files", default="/etc/frr"
1955 )
1956 parser.add_argument(
1957 "--rundir", help="path for the temp config file", default="/var/run/frr"
1958 )
1959 parser.add_argument(
1960 "--vty_socket",
1961 help="socket to be used by vtysh to connect to the daemons",
1962 default=None,
1963 )
1964 parser.add_argument(
1965 "--daemon", help="daemon for which want to replace the config", default=""
1966 )
1967
1968 args = parser.parse_args()
1969
1970 # Logging
1971 # For --test log to stdout
1972 # For --reload log to /var/log/frr/frr-reload.log
1973 if args.test or args.stdout:
1974 logging.basicConfig(format="%(asctime)s %(levelname)5s: %(message)s")
1975
1976 # Color the errors and warnings in red
1977 logging.addLevelName(
1978 logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR)
1979 )
1980 logging.addLevelName(
1981 logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING)
1982 )
1983
1984 elif args.reload:
1985 if not os.path.isdir("/var/log/frr/"):
1986 os.makedirs("/var/log/frr/")
1987
1988 logging.basicConfig(
1989 filename="/var/log/frr/frr-reload.log",
1990 format="%(asctime)s %(levelname)5s: %(message)s",
1991 )
1992
1993 # argparse should prevent this from happening but just to be safe...
1994 else:
1995 raise Exception("Must specify --reload or --test")
1996 log = logging.getLogger(__name__)
1997
1998 if args.debug:
1999 log.setLevel(logging.DEBUG)
2000 else:
2001 log.setLevel(args.log_level.upper())
2002
2003 if args.reload and not args.stdout:
2004 # Additionally send errors and above to STDOUT, with no metadata,
2005 # when we are logging to a file. This specifically does not follow
2006 # args.log_level, and is analagous to behaviour in earlier versions
2007 # which additionally logged most errors using print().
2008
2009 stdout_hdlr = logging.StreamHandler(sys.stdout)
2010 stdout_hdlr.setLevel(logging.ERROR)
2011 stdout_hdlr.setFormatter(logging.Formatter())
2012 log.addHandler(stdout_hdlr)
2013
2014 # Verify the new config file is valid
2015 if not os.path.isfile(args.filename):
2016 log.error("Filename %s does not exist" % args.filename)
2017 sys.exit(1)
2018
2019 if not os.path.getsize(args.filename):
2020 log.error("Filename %s is an empty file" % args.filename)
2021 sys.exit(1)
2022
2023 # Verify that confdir is correct
2024 if not os.path.isdir(args.confdir):
2025 log.error("Confdir %s is not a valid path" % args.confdir)
2026 sys.exit(1)
2027
2028 # Verify that bindir is correct
2029 if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + "/vtysh"):
2030 log.error("Bindir %s is not a valid path to vtysh" % args.bindir)
2031 sys.exit(1)
2032
2033 # verify that the vty_socket, if specified, is valid
2034 if args.vty_socket and not os.path.isdir(args.vty_socket):
2035 log.error("vty_socket %s is not a valid path" % args.vty_socket)
2036 sys.exit(1)
2037
2038 # verify that the daemon, if specified, is valid
2039 if args.daemon and args.daemon not in [
2040 "zebra",
2041 "bgpd",
2042 "fabricd",
2043 "isisd",
2044 "ospf6d",
2045 "ospfd",
2046 "pbrd",
2047 "pimd",
2048 "ripd",
2049 "ripngd",
2050 "sharpd",
2051 "staticd",
2052 "vrrpd",
2053 "ldpd",
2054 "pathd",
2055 "bfdd",
2056 ]:
2057 msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon
2058 print(msg)
2059 log.error(msg)
2060 sys.exit(1)
2061
2062 vtysh = Vtysh(args.bindir, args.confdir, args.vty_socket, args.pathspace)
2063
2064 # Verify that 'service integrated-vtysh-config' is configured
2065 if args.pathspace:
2066 vtysh_filename = args.confdir + "/" + args.pathspace + "/vtysh.conf"
2067 else:
2068 vtysh_filename = args.confdir + "/vtysh.conf"
2069 service_integrated_vtysh_config = True
2070
2071 if os.path.isfile(vtysh_filename):
2072 with open(vtysh_filename, "r") as fh:
2073 for line in fh.readlines():
2074 line = line.strip()
2075
2076 if line == "no service integrated-vtysh-config":
2077 service_integrated_vtysh_config = False
2078 break
2079
2080 if not service_integrated_vtysh_config and not args.daemon:
2081 log.error(
2082 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
2083 )
2084 sys.exit(1)
2085
2086 log.info('Called via "%s"', str(args))
2087
2088 # Create a Config object from the config generated by newconf
2089 newconf = Config(vtysh)
2090 try:
2091 newconf.load_from_file(args.filename)
2092 reload_ok = True
2093 except VtyshException as ve:
2094 log.error("vtysh failed to process new configuration: {}".format(ve))
2095 reload_ok = False
2096
2097 if args.test:
2098
2099 # Create a Config object from the running config
2100 running = Config(vtysh)
2101
2102 if args.input:
2103 running.load_from_file(args.input)
2104 else:
2105 running.load_from_show_running(args.daemon)
2106
2107 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2108 lines_to_configure = []
2109
2110 if lines_to_del:
2111 print("\nLines To Delete")
2112 print("===============")
2113
2114 for (ctx_keys, line) in lines_to_del:
2115
2116 if line == "!":
2117 continue
2118
2119 cmd = "\n".join(lines_to_config(ctx_keys, line, True))
2120 lines_to_configure.append(cmd)
2121 print(cmd)
2122
2123 if lines_to_add:
2124 print("\nLines To Add")
2125 print("============")
2126
2127 for (ctx_keys, line) in lines_to_add:
2128
2129 if line == "!":
2130 continue
2131
2132 cmd = "\n".join(lines_to_config(ctx_keys, line, False))
2133 lines_to_configure.append(cmd)
2134 print(cmd)
2135
2136 elif args.reload:
2137
2138 # We will not be able to do anything, go ahead and exit(1)
2139 if not vtysh.is_config_available():
2140 sys.exit(1)
2141
2142 log.debug("New Frr Config\n%s", newconf.get_lines())
2143
2144 # This looks a little odd but we have to do this twice...here is why
2145 # If the user had this running bgp config:
2146 #
2147 # router bgp 10
2148 # neighbor 1.1.1.1 remote-as 50
2149 # neighbor 1.1.1.1 route-map FOO out
2150 #
2151 # and this config in the newconf config file
2152 #
2153 # router bgp 10
2154 # neighbor 1.1.1.1 remote-as 999
2155 # neighbor 1.1.1.1 route-map FOO out
2156 #
2157 #
2158 # Then the script will do
2159 # - no neighbor 1.1.1.1 remote-as 50
2160 # - neighbor 1.1.1.1 remote-as 999
2161 #
2162 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
2163 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
2164 # configs again to put this line back.
2165
2166 # There are many keywords in FRR that can only appear one time under
2167 # a context, take "bgp router-id" for example. If the config that we are
2168 # reloading against has the following:
2169 #
2170 # router bgp 10
2171 # bgp router-id 1.1.1.1
2172 # bgp router-id 2.2.2.2
2173 #
2174 # The final config needs to contain "bgp router-id 2.2.2.2". On the
2175 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
2176 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
2177 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
2178 # second pass to include all of the "adds" from the first pass.
2179 lines_to_add_first_pass = []
2180
2181 for x in range(2):
2182 running = Config(vtysh)
2183 running.load_from_show_running(args.daemon)
2184 log.debug("Running Frr Config (Pass #%d)\n%s", x, running.get_lines())
2185
2186 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2187
2188 if x == 0:
2189 lines_to_add_first_pass = lines_to_add
2190 else:
2191 lines_to_add.extend(lines_to_add_first_pass)
2192
2193 # Only do deletes on the first pass. The reason being if we
2194 # configure a bgp neighbor via "neighbor swp1 interface" FRR
2195 # will automatically add:
2196 #
2197 # interface swp1
2198 # ipv6 nd ra-interval 10
2199 # no ipv6 nd suppress-ra
2200 # !
2201 #
2202 # but those lines aren't in the config we are reloading against so
2203 # on the 2nd pass they will show up in lines_to_del. This could
2204 # apply to other scenarios as well where configuring FOO adds BAR
2205 # to the config.
2206 if lines_to_del and x == 0:
2207 for (ctx_keys, line) in lines_to_del:
2208
2209 if line == "!":
2210 continue
2211
2212 # 'no' commands are tricky, we can't just put them in a file and
2213 # vtysh -f that file. See the next comment for an explanation
2214 # of their quirks
2215 cmd = lines_to_config(ctx_keys, line, True)
2216 original_cmd = cmd
2217
2218 # Some commands in frr are picky about taking a "no" of the entire line.
2219 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
2220 # only the beginning. If we hit one of these command an exception will be
2221 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2222 #
2223 # Example:
2224 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2225 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2226 # % Unknown command.
2227 # frr(config-if)# no ip ospf authentication message-digest
2228 # % Unknown command.
2229 # frr(config-if)# no ip ospf authentication
2230 # frr(config-if)#
2231
2232 stdouts = []
2233 while True:
2234 try:
2235 vtysh(["configure"] + cmd, stdouts)
2236
2237 except VtyshException:
2238
2239 # - Pull the last entry from cmd (this would be
2240 # 'no ip ospf authentication message-digest 1.1.1.1' in
2241 # our example above
2242 # - Split that last entry by whitespace and drop the last word
2243 log.info("Failed to execute %s", " ".join(cmd))
2244 last_arg = cmd[-1].split(" ")
2245
2246 if len(last_arg) <= 2:
2247 log.error(
2248 '"%s" we failed to remove this command',
2249 " -- ".join(original_cmd),
2250 )
2251 # Log first error msg for original_cmd
2252 if stdouts:
2253 log.error(stdouts[0])
2254 reload_ok = False
2255 break
2256
2257 new_last_arg = last_arg[0:-1]
2258 cmd[-1] = " ".join(new_last_arg)
2259 else:
2260 log.info('Executed "%s"', " ".join(cmd))
2261 break
2262
2263 if lines_to_add:
2264 lines_to_configure = []
2265
2266 for (ctx_keys, line) in lines_to_add:
2267
2268 if line == "!":
2269 continue
2270
2271 # Don't run "no" commands twice since they can error
2272 # out the second time due to first deletion
2273 if x == 1 and ctx_keys[0].startswith("no "):
2274 continue
2275
2276 cmd = "\n".join(lines_to_config(ctx_keys, line, False)) + "\n"
2277 lines_to_configure.append(cmd)
2278
2279 if lines_to_configure:
2280 random_string = "".join(
2281 random.SystemRandom().choice(
2282 string.ascii_uppercase + string.digits
2283 )
2284 for _ in range(6)
2285 )
2286
2287 filename = args.rundir + "/reload-%s.txt" % random_string
2288 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
2289
2290 with open(filename, "w") as fh:
2291 for line in lines_to_configure:
2292 fh.write(line + "\n")
2293
2294 try:
2295 vtysh.exec_file(filename)
2296 except VtyshException as e:
2297 log.warning("frr-reload.py failed due to\n%s" % e.args)
2298 reload_ok = False
2299 os.unlink(filename)
2300
2301 # Make these changes persistent
2302 target = str(args.confdir + "/frr.conf")
2303 if args.overwrite or (not args.daemon and args.filename != target):
2304 vtysh("write")
2305
2306 if not reload_ok:
2307 sys.exit(1)