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