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