]> git.proxmox.com Git - mirror_frr.git/blame - tools/frr-reload.py
zebra: debug flags for MAC-IP sync
[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
1c64265f 42try:
43 from ipaddress import IPv6Address, ip_network
44except ImportError:
45 from ipaddr import IPv6Address, IPNetwork
4a2587c6
DW
46from pprint import pformat
47
1c64265f 48try:
49 dict.iteritems
50except AttributeError:
51 # Python 3
52 def iteritems(d):
53 return iter(d.items())
54else:
55 # Python 2
56 def iteritems(d):
57 return d.iteritems()
2fc76430 58
a782e613
DW
59log = logging.getLogger(__name__)
60
61
663ece2f 62class VtyshException(Exception):
276887bb
DW
63 pass
64
663ece2f 65class Vtysh(object):
a0a7dead 66 def __init__(self, bindir=None, confdir=None, sockdir=None, pathspace=None):
663ece2f
DL
67 self.bindir = bindir
68 self.confdir = confdir
a0a7dead 69 self.pathspace = pathspace
663ece2f
DL
70 self.common_args = [os.path.join(bindir or '', 'vtysh')]
71 if confdir:
72 self.common_args.extend(['--config_dir', confdir])
fa18c6bb
DL
73 if sockdir:
74 self.common_args.extend(['--vty_socket', sockdir])
a0a7dead
DL
75 if pathspace:
76 self.common_args.extend(['-N', pathspace])
663ece2f
DL
77
78 def _call(self, args, stdin=None, stdout=None, stderr=None):
79 kwargs = {}
80 if stdin is not None:
81 kwargs['stdin'] = stdin
82 if stdout is not None:
83 kwargs['stdout'] = stdout
84 if stderr is not None:
85 kwargs['stderr'] = stderr
86 return subprocess.Popen(self.common_args + args, **kwargs)
87
88 def _call_cmd(self, command, stdin=None, stdout=None, stderr=None):
89 if isinstance(command, list):
90 args = [item for sub in command for item in ['-c', sub]]
91 else:
92 args = ['-c', command]
93 return self._call(args, stdin, stdout, stderr)
94
95 def __call__(self, command):
96 """
97 Call a CLI command (e.g. "show running-config")
98
99 Output text is automatically redirected, decoded and returned.
100 Multiple commands may be passed as list.
101 """
102 proc = self._call_cmd(command, stdout=subprocess.PIPE)
103 stdout, stderr = proc.communicate()
104 if proc.wait() != 0:
105 raise VtyshException('vtysh returned status %d for command "%s"'
106 % (proc.returncode, command))
107 return stdout.decode('UTF-8')
108
109 def is_config_available(self):
110 """
111 Return False if no frr daemon is running or some other vtysh session is
112 in 'configuration terminal' mode which will prevent us from making any
113 configuration changes.
114 """
115
116 output = self('configure')
117
118 if 'VTY configuration is locked by other VTY' in output:
119 print(output)
120 log.error("vtysh 'configure' returned\n%s\n" % (output))
121 return False
122
123 return True
124
125 def exec_file(self, filename):
126 child = self._call(['-f', filename])
127 if child.wait() != 0:
128 raise VtyshException('vtysh (exec file) exited with status %d'
129 % (child.returncode))
130
131 def mark_file(self, filename, stdin=None):
132 kwargs = {}
133 if stdin is not None:
134 kwargs['stdin'] = stdin
135
136 child = self._call(['-m', '-f', filename],
137 stdout=subprocess.PIPE, **kwargs)
138 try:
139 stdout, stderr = child.communicate()
140 except subprocess.TimeoutExpired:
141 child.kill()
142 stdout, stderr = proc.communicate()
143 raise VtyshException('vtysh call timed out!')
144
145 if child.wait() != 0:
146 raise VtyshException('vtysh (mark file) exited with status %d:\n%s'
147 % (child.returncode, stderr))
148
149 return stdout.decode('UTF-8')
150
151 def mark_show_run(self, daemon = None):
7e7fedcb 152 cmd = 'show running-config'
663ece2f
DL
153 if daemon:
154 cmd += ' %s' % daemon
7e7fedcb 155 cmd += ' no-header'
663ece2f
DL
156 show_run = self._call_cmd(cmd, stdout=subprocess.PIPE)
157 mark = self._call(['-m', '-f', '-'], stdin=show_run.stdout, stdout=subprocess.PIPE)
158
159 show_run.wait()
160 stdout, stderr = mark.communicate()
161 mark.wait()
162
163 if show_run.returncode != 0:
164 raise VtyshException('vtysh (show running-config) exited with status %d:'
165 % (show_run.returncode))
166 if mark.returncode != 0:
167 raise VtyshException('vtysh (mark running-config) exited with status %d'
168 % (mark.returncode))
169
170 return stdout.decode('UTF-8')
276887bb 171
2fc76430 172class Context(object):
4a2587c6 173
2fc76430 174 """
d8e4c438 175 A Context object represents a section of frr configuration such as:
2fc76430
DS
176!
177interface swp3
178 description swp3 -> r8's swp1
179 ipv6 nd suppress-ra
180 link-detect
181!
182
183or a single line context object such as this:
184
185ip forwarding
186
187 """
188
189 def __init__(self, keys, lines):
190 self.keys = keys
191 self.lines = lines
192
193 # Keep a dictionary of the lines, this is to make it easy to tell if a
194 # line exists in this Context
195 self.dlines = OrderedDict()
196
197 for ligne in lines:
198 self.dlines[ligne] = True
199
200 def add_lines(self, lines):
201 """
202 Add lines to specified context
203 """
204
205 self.lines.extend(lines)
206
207 for ligne in lines:
208 self.dlines[ligne] = True
209
210
211class Config(object):
4a2587c6 212
2fc76430 213 """
d8e4c438 214 A frr configuration is stored in a Config object. A Config object
2fc76430
DS
215 contains a dictionary of Context objects where the Context keys
216 ('router ospf' for example) are our dictionary key.
217 """
218
663ece2f 219 def __init__(self, vtysh):
2fc76430
DS
220 self.lines = []
221 self.contexts = OrderedDict()
663ece2f 222 self.vtysh = vtysh
2fc76430 223
663ece2f 224 def load_from_file(self, filename):
2fc76430
DS
225 """
226 Read configuration from specified file and slurp it into internal memory
227 The internal representation has been marked appropriately by passing it
228 through vtysh with the -m parameter
229 """
a782e613 230 log.info('Loading Config object from file %s', filename)
2fc76430 231
663ece2f
DL
232 file_output = self.vtysh.mark_file(filename)
233
234 for line in file_output.split('\n'):
2fc76430 235 line = line.strip()
89cca49b
DW
236
237 # Compress duplicate whitespaces
238 line = ' '.join(line.split())
239
e238920d 240 if ":" in line and not "ipv6 add":
2fc76430
DS
241 qv6_line = get_normalized_ipv6_line(line)
242 self.lines.append(qv6_line)
243 else:
244 self.lines.append(line)
245
246 self.load_contexts()
247
663ece2f 248 def load_from_show_running(self, daemon):
2fc76430
DS
249 """
250 Read running configuration and slurp it into internal memory
251 The internal representation has been marked appropriately by passing it
252 through vtysh with the -m parameter
253 """
a782e613 254 log.info('Loading Config object from vtysh show running')
2fc76430 255
663ece2f
DL
256 config_text = self.vtysh.mark_show_run(daemon)
257
258 for line in config_text.split('\n'):
2fc76430
DS
259 line = line.strip()
260
261 if (line == 'Building configuration...' or
262 line == 'Current configuration:' or
4a2587c6 263 not line):
2fc76430
DS
264 continue
265
266 self.lines.append(line)
267
268 self.load_contexts()
269
270 def get_lines(self):
271 """
272 Return the lines read in from the configuration
273 """
274
275 return '\n'.join(self.lines)
276
277 def get_contexts(self):
278 """
279 Return the parsed context as strings for display, log etc.
280 """
281
1c64265f 282 for (_, ctx) in sorted(iteritems(self.contexts)):
283 print(str(ctx) + '\n')
2fc76430
DS
284
285 def save_contexts(self, key, lines):
286 """
287 Save the provided key and lines as a context
288 """
289
290 if not key:
291 return
292
bb972e44
DD
293 '''
294 IP addresses specified in "network" statements, "ip prefix-lists"
295 etc. can differ in the host part of the specification the user
296 provides and what the running config displays. For example, user
297 can specify 11.1.1.1/24, and the running config displays this as
298 11.1.1.0/24. Ensure we don't do a needless operation for such
299 lines. IS-IS & OSPFv3 have no "network" support.
300 '''
4d760f42 301 re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0])
bb972e44
DD
302 if re_key_rt:
303 addr = re_key_rt.group(2)
304 if '/' in addr:
305 try:
1c64265f 306 if 'ipaddress' not in sys.modules:
307 newaddr = IPNetwork(addr)
308 key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
309 newaddr.network,
310 newaddr.prefixlen,
311 re_key_rt.group(3))
312 else:
313 newaddr = ip_network(addr, strict=False)
314 key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
315 str(newaddr.network_address),
316 newaddr.prefixlen,
317 re_key_rt.group(3))
bb972e44
DD
318 except ValueError:
319 pass
320
321 re_key_rt = re.match(
322 r'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
323 key[0]
324 )
325 if re_key_rt:
326 addr = re_key_rt.group(4)
327 if '/' in addr:
328 try:
1c64265f 329 if 'ipaddress' not in sys.modules:
330 newaddr = '%s/%s' % (IPNetwork(addr).network,
331 IPNetwork(addr).prefixlen)
332 else:
333 network_addr = ip_network(addr, strict=False)
334 newaddr = '%s/%s' % (str(network_addr.network_address),
335 network_addr.prefixlen)
bb972e44
DD
336 except ValueError:
337 newaddr = addr
0845b872
DD
338 else:
339 newaddr = addr
bb972e44
DD
340
341 legestr = re_key_rt.group(5)
342 re_lege = re.search(r'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr)
343 if re_lege:
344 legestr = '%sge %s le %s%s' % (re_lege.group(1),
345 re_lege.group(3),
346 re_lege.group(2),
347 re_lege.group(4))
348 re_lege = re.search(r'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr)
349
350 if (re_lege and ((re_key_rt.group(1) == "ip" and
351 re_lege.group(3) == "32") or
352 (re_key_rt.group(1) == "ipv6" and
353 re_lege.group(3) == "128"))):
354 legestr = '%sge %s%s' % (re_lege.group(1),
355 re_lege.group(2),
356 re_lege.group(4))
357
358 key[0] = '%s prefix-list%s%s %s%s' % (re_key_rt.group(1),
359 re_key_rt.group(2),
360 re_key_rt.group(3),
361 newaddr,
362 legestr)
363
364 if lines and key[0].startswith('router bgp'):
365 newlines = []
366 for line in lines:
367 re_net = re.match(r'network\s+([A-Fa-f:.0-9/]+)(.*)$', line)
368 if re_net:
369 addr = re_net.group(1)
370 if '/' not in addr and key[0].startswith('router bgp'):
371 # This is most likely an error because with no
372 # prefixlen, BGP treats the prefixlen as 8
373 addr = addr + '/8'
374
375 try:
1c64265f 376 if 'ipaddress' not in sys.modules:
377 newaddr = IPNetwork(addr)
378 line = 'network %s/%s %s' % (newaddr.network,
379 newaddr.prefixlen,
380 re_net.group(2))
381 else:
382 network_addr = ip_network(addr, strict=False)
383 line = 'network %s/%s %s' % (str(network_addr.network_address),
384 network_addr.prefixlen,
385 re_net.group(2))
bb972e44 386 newlines.append(line)
0845b872 387 except ValueError:
bb972e44
DD
388 # Really this should be an error. Whats a network
389 # without an IP Address following it ?
390 newlines.append(line)
391 else:
392 newlines.append(line)
393 lines = newlines
394
395 '''
396 More fixups in user specification and what running config shows.
348135a5 397 "null0" in routes must be replaced by Null0.
bb972e44
DD
398 '''
399 if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and
348135a5 400 'null0' in key[0]):
bb972e44 401 key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0])
bb972e44 402
2fc76430
DS
403 if lines:
404 if tuple(key) not in self.contexts:
405 ctx = Context(tuple(key), lines)
406 self.contexts[tuple(key)] = ctx
407 else:
408 ctx = self.contexts[tuple(key)]
409 ctx.add_lines(lines)
410
411 else:
412 if tuple(key) not in self.contexts:
413 ctx = Context(tuple(key), [])
414 self.contexts[tuple(key)] = ctx
415
416 def load_contexts(self):
417 """
418 Parse the configuration and create contexts for each appropriate block
419 """
420
421 current_context_lines = []
422 ctx_keys = []
423
424 '''
425 The end of a context is flagged via the 'end' keyword:
426
427!
428interface swp52
429 ipv6 nd suppress-ra
430 link-detect
431!
432end
433router bgp 10
434 bgp router-id 10.0.0.1
435 bgp log-neighbor-changes
436 no bgp default ipv4-unicast
437 neighbor EBGP peer-group
438 neighbor EBGP advertisement-interval 1
439 neighbor EBGP timers connect 10
440 neighbor 2001:40:1:4::6 remote-as 40
441 neighbor 2001:40:1:8::a remote-as 40
442!
443end
444 address-family ipv6
445 neighbor IBGPv6 activate
446 neighbor 2001:10::2 peer-group IBGPv6
447 neighbor 2001:10::3 peer-group IBGPv6
448 exit-address-family
449!
7918b335
DW
450end
451 address-family evpn
452 neighbor LEAF activate
453 advertise-all-vni
454 vni 10100
455 rd 65000:10100
456 route-target import 10.1.1.1:10100
457 route-target export 10.1.1.1:10100
458 exit-vni
459 exit-address-family
460!
2fc76430
DS
461end
462router ospf
463 ospf router-id 10.0.0.1
464 log-adjacency-changes detail
465 timers throttle spf 0 50 5000
466!
467end
468 '''
469
470 # The code assumes that its working on the output from the "vtysh -m"
471 # command. That provides the appropriate markers to signify end of
472 # a context. This routine uses that to build the contexts for the
473 # config.
474 #
475 # There are single line contexts such as "log file /media/node/zebra.log"
476 # and multi-line contexts such as "router ospf" and subcontexts
477 # within a context such as "address-family" within "router bgp"
478 # In each of these cases, the first line of the context becomes the
479 # key of the context. So "router bgp 10" is the key for the non-address
480 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
481 # the key for the subcontext and so on.
2fc76430
DS
482 ctx_keys = []
483 main_ctx_key = []
484 new_ctx = True
2fc76430
DS
485
486 # the keywords that we know are single line contexts. bgp in this case
487 # is not the main router bgp block, but enabling multi-instance
2fed5dcd 488 oneline_ctx_keywords = ("access-list ",
e80c8c55 489 "agentx",
55c8666a 490 "allow-external-route-update",
2fed5dcd
DW
491 "bgp ",
492 "debug ",
55c8666a 493 "domainname ",
2fed5dcd
DW
494 "dump ",
495 "enable ",
825be4c2 496 "frr ",
2fed5dcd
DW
497 "hostname ",
498 "ip ",
499 "ipv6 ",
500 "log ",
ccef6e47
EDP
501 "mpls lsp",
502 "mpls label",
a11209a7 503 "no ",
2fed5dcd
DW
504 "password ",
505 "ptm-enable",
506 "router-id ",
507 "service ",
508 "table ",
509 "username ",
f9d31f6f 510 "zebra ",
a840a40d
QY
511 "vrrp autoconfigure",
512 "evpn mh")
2fed5dcd 513
2fc76430
DS
514 for line in self.lines:
515
516 if not line:
517 continue
518
519 if line.startswith('!') or line.startswith('#'):
520 continue
521
522 # one line contexts
ccef6e47
EDP
523 # there is one exception though: ldpd accepts a 'router-id' clause
524 # as part of its 'mpls ldp' config context. If we are processing
525 # ldp configuration and encounter a router-id we should NOT switch
526 # to a new context
527 if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords) and not (
528 ctx_keys and ctx_keys[0].startswith("mpls ldp") and line.startswith("router-id ")):
2fc76430
DS
529 self.save_contexts(ctx_keys, current_context_lines)
530
531 # Start a new context
532 main_ctx_key = []
4a2587c6 533 ctx_keys = [line, ]
2fc76430
DS
534 current_context_lines = []
535
a782e613 536 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
2fc76430
DS
537 self.save_contexts(ctx_keys, current_context_lines)
538 new_ctx = True
539
06ad470d 540 elif line == "end":
2fc76430 541 self.save_contexts(ctx_keys, current_context_lines)
a782e613 542 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
2fc76430
DS
543
544 # Start a new context
06ad470d
DS
545 new_ctx = True
546 main_ctx_key = []
547 ctx_keys = []
548 current_context_lines = []
549
550 elif line == "exit-vrf":
551 self.save_contexts(ctx_keys, current_context_lines)
552 current_context_lines.append(line)
553 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
554
555 #Start a new context
2fc76430
DS
556 new_ctx = True
557 main_ctx_key = []
558 ctx_keys = []
559 current_context_lines = []
560
d60f4800 561 elif line in ["exit-address-family", "exit", "exit-vnc"]:
2fc76430
DS
562 # if this exit is for address-family ipv4 unicast, ignore the pop
563 if main_ctx_key:
564 self.save_contexts(ctx_keys, current_context_lines)
565
566 # Start a new context
567 ctx_keys = copy.deepcopy(main_ctx_key)
568 current_context_lines = []
a782e613 569 log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
2fc76430 570
609ac8dd 571 elif line in ["exit-vni", "exit-ldp-if"]:
d60f4800
DS
572 if sub_main_ctx_key:
573 self.save_contexts(ctx_keys, current_context_lines)
574
575 # Start a new context
576 ctx_keys = copy.deepcopy(sub_main_ctx_key)
577 current_context_lines = []
578 log.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line, ctx_keys)
579
2fc76430
DS
580 elif new_ctx is True:
581 if not main_ctx_key:
4a2587c6 582 ctx_keys = [line, ]
2fc76430
DS
583 else:
584 ctx_keys = copy.deepcopy(main_ctx_key)
585 main_ctx_key = []
586
587 current_context_lines = []
588 new_ctx = False
a782e613 589 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
6bd0508a
CF
590 elif (line.startswith("address-family ") or
591 line.startswith("vnc defaults") or
592 line.startswith("vnc l2-group") or
ccef6e47 593 line.startswith("vnc nve-group") or
1c23a0aa 594 line.startswith("peer") or
ccef6e47 595 line.startswith("member pseudowire")):
2fc76430
DS
596 main_ctx_key = []
597
0b960b4d
DW
598 # Save old context first
599 self.save_contexts(ctx_keys, current_context_lines)
600 current_context_lines = []
601 main_ctx_key = copy.deepcopy(ctx_keys)
a782e613 602 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
2fc76430 603
ccef6e47 604 if line == "address-family ipv6" and not ctx_keys[0].startswith("mpls ldp"):
0b960b4d 605 ctx_keys.append("address-family ipv6 unicast")
ccef6e47 606 elif line == "address-family ipv4" and not ctx_keys[0].startswith("mpls ldp"):
0b960b4d 607 ctx_keys.append("address-family ipv4 unicast")
5014d96f
DW
608 elif line == "address-family evpn":
609 ctx_keys.append("address-family l2vpn evpn")
0b960b4d
DW
610 else:
611 ctx_keys.append(line)
2fc76430 612
d60f4800
DS
613 elif ((line.startswith("vni ") and
614 len(ctx_keys) == 2 and
615 ctx_keys[0].startswith('router bgp') and
616 ctx_keys[1] == 'address-family l2vpn evpn')):
617
618 # Save old context first
619 self.save_contexts(ctx_keys, current_context_lines)
620 current_context_lines = []
621 sub_main_ctx_key = copy.deepcopy(ctx_keys)
622 log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line)
623 ctx_keys.append(line)
609ac8dd
EDP
624
625 elif ((line.startswith("interface ") and
626 len(ctx_keys) == 2 and
627 ctx_keys[0].startswith('mpls ldp') and
628 ctx_keys[1].startswith('address-family'))):
629
630 # Save old context first
631 self.save_contexts(ctx_keys, current_context_lines)
d60f4800
DS
632 current_context_lines = []
633 sub_main_ctx_key = copy.deepcopy(ctx_keys)
634 log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line)
635 ctx_keys.append(line)
636
2fc76430
DS
637 else:
638 # Continuing in an existing context, add non-commented lines to it
639 current_context_lines.append(line)
a782e613 640 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
2fc76430
DS
641
642 # Save the context of the last one
643 self.save_contexts(ctx_keys, current_context_lines)
644
4a2587c6 645
663ece2f 646def lines_to_config(ctx_keys, line, delete):
4a2587c6 647 """
e20dc2ba 648 Return the command as it would appear in frr.conf
4a2587c6
DW
649 """
650 cmd = []
651
652 if line:
653 for (i, ctx_key) in enumerate(ctx_keys):
654 cmd.append(' ' * i + ctx_key)
655
656 line = line.lstrip()
657 indent = len(ctx_keys) * ' '
658
663ece2f
DL
659 # There are some commands that are on by default so their "no" form will be
660 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
661 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
662 # not by doing a "no no bgp default ipv4-unicast"
4a2587c6
DW
663 if delete:
664 if line.startswith('no '):
665 cmd.append('%s%s' % (indent, line[3:]))
666 else:
667 cmd.append('%sno %s' % (indent, line))
668
669 else:
670 cmd.append(indent + line)
671
672 # If line is None then we are typically deleting an entire
673 # context ('no router ospf' for example)
674 else:
663ece2f
DL
675 for i, ctx_key in enumerate(ctx_keys[:-1]):
676 cmd.append('%s%s' % (' ' * i, ctx_key))
4a2587c6 677
663ece2f
DL
678 # Only put the 'no' on the last sub-context
679 if delete:
680 if ctx_keys[-1].startswith('no '):
681 cmd.append('%s%s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1][3:]))
682 else:
683 cmd.append('%sno %s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1]))
4a2587c6 684 else:
663ece2f 685 cmd.append('%s%s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1]))
8ad1fe6c
DW
686
687 return cmd
4a2587c6
DW
688
689
2fc76430
DS
690def get_normalized_ipv6_line(line):
691 """
d8e4c438 692 Return a normalized IPv6 line as produced by frr,
2fc76430 693 with all letters in lower case and trailing and leading
bb972e44
DD
694 zeros removed, and only the network portion present if
695 the IPv6 word is a network
2fc76430
DS
696 """
697 norm_line = ""
698 words = line.split(' ')
699 for word in words:
700 if ":" in word:
bb972e44
DD
701 norm_word = None
702 if "/" in word:
703 try:
1c64265f 704 if 'ipaddress' not in sys.modules:
705 v6word = IPNetwork(word)
706 norm_word = '%s/%s' % (v6word.network, v6word.prefixlen)
707 else:
708 v6word = ip_network(word, strict=False)
709 norm_word = '%s/%s' % (str(v6word.network_address), v6word.prefixlen)
bb972e44
DD
710 except ValueError:
711 pass
712 if not norm_word:
713 try:
714 norm_word = '%s' % IPv6Address(word)
0845b872 715 except ValueError:
bb972e44 716 norm_word = word
2fc76430
DS
717 else:
718 norm_word = word
719 norm_line = norm_line + " " + norm_word
720
721 return norm_line.strip()
722
4a2587c6 723
c755f5c4 724def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
9fe88bc7 725 for (ctx_keys, line) in lines:
c755f5c4
DW
726 if ctx_keys == target_ctx_keys:
727 if exact_match:
728 if line == target_line:
729 return True
730 else:
731 if line.startswith(target_line):
732 return True
9fe88bc7
DW
733 return False
734
eb9113df
DS
735def check_for_exit_vrf(lines_to_add, lines_to_del):
736
737 # exit-vrf is a bit tricky. If the new config is missing it but we
738 # have configs under a vrf, we need to add it at the end to do the
739 # right context changes. If exit-vrf exists in both the running and
740 # new config, we cannot delete it or it will break context changes.
741 add_exit_vrf = False
742 index = 0
743
744 for (ctx_keys, line) in lines_to_add:
745 if add_exit_vrf == True:
746 if ctx_keys[0] != prior_ctx_key:
747 insert_key=(prior_ctx_key),
748 lines_to_add.insert(index, ((insert_key, "exit-vrf")))
749 add_exit_vrf = False
750
751 if ctx_keys[0].startswith('vrf') and line:
752 if line is not "exit-vrf":
753 add_exit_vrf = True
754 prior_ctx_key = (ctx_keys[0])
755 else:
756 add_exit_vrf = False
757 index+=1
758
759 for (ctx_keys, line) in lines_to_del:
760 if line == "exit-vrf":
761 if (line_exist(lines_to_add, ctx_keys, line)):
762 lines_to_del.remove((ctx_keys, line))
763
764 return (lines_to_add, lines_to_del)
9fe88bc7 765
9b166171 766def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
9fe88bc7
DW
767
768 # Quite possibly the most confusing (while accurate) variable names in history
769 lines_to_add_to_del = []
770 lines_to_del_to_del = []
771
772 for (ctx_keys, line) in lines_to_del:
9b166171
DW
773 deleted = False
774
028bcc88
DW
775 if ctx_keys[0].startswith('router bgp') and line:
776
777 if line.startswith('neighbor '):
778 '''
779 BGP changed how it displays swpX peers that are part of peer-group. Older
780 versions of frr would display these on separate lines:
781 neighbor swp1 interface
782 neighbor swp1 peer-group FOO
783
784 but today we display via a single line
785 neighbor swp1 interface peer-group FOO
786
787 This change confuses frr-reload.py so check to see if we are deleting
788 neighbor swp1 interface peer-group FOO
789
790 and adding
791 neighbor swp1 interface
792 neighbor swp1 peer-group FOO
793
794 If so then chop the del line and the corresponding add lines
795 '''
796
797 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
798 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
799
800 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
801 swpx_interface = None
802 swpx_peergroup = None
803
804 if re_swpx_int_peergroup:
805 swpx = re_swpx_int_peergroup.group(1)
806 peergroup = re_swpx_int_peergroup.group(2)
807 swpx_interface = "neighbor %s interface" % swpx
808 elif re_swpx_int_v6only_peergroup:
809 swpx = re_swpx_int_v6only_peergroup.group(1)
810 peergroup = re_swpx_int_v6only_peergroup.group(2)
811 swpx_interface = "neighbor %s interface v6only" % swpx
812
813 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
814 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
815 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
816 tmp_ctx_keys = tuple(list(ctx_keys))
9b166171
DW
817
818 if not found_add_swpx_peergroup:
819 tmp_ctx_keys = list(ctx_keys)
028bcc88 820 tmp_ctx_keys.append('address-family ipv4 unicast')
9b166171
DW
821 tmp_ctx_keys = tuple(tmp_ctx_keys)
822 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
9fe88bc7 823
028bcc88
DW
824 if not found_add_swpx_peergroup:
825 tmp_ctx_keys = list(ctx_keys)
826 tmp_ctx_keys.append('address-family ipv6 unicast')
827 tmp_ctx_keys = tuple(tmp_ctx_keys)
828 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
829
830 if found_add_swpx_interface and found_add_swpx_peergroup:
831 deleted = True
832 lines_to_del_to_del.append((ctx_keys, line))
833 lines_to_add_to_del.append((ctx_keys, swpx_interface))
834 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
835
ee951352
DS
836 '''
837 Changing the bfd timers on neighbors is allowed without doing
838 a delete/add process. Since doing a "no neighbor blah bfd ..."
839 will cause the peer to bounce unnecessarily, just skip the delete
840 and just do the add.
841 '''
842 re_nbr_bfd_timers = re.search(r'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line)
843
844 if re_nbr_bfd_timers:
845 nbr = re_nbr_bfd_timers.group(1)
846 bfd_nbr = "neighbor %s" % nbr
deb2d401 847 bfd_search_string = bfd_nbr + r' bfd (\S+) (\S+) (\S+)'
ee951352
DS
848
849 for (ctx_keys, add_line) in lines_to_add:
ca7f0496
DS
850 if ctx_keys[0].startswith('router bgp'):
851 re_add_nbr_bfd_timers = re.search(bfd_search_string, add_line)
ee951352 852
ca7f0496
DS
853 if re_add_nbr_bfd_timers:
854 found_add_bfd_nbr = line_exist(lines_to_add, ctx_keys, bfd_nbr, False)
ee951352 855
ca7f0496
DS
856 if found_add_bfd_nbr:
857 lines_to_del_to_del.append((ctx_keys, line))
ee951352 858
028bcc88 859 '''
4c76e592 860 We changed how we display the neighbor interface command. Older
028bcc88
DW
861 versions of frr would display the following:
862 neighbor swp1 interface
863 neighbor swp1 remote-as external
864 neighbor swp1 capability extended-nexthop
865
866 but today we display via a single line
867 neighbor swp1 interface remote-as external
868
869 and capability extended-nexthop is no longer needed because we
870 automatically enable it when the neighbor is of type interface.
871
872 This change confuses frr-reload.py so check to see if we are deleting
873 neighbor swp1 interface remote-as (external|internal|ASNUM)
874
875 and adding
876 neighbor swp1 interface
877 neighbor swp1 remote-as (external|internal|ASNUM)
878 neighbor swp1 capability extended-nexthop
879
880 If so then chop the del line and the corresponding add lines
881 '''
882 re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
883 re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
884
885 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
886 swpx_interface = None
887 swpx_remoteas = None
888
889 if re_swpx_int_remoteas:
890 swpx = re_swpx_int_remoteas.group(1)
891 remoteas = re_swpx_int_remoteas.group(2)
892 swpx_interface = "neighbor %s interface" % swpx
893 elif re_swpx_int_v6only_remoteas:
894 swpx = re_swpx_int_v6only_remoteas.group(1)
895 remoteas = re_swpx_int_v6only_remoteas.group(2)
896 swpx_interface = "neighbor %s interface v6only" % swpx
897
898 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
899 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
900 found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
901 tmp_ctx_keys = tuple(list(ctx_keys))
902
903 if found_add_swpx_interface and found_add_swpx_remoteas:
904 deleted = True
905 lines_to_del_to_del.append((ctx_keys, line))
906 lines_to_add_to_del.append((ctx_keys, swpx_interface))
907 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
908
909 '''
4c76e592
DW
910 We made the 'bgp bestpath as-path multipath-relax' command
911 automatically assume 'no-as-set' since the lack of this option caused
912 weird routing problems. When the running config is shown in
913 releases with this change, the no-as-set keyword is not shown as it
914 is the default. This causes frr-reload to unnecessarily unapply
915 this option only to apply it back again, causing unnecessary session
916 resets.
028bcc88
DW
917 '''
918 if 'multipath-relax' in line:
919 re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line)
920 old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set'
921 found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
922
923 if re_asrelax_new and found_asrelax_old:
9b166171 924 deleted = True
9fe88bc7 925 lines_to_del_to_del.append((ctx_keys, line))
028bcc88
DW
926 lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
927
928 '''
929 If we are modifying the BGP table-map we need to avoid a del/add and
930 instead modify the table-map in place via an add. This is needed to
931 avoid installing all routes in the RIB the second the 'no table-map'
932 is issued.
933 '''
934 if line.startswith('table-map'):
935 found_table_map = line_exist(lines_to_add, ctx_keys, 'table-map', False)
936
937 if found_table_map:
b3a39dc5 938 lines_to_del_to_del.append((ctx_keys, line))
c755f5c4 939
78e31f46 940 '''
028bcc88
DW
941 More old-to-new config handling. ip import-table no longer accepts
942 distance, but we honor the old syntax. But 'show running' shows only
943 the new syntax. This causes an unnecessary 'no import-table' followed
944 by the same old 'ip import-table' which causes perturbations in
945 announced routes leading to traffic blackholes. Fix this issue.
78e31f46
DD
946 '''
947 re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0])
948 if re_importtbl:
949 table_num = re_importtbl.group(1)
950 for ctx in lines_to_add:
951 if ctx[0][0].startswith('ip import-table %s distance' % table_num):
78e31f46
DD
952 lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
953 lines_to_add_to_del.append((ctx[0], None))
0bf7cc28
DD
954
955 '''
956 ip/ipv6 prefix-list can be specified without a seq number. However,
957 the running config always adds 'seq x', where x is a number incremented
958 by 5 for every element, to the prefix list. So, ignore such lines as
959 well. Sample prefix-list lines:
960 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
961 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
962 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
963 '''
964 re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
965 ctx_keys[0])
966 if re_ip_pfxlst:
967 tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) +
968 re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) +
969 re_ip_pfxlst.group(6))
970 for ctx in lines_to_add:
971 if ctx[0][0] == tmpline:
972 lines_to_del_to_del.append((ctx_keys, None))
973 lines_to_add_to_del.append(((tmpline,), None))
974
5014d96f
DW
975 if (len(ctx_keys) == 3 and
976 ctx_keys[0].startswith('router bgp') and
977 ctx_keys[1] == 'address-family l2vpn evpn' and
978 ctx_keys[2].startswith('vni')):
979
980 re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False
981
982 if re_route_target:
983 rt = re_route_target.group(1).strip()
984 route_target_import_line = line
985 route_target_export_line = "route-target export %s" % rt
986 route_target_both_line = "route-target both %s" % rt
987
988 found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line)
989 found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line)
990
991 '''
992 If the running configs has
993 route-target import 1:1
994 route-target export 1:1
995
996 and the config we are reloading against has
997 route-target both 1:1
998
999 then we can ignore deleting the import/export and ignore adding the 'both'
1000 '''
1001 if found_route_target_export_line and found_route_target_both_line:
1002 lines_to_del_to_del.append((ctx_keys, route_target_import_line))
1003 lines_to_del_to_del.append((ctx_keys, route_target_export_line))
1004 lines_to_add_to_del.append((ctx_keys, route_target_both_line))
1005
6024e562
DS
1006 # Deleting static routes under a vrf can lead to time-outs if each is sent
1007 # as separate vtysh -c commands. Change them from being in lines_to_del and
1008 # put the "no" form in lines_to_add
1009 if ctx_keys[0].startswith('vrf ') and line:
1010 if (line.startswith('ip route') or
1011 line.startswith('ipv6 route')):
1012 add_cmd = ('no ' + line)
1013 lines_to_add.append((ctx_keys, add_cmd))
1014 lines_to_del_to_del.append((ctx_keys, line))
1015
9b166171
DW
1016 if not deleted:
1017 found_add_line = line_exist(lines_to_add, ctx_keys, line)
1018
1019 if found_add_line:
1020 lines_to_del_to_del.append((ctx_keys, line))
1021 lines_to_add_to_del.append((ctx_keys, line))
1022 else:
1023 '''
1024 We have commands that used to be displayed in the global part
1025 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1026
1027 # old way
1028 router bgp 64900
1029 neighbor ISL advertisement-interval 0
1030
1031 vs.
1032
1033 # new way
1034 router bgp 64900
1035 address-family ipv4 unicast
1036 neighbor ISL advertisement-interval 0
1037
1038 Look to see if we are deleting it in one format just to add it back in the other
1039 '''
1040 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
1041 tmp_ctx_keys = list(ctx_keys)[:-1]
1042 tmp_ctx_keys = tuple(tmp_ctx_keys)
1043
1044 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
1045
1046 if found_add_line:
1047 lines_to_del_to_del.append((ctx_keys, line))
1048 lines_to_add_to_del.append((tmp_ctx_keys, line))
9fe88bc7
DW
1049
1050 for (ctx_keys, line) in lines_to_del_to_del:
1051 lines_to_del.remove((ctx_keys, line))
1052
1053 for (ctx_keys, line) in lines_to_add_to_del:
1054 lines_to_add.remove((ctx_keys, line))
1055
1056 return (lines_to_add, lines_to_del)
1057
1058
b05a1d3c
DW
1059def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
1060 """
1061 There are certain commands that cannot be removed. Remove
1062 those commands from lines_to_del.
1063 """
1064 lines_to_del_to_del = []
1065
1066 for (ctx_keys, line) in lines_to_del:
1067
1068 if (ctx_keys[0].startswith('frr version') or
1069 ctx_keys[0].startswith('frr defaults') or
663ece2f 1070 ctx_keys[0].startswith('username') or
b05a1d3c
DW
1071 ctx_keys[0].startswith('password') or
1072 ctx_keys[0].startswith('line vty') or
1073
1074 # This is technically "no"able but if we did so frr-reload would
1075 # stop working so do not let the user shoot themselves in the foot
1076 # by removing this.
1077 ctx_keys[0].startswith('service integrated-vtysh-config')):
1078
663ece2f 1079 log.info('"%s" cannot be removed' % (ctx_keys[-1],))
b05a1d3c
DW
1080 lines_to_del_to_del.append((ctx_keys, line))
1081
1082 for (ctx_keys, line) in lines_to_del_to_del:
1083 lines_to_del.remove((ctx_keys, line))
1084
1085 return (lines_to_add, lines_to_del)
1086
1087
2fc76430
DS
1088def compare_context_objects(newconf, running):
1089 """
1090 Create a context diff for the two specified contexts
1091 """
1092
1093 # Compare the two Config objects to find the lines that we need to add/del
1094 lines_to_add = []
1095 lines_to_del = []
926ea62e 1096 delete_bgpd = False
2fc76430
DS
1097
1098 # Find contexts that are in newconf but not in running
1099 # Find contexts that are in running but not in newconf
1c64265f 1100 for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
2fc76430
DS
1101
1102 if running_ctx_keys not in newconf.contexts:
1103
ab5f8310
DW
1104 # We check that the len is 1 here so that we only look at ('router bgp 10')
1105 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
926ea62e 1106 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
ab5f8310
DW
1107 # running but not in newconf.
1108 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
926ea62e
DW
1109 delete_bgpd = True
1110 lines_to_del.append((running_ctx_keys, None))
1111
2a2b64e4
DS
1112 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1113 elif running_ctx_keys[0].startswith('interface') or running_ctx_keys[0].startswith('vrf'):
768bf950
DD
1114 for line in running_ctx.lines:
1115 lines_to_del.append((running_ctx_keys, line))
1116
926ea62e
DW
1117 # If this is an address-family under 'router bgp' and we are already deleting the
1118 # entire 'router bgp' context then ignore this sub-context
1119 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
76f69d1c 1120 continue
514665b9 1121
5014d96f
DW
1122 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1123 elif ("router bgp" in running_ctx_keys[0] and
1124 len(running_ctx_keys) > 2 and
1125 running_ctx_keys[1].startswith('address-family l2vpn evpn') and
1126 running_ctx_keys[2].startswith('vni ')):
1127 lines_to_del.append((running_ctx_keys, None))
1128
afa2e8e1
DW
1129 elif ("router bgp" in running_ctx_keys[0] and
1130 len(running_ctx_keys) > 1 and
1131 running_ctx_keys[1].startswith('address-family')):
1132 # There's no 'no address-family' support and so we have to
1133 # delete each line individually again
1134 for line in running_ctx.lines:
1135 lines_to_del.append((running_ctx_keys, line))
1136
6024e562
DS
1137 # Some commands can happen at higher counts that make
1138 # doing vtysh -c inefficient (and can time out.) For
1139 # these commands, instead of adding them to lines_to_del,
1140 # add the "no " version to lines_to_add.
1141 elif (running_ctx_keys[0].startswith('ip route') or
1142 running_ctx_keys[0].startswith('ipv6 route') or
1143 running_ctx_keys[0].startswith('access-list') or
1144 running_ctx_keys[0].startswith('ipv6 access-list') or
1145 running_ctx_keys[0].startswith('ip prefix-list') or
1146 running_ctx_keys[0].startswith('ipv6 prefix-list')):
1147 add_cmd = ('no ' + running_ctx_keys[0],)
1148 lines_to_add.append((add_cmd, None))
1149
e04ff92e
EDP
1150 # if this an interface sub-subcontext in an address-family block in ldpd and
1151 # we are already deleting the whole context, then ignore this
1152 elif (len(running_ctx_keys) > 2 and running_ctx_keys[0].startswith('mpls ldp') and
1153 running_ctx_keys[1].startswith('address-family') and
1154 (running_ctx_keys[:2], None) in lines_to_del):
1155 continue
1156
2fc76430 1157 # Non-global context
926ea62e 1158 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
1159 lines_to_del.append((running_ctx_keys, None))
1160
7918b335
DW
1161 elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys):
1162 lines_to_del.append((running_ctx_keys, None))
1163
2fc76430
DS
1164 # Global context
1165 else:
1166 for line in running_ctx.lines:
1167 lines_to_del.append((running_ctx_keys, line))
1168
1169 # Find the lines within each context to add
1170 # Find the lines within each context to del
1c64265f 1171 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
2fc76430
DS
1172
1173 if newconf_ctx_keys in running.contexts:
1174 running_ctx = running.contexts[newconf_ctx_keys]
1175
1176 for line in newconf_ctx.lines:
1177 if line not in running_ctx.dlines:
1178 lines_to_add.append((newconf_ctx_keys, line))
1179
1180 for line in running_ctx.lines:
1181 if line not in newconf_ctx.dlines:
1182 lines_to_del.append((newconf_ctx_keys, line))
1183
1c64265f 1184 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
2fc76430
DS
1185
1186 if newconf_ctx_keys not in running.contexts:
1187 lines_to_add.append((newconf_ctx_keys, None))
1188
1189 for line in newconf_ctx.lines:
1190 lines_to_add.append((newconf_ctx_keys, line))
1191
eb9113df 1192 (lines_to_add, lines_to_del) = check_for_exit_vrf(lines_to_add, lines_to_del)
9b166171 1193 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
b05a1d3c 1194 (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del)
9fe88bc7 1195
926ea62e 1196 return (lines_to_add, lines_to_del)
2fc76430 1197
8ad1fe6c 1198
2fc76430
DS
1199if __name__ == '__main__':
1200 # Command line options
d8e4c438 1201 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
2fc76430
DS
1202 parser.add_argument('--input', help='Read running config from file instead of "show running"')
1203 group = parser.add_mutually_exclusive_group(required=True)
1204 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
1205 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
1206 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
cc146ecc 1207 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
a0a7dead 1208 parser.add_argument('--pathspace', '-N', metavar='NAME', help='Reload specified path/namespace', default=None)
d8e4c438 1209 parser.add_argument('filename', help='Location of new frr config file')
e20dc2ba 1210 parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
1a11d9cd
EDP
1211 parser.add_argument('--bindir', help='path to the vtysh executable', default='/usr/bin')
1212 parser.add_argument('--confdir', help='path to the daemon config files', default='/etc/frr')
1213 parser.add_argument('--rundir', help='path for the temp config file', default='/var/run/frr')
fa18c6bb 1214 parser.add_argument('--vty_socket', help='socket to be used by vtysh to connect to the daemons', default=None)
d9730542
EDP
1215 parser.add_argument('--daemon', help='daemon for which want to replace the config', default='')
1216
2fc76430
DS
1217 args = parser.parse_args()
1218
1219 # Logging
1220 # For --test log to stdout
d8e4c438 1221 # For --reload log to /var/log/frr/frr-reload.log
cc146ecc 1222 if args.test or args.stdout:
c50aceee 1223 logging.basicConfig(level=logging.INFO,
2fc76430 1224 format='%(asctime)s %(levelname)5s: %(message)s')
926ea62e
DW
1225
1226 # Color the errors and warnings in red
1227 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
1228 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
1229
2fc76430 1230 elif args.reload:
d8e4c438
DS
1231 if not os.path.isdir('/var/log/frr/'):
1232 os.makedirs('/var/log/frr/')
2fc76430 1233
d8e4c438 1234 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
c50aceee 1235 level=logging.INFO,
2fc76430
DS
1236 format='%(asctime)s %(levelname)5s: %(message)s')
1237
1238 # argparse should prevent this from happening but just to be safe...
1239 else:
1240 raise Exception('Must specify --reload or --test')
a782e613 1241 log = logging.getLogger(__name__)
2fc76430 1242
76f69d1c
DW
1243 # Verify the new config file is valid
1244 if not os.path.isfile(args.filename):
825be4c2 1245 msg = "Filename %s does not exist" % args.filename
1c64265f 1246 print(msg)
825be4c2 1247 log.error(msg)
76f69d1c
DW
1248 sys.exit(1)
1249
1250 if not os.path.getsize(args.filename):
825be4c2 1251 msg = "Filename %s is an empty file" % args.filename
1c64265f 1252 print(msg)
825be4c2 1253 log.error(msg)
76f69d1c
DW
1254 sys.exit(1)
1255
1a11d9cd
EDP
1256 # Verify that confdir is correct
1257 if not os.path.isdir(args.confdir):
1258 msg = "Confdir %s is not a valid path" % args.confdir
1259 print(msg)
1260 log.error(msg)
1261 sys.exit(1)
1262
1263 # Verify that bindir is correct
1264 if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + '/vtysh'):
1265 msg = "Bindir %s is not a valid path to vtysh" % args.bindir
1266 print(msg)
1267 log.error(msg)
1268 sys.exit(1)
1269
fa18c6bb
DL
1270 # verify that the vty_socket, if specified, is valid
1271 if args.vty_socket and not os.path.isdir(args.vty_socket):
1272 msg = 'vty_socket %s is not a valid path' % args.vty_socket
1273 print(msg)
1274 log.error(msg)
1275 sys.exit(1)
1276
d9730542
EDP
1277 # verify that the daemon, if specified, is valid
1278 if args.daemon and args.daemon not in ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']:
1279 msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon
1280 print(msg)
1281 log.error(msg)
1282 sys.exit(1)
1283
a0a7dead 1284 vtysh = Vtysh(args.bindir, args.confdir, args.vty_socket, args.pathspace)
663ece2f 1285
76f69d1c 1286 # Verify that 'service integrated-vtysh-config' is configured
a0a7dead
DL
1287 if args.pathspace:
1288 vtysh_filename = args.confdir + '/' + args.pathspace + '/vtysh.conf'
1289 else:
1290 vtysh_filename = args.confdir + '/vtysh.conf'
6ac9179c 1291 service_integrated_vtysh_config = True
76f69d1c 1292
f850d14d
DW
1293 if os.path.isfile(vtysh_filename):
1294 with open(vtysh_filename, 'r') as fh:
1295 for line in fh.readlines():
1296 line = line.strip()
76f69d1c 1297
6ac9179c
DD
1298 if line == 'no service integrated-vtysh-config':
1299 service_integrated_vtysh_config = False
f850d14d 1300 break
76f69d1c 1301
d9730542 1302 if not service_integrated_vtysh_config and not args.daemon:
825be4c2 1303 msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1c64265f 1304 print(msg)
825be4c2 1305 log.error(msg)
76f69d1c 1306 sys.exit(1)
2fc76430 1307
c50aceee 1308 if args.debug:
a782e613 1309 log.setLevel(logging.DEBUG)
c50aceee 1310
a782e613 1311 log.info('Called via "%s"', str(args))
c50aceee 1312
2fc76430 1313 # Create a Config object from the config generated by newconf
663ece2f
DL
1314 newconf = Config(vtysh)
1315 newconf.load_from_file(args.filename)
825be4c2 1316 reload_ok = True
2fc76430
DS
1317
1318 if args.test:
1319
1320 # Create a Config object from the running config
663ece2f 1321 running = Config(vtysh)
2fc76430
DS
1322
1323 if args.input:
663ece2f 1324 running.load_from_file(args.input)
2fc76430 1325 else:
663ece2f 1326 running.load_from_show_running(args.daemon)
2fc76430 1327
926ea62e 1328 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
4a2587c6 1329 lines_to_configure = []
2fc76430
DS
1330
1331 if lines_to_del:
1c64265f 1332 print("\nLines To Delete")
1333 print("===============")
2fc76430
DS
1334
1335 for (ctx_keys, line) in lines_to_del:
1336
1337 if line == '!':
1338 continue
1339
663ece2f 1340 cmd = '\n'.join(lines_to_config(ctx_keys, line, True))
4a2587c6 1341 lines_to_configure.append(cmd)
1c64265f 1342 print(cmd)
2fc76430
DS
1343
1344 if lines_to_add:
1c64265f 1345 print("\nLines To Add")
1346 print("============")
2fc76430
DS
1347
1348 for (ctx_keys, line) in lines_to_add:
1349
1350 if line == '!':
1351 continue
1352
663ece2f 1353 cmd = '\n'.join(lines_to_config(ctx_keys, line, False))
4a2587c6 1354 lines_to_configure.append(cmd)
1c64265f 1355 print(cmd)
2fc76430 1356
2fc76430
DS
1357 elif args.reload:
1358
2f52ad96 1359 # We will not be able to do anything, go ahead and exit(1)
663ece2f 1360 if not vtysh.is_config_available():
2f52ad96
DW
1361 sys.exit(1)
1362
d8e4c438 1363 log.debug('New Frr Config\n%s', newconf.get_lines())
2fc76430
DS
1364
1365 # This looks a little odd but we have to do this twice...here is why
1366 # If the user had this running bgp config:
4a2587c6 1367 #
2fc76430
DS
1368 # router bgp 10
1369 # neighbor 1.1.1.1 remote-as 50
1370 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 1371 #
2fc76430 1372 # and this config in the newconf config file
4a2587c6 1373 #
2fc76430
DS
1374 # router bgp 10
1375 # neighbor 1.1.1.1 remote-as 999
1376 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
1377 #
1378 #
2fc76430
DS
1379 # Then the script will do
1380 # - no neighbor 1.1.1.1 remote-as 50
1381 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 1382 #
2fc76430
DS
1383 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1384 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1385 # configs again to put this line back.
1386
1a8c43f1 1387 # There are many keywords in FRR that can only appear one time under
2ce26af1
DW
1388 # a context, take "bgp router-id" for example. If the config that we are
1389 # reloading against has the following:
1390 #
1391 # router bgp 10
1392 # bgp router-id 1.1.1.1
1393 # bgp router-id 2.2.2.2
1394 #
1395 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1396 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1397 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1398 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1399 # second pass to include all of the "adds" from the first pass.
1400 lines_to_add_first_pass = []
1401
2fc76430 1402 for x in range(2):
663ece2f
DL
1403 running = Config(vtysh)
1404 running.load_from_show_running(args.daemon)
d8e4c438 1405 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 1406
926ea62e 1407 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2fc76430 1408
2ce26af1
DW
1409 if x == 0:
1410 lines_to_add_first_pass = lines_to_add
1411 else:
1412 lines_to_add.extend(lines_to_add_first_pass)
1413
53bddc22 1414 # Only do deletes on the first pass. The reason being if we
1a8c43f1 1415 # configure a bgp neighbor via "neighbor swp1 interface" FRR
53bddc22
DW
1416 # will automatically add:
1417 #
1418 # interface swp1
1419 # ipv6 nd ra-interval 10
1420 # no ipv6 nd suppress-ra
1421 # !
1422 #
1423 # but those lines aren't in the config we are reloading against so
1424 # on the 2nd pass they will show up in lines_to_del. This could
1425 # apply to other scenarios as well where configuring FOO adds BAR
1426 # to the config.
1427 if lines_to_del and x == 0:
2fc76430
DS
1428 for (ctx_keys, line) in lines_to_del:
1429
1430 if line == '!':
1431 continue
1432
4a2587c6
DW
1433 # 'no' commands are tricky, we can't just put them in a file and
1434 # vtysh -f that file. See the next comment for an explanation
1435 # of their quirks
663ece2f 1436 cmd = lines_to_config(ctx_keys, line, True)
2fc76430
DS
1437 original_cmd = cmd
1438
d8e4c438 1439 # Some commands in frr are picky about taking a "no" of the entire line.
76f69d1c
DW
1440 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1441 # only the beginning. If we hit one of these command an exception will be
1442 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 1443 #
76f69d1c 1444 # Example:
d8e4c438
DS
1445 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1446 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 1447 # % Unknown command.
d8e4c438 1448 # frr(config-if)# no ip ospf authentication message-digest
76f69d1c 1449 # % Unknown command.
d8e4c438
DS
1450 # frr(config-if)# no ip ospf authentication
1451 # frr(config-if)#
2fc76430
DS
1452
1453 while True:
2fc76430 1454 try:
663ece2f 1455 vtysh(['configure'] + cmd)
2fc76430 1456
663ece2f 1457 except VtyshException:
2fc76430
DS
1458
1459 # - Pull the last entry from cmd (this would be
1460 # 'no ip ospf authentication message-digest 1.1.1.1' in
1461 # our example above
1462 # - Split that last entry by whitespace and drop the last word
825be4c2 1463 log.info('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
1464 last_arg = cmd[-1].split(' ')
1465
1466 if len(last_arg) <= 2:
663ece2f 1467 log.error('"%s" we failed to remove this command', ' -- '.join(original_cmd))
2fc76430
DS
1468 break
1469
1470 new_last_arg = last_arg[0:-1]
1471 cmd[-1] = ' '.join(new_last_arg)
1472 else:
a782e613 1473 log.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
1474 break
1475
2fc76430 1476 if lines_to_add:
4a2587c6
DW
1477 lines_to_configure = []
1478
2fc76430
DS
1479 for (ctx_keys, line) in lines_to_add:
1480
1481 if line == '!':
1482 continue
1483
6024e562
DS
1484 # Don't run "no" commands twice since they can error
1485 # out the second time due to first deletion
1486 if x == 1 and ctx_keys[0].startswith('no '):
1487 continue
1488
663ece2f 1489 cmd = '\n'.join(lines_to_config(ctx_keys, line, False)) + '\n'
4a2587c6
DW
1490 lines_to_configure.append(cmd)
1491
1492 if lines_to_configure:
1493 random_string = ''.join(random.SystemRandom().choice(
1494 string.ascii_uppercase +
1495 string.digits) for _ in range(6))
1496
1a11d9cd 1497 filename = args.rundir + "/reload-%s.txt" % random_string
a782e613 1498 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
4a2587c6
DW
1499
1500 with open(filename, 'w') as fh:
1501 for line in lines_to_configure:
1502 fh.write(line + '\n')
825be4c2 1503
596074af 1504 try:
663ece2f
DL
1505 vtysh.exec_file(filename)
1506 except VtyshException as e:
1507 log.warning("frr-reload.py failed due to\n%s" % e.args)
596074af 1508 reload_ok = False
4a2587c6 1509 os.unlink(filename)
2fc76430 1510
4b78098d 1511 # Make these changes persistent
d9730542
EDP
1512 target = str(args.confdir + '/frr.conf')
1513 if args.overwrite or (not args.daemon and args.filename != target):
663ece2f 1514 vtysh('write')
825be4c2
DW
1515
1516 if not reload_ok:
1517 sys.exit(1)