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