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