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