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