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