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