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