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