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