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