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