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