]> git.proxmox.com Git - mirror_frr.git/blame - tools/frr-reload.py
Merge pull request #985 from chiragshah6/mdev2
[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
31import argparse
32import copy
33import logging
34import os
4a2587c6 35import random
9fe88bc7 36import re
4a2587c6 37import string
2fc76430
DS
38import subprocess
39import sys
40from collections import OrderedDict
bb972e44 41from ipaddr import IPv6Address, IPNetwork
4a2587c6
DW
42from pprint import pformat
43
2fc76430 44
a782e613
DW
45log = logging.getLogger(__name__)
46
47
276887bb
DW
48class VtyshMarkException(Exception):
49 pass
50
51
2fc76430 52class Context(object):
4a2587c6 53
2fc76430 54 """
d8e4c438 55 A Context object represents a section of frr configuration such as:
2fc76430
DS
56!
57interface swp3
58 description swp3 -> r8's swp1
59 ipv6 nd suppress-ra
60 link-detect
61!
62
63or a single line context object such as this:
64
65ip forwarding
66
67 """
68
69 def __init__(self, keys, lines):
70 self.keys = keys
71 self.lines = lines
72
73 # Keep a dictionary of the lines, this is to make it easy to tell if a
74 # line exists in this Context
75 self.dlines = OrderedDict()
76
77 for ligne in lines:
78 self.dlines[ligne] = True
79
80 def add_lines(self, lines):
81 """
82 Add lines to specified context
83 """
84
85 self.lines.extend(lines)
86
87 for ligne in lines:
88 self.dlines[ligne] = True
89
90
91class Config(object):
4a2587c6 92
2fc76430 93 """
d8e4c438 94 A frr configuration is stored in a Config object. A Config object
2fc76430
DS
95 contains a dictionary of Context objects where the Context keys
96 ('router ospf' for example) are our dictionary key.
97 """
98
99 def __init__(self):
100 self.lines = []
101 self.contexts = OrderedDict()
102
103 def load_from_file(self, filename):
104 """
105 Read configuration from specified file and slurp it into internal memory
106 The internal representation has been marked appropriately by passing it
107 through vtysh with the -m parameter
108 """
a782e613 109 log.info('Loading Config object from file %s', filename)
2fc76430
DS
110
111 try:
ec3fd957
DW
112 file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename],
113 stderr=subprocess.STDOUT)
2fc76430 114 except subprocess.CalledProcessError as e:
ec3fd957
DW
115 ve = VtyshMarkException(e)
116 ve.output = e.output
117 raise ve
2fc76430
DS
118
119 for line in file_output.split('\n'):
120 line = line.strip()
121 if ":" in line:
122 qv6_line = get_normalized_ipv6_line(line)
123 self.lines.append(qv6_line)
124 else:
125 self.lines.append(line)
126
127 self.load_contexts()
128
129 def load_from_show_running(self):
130 """
131 Read running configuration and slurp it into internal memory
132 The internal representation has been marked appropriately by passing it
133 through vtysh with the -m parameter
134 """
a782e613 135 log.info('Loading Config object from vtysh show running')
2fc76430
DS
136
137 try:
4a2587c6
DW
138 config_text = subprocess.check_output(
139 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
ec3fd957 140 shell=True, stderr=subprocess.STDOUT)
2fc76430 141 except subprocess.CalledProcessError as e:
ec3fd957
DW
142 ve = VtyshMarkException(e)
143 ve.output = e.output
144 raise ve
2fc76430 145
2fc76430
DS
146 for line in config_text.split('\n'):
147 line = line.strip()
148
149 if (line == 'Building configuration...' or
150 line == 'Current configuration:' or
4a2587c6 151 not line):
2fc76430
DS
152 continue
153
154 self.lines.append(line)
155
156 self.load_contexts()
157
158 def get_lines(self):
159 """
160 Return the lines read in from the configuration
161 """
162
163 return '\n'.join(self.lines)
164
165 def get_contexts(self):
166 """
167 Return the parsed context as strings for display, log etc.
168 """
169
170 for (_, ctx) in sorted(self.contexts.iteritems()):
171 print str(ctx) + '\n'
172
173 def save_contexts(self, key, lines):
174 """
175 Save the provided key and lines as a context
176 """
177
178 if not key:
179 return
180
bb972e44
DD
181 '''
182 IP addresses specified in "network" statements, "ip prefix-lists"
183 etc. can differ in the host part of the specification the user
184 provides and what the running config displays. For example, user
185 can specify 11.1.1.1/24, and the running config displays this as
186 11.1.1.0/24. Ensure we don't do a needless operation for such
187 lines. IS-IS & OSPFv3 have no "network" support.
188 '''
4d760f42 189 re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0])
bb972e44
DD
190 if re_key_rt:
191 addr = re_key_rt.group(2)
192 if '/' in addr:
193 try:
194 newaddr = IPNetwork(addr)
4d760f42
DD
195 key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
196 newaddr.network,
197 newaddr.prefixlen,
198 re_key_rt.group(3))
bb972e44
DD
199 except ValueError:
200 pass
201
202 re_key_rt = re.match(
203 r'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
204 key[0]
205 )
206 if re_key_rt:
207 addr = re_key_rt.group(4)
208 if '/' in addr:
209 try:
210 newaddr = '%s/%s' % (IPNetwork(addr).network,
211 IPNetwork(addr).prefixlen)
212 except ValueError:
213 newaddr = addr
0845b872
DD
214 else:
215 newaddr = addr
bb972e44
DD
216
217 legestr = re_key_rt.group(5)
218 re_lege = re.search(r'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr)
219 if re_lege:
220 legestr = '%sge %s le %s%s' % (re_lege.group(1),
221 re_lege.group(3),
222 re_lege.group(2),
223 re_lege.group(4))
224 re_lege = re.search(r'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr)
225
226 if (re_lege and ((re_key_rt.group(1) == "ip" and
227 re_lege.group(3) == "32") or
228 (re_key_rt.group(1) == "ipv6" and
229 re_lege.group(3) == "128"))):
230 legestr = '%sge %s%s' % (re_lege.group(1),
231 re_lege.group(2),
232 re_lege.group(4))
233
234 key[0] = '%s prefix-list%s%s %s%s' % (re_key_rt.group(1),
235 re_key_rt.group(2),
236 re_key_rt.group(3),
237 newaddr,
238 legestr)
239
240 if lines and key[0].startswith('router bgp'):
241 newlines = []
242 for line in lines:
243 re_net = re.match(r'network\s+([A-Fa-f:.0-9/]+)(.*)$', line)
244 if re_net:
245 addr = re_net.group(1)
246 if '/' not in addr and key[0].startswith('router bgp'):
247 # This is most likely an error because with no
248 # prefixlen, BGP treats the prefixlen as 8
249 addr = addr + '/8'
250
251 try:
252 newaddr = IPNetwork(addr)
253 line = 'network %s/%s %s' % (newaddr.network,
254 newaddr.prefixlen,
255 re_net.group(2))
256 newlines.append(line)
0845b872 257 except ValueError:
bb972e44
DD
258 # Really this should be an error. Whats a network
259 # without an IP Address following it ?
260 newlines.append(line)
261 else:
262 newlines.append(line)
263 lines = newlines
264
265 '''
266 More fixups in user specification and what running config shows.
267 "null0" in routes must be replaced by Null0, and "blackhole" must
268 be replaced by Null0 as well.
269 '''
270 if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and
271 'null0' in key[0] or 'blackhole' in key[0]):
272 key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0])
273 key[0] = re.sub(r'\s+blackhole(\s*$)', ' Null0', key[0])
274
2fc76430
DS
275 if lines:
276 if tuple(key) not in self.contexts:
277 ctx = Context(tuple(key), lines)
278 self.contexts[tuple(key)] = ctx
279 else:
280 ctx = self.contexts[tuple(key)]
281 ctx.add_lines(lines)
282
283 else:
284 if tuple(key) not in self.contexts:
285 ctx = Context(tuple(key), [])
286 self.contexts[tuple(key)] = ctx
287
288 def load_contexts(self):
289 """
290 Parse the configuration and create contexts for each appropriate block
291 """
292
293 current_context_lines = []
294 ctx_keys = []
295
296 '''
297 The end of a context is flagged via the 'end' keyword:
298
299!
300interface swp52
301 ipv6 nd suppress-ra
302 link-detect
303!
304end
305router bgp 10
306 bgp router-id 10.0.0.1
307 bgp log-neighbor-changes
308 no bgp default ipv4-unicast
309 neighbor EBGP peer-group
310 neighbor EBGP advertisement-interval 1
311 neighbor EBGP timers connect 10
312 neighbor 2001:40:1:4::6 remote-as 40
313 neighbor 2001:40:1:8::a remote-as 40
314!
315end
316 address-family ipv6
317 neighbor IBGPv6 activate
318 neighbor 2001:10::2 peer-group IBGPv6
319 neighbor 2001:10::3 peer-group IBGPv6
320 exit-address-family
321!
322end
323router ospf
324 ospf router-id 10.0.0.1
325 log-adjacency-changes detail
326 timers throttle spf 0 50 5000
327!
328end
329 '''
330
331 # The code assumes that its working on the output from the "vtysh -m"
332 # command. That provides the appropriate markers to signify end of
333 # a context. This routine uses that to build the contexts for the
334 # config.
335 #
336 # There are single line contexts such as "log file /media/node/zebra.log"
337 # and multi-line contexts such as "router ospf" and subcontexts
338 # within a context such as "address-family" within "router bgp"
339 # In each of these cases, the first line of the context becomes the
340 # key of the context. So "router bgp 10" is the key for the non-address
341 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
342 # the key for the subcontext and so on.
2fc76430
DS
343 ctx_keys = []
344 main_ctx_key = []
345 new_ctx = True
2fc76430
DS
346
347 # the keywords that we know are single line contexts. bgp in this case
348 # is not the main router bgp block, but enabling multi-instance
2fed5dcd 349 oneline_ctx_keywords = ("access-list ",
e80c8c55 350 "agentx",
2fed5dcd
DW
351 "bgp ",
352 "debug ",
353 "dump ",
354 "enable ",
825be4c2 355 "frr ",
2fed5dcd
DW
356 "hostname ",
357 "ip ",
358 "ipv6 ",
359 "log ",
45cb21bf 360 "mpls",
a11209a7 361 "no ",
2fed5dcd
DW
362 "password ",
363 "ptm-enable",
364 "router-id ",
365 "service ",
366 "table ",
367 "username ",
368 "zebra ")
369
2fc76430
DS
370 for line in self.lines:
371
372 if not line:
373 continue
374
375 if line.startswith('!') or line.startswith('#'):
376 continue
377
378 # one line contexts
379 if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords):
380 self.save_contexts(ctx_keys, current_context_lines)
381
382 # Start a new context
383 main_ctx_key = []
4a2587c6 384 ctx_keys = [line, ]
2fc76430
DS
385 current_context_lines = []
386
a782e613 387 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
2fc76430
DS
388 self.save_contexts(ctx_keys, current_context_lines)
389 new_ctx = True
390
391 elif line == "end":
392 self.save_contexts(ctx_keys, current_context_lines)
a782e613 393 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
2fc76430
DS
394
395 # Start a new context
396 new_ctx = True
397 main_ctx_key = []
398 ctx_keys = []
399 current_context_lines = []
400
401 elif line == "exit-address-family" or line == "exit":
402 # if this exit is for address-family ipv4 unicast, ignore the pop
403 if main_ctx_key:
404 self.save_contexts(ctx_keys, current_context_lines)
405
406 # Start a new context
407 ctx_keys = copy.deepcopy(main_ctx_key)
408 current_context_lines = []
a782e613 409 log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
2fc76430
DS
410
411 elif new_ctx is True:
412 if not main_ctx_key:
4a2587c6 413 ctx_keys = [line, ]
2fc76430
DS
414 else:
415 ctx_keys = copy.deepcopy(main_ctx_key)
416 main_ctx_key = []
417
418 current_context_lines = []
419 new_ctx = False
a782e613 420 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
2fc76430
DS
421
422 elif "address-family " in line:
423 main_ctx_key = []
424
0b960b4d
DW
425 # Save old context first
426 self.save_contexts(ctx_keys, current_context_lines)
427 current_context_lines = []
428 main_ctx_key = copy.deepcopy(ctx_keys)
a782e613 429 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
2fc76430 430
0b960b4d
DW
431 if line == "address-family ipv6":
432 ctx_keys.append("address-family ipv6 unicast")
433 elif line == "address-family ipv4":
434 ctx_keys.append("address-family ipv4 unicast")
5014d96f
DW
435 elif line == "address-family evpn":
436 ctx_keys.append("address-family l2vpn evpn")
0b960b4d
DW
437 else:
438 ctx_keys.append(line)
2fc76430
DS
439
440 else:
441 # Continuing in an existing context, add non-commented lines to it
442 current_context_lines.append(line)
a782e613 443 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
2fc76430
DS
444
445 # Save the context of the last one
446 self.save_contexts(ctx_keys, current_context_lines)
447
4a2587c6 448
2fc76430
DS
449def line_to_vtysh_conft(ctx_keys, line, delete):
450 """
4a2587c6 451 Return the vtysh command for the specified context line
2fc76430
DS
452 """
453
454 cmd = []
455 cmd.append('vtysh')
456 cmd.append('-c')
457 cmd.append('conf t')
458
459 if line:
460 for ctx_key in ctx_keys:
461 cmd.append('-c')
462 cmd.append(ctx_key)
463
464 line = line.lstrip()
465
466 if delete:
467 cmd.append('-c')
468
469 if line.startswith('no '):
470 cmd.append('%s' % line[3:])
471 else:
472 cmd.append('no %s' % line)
473
474 else:
475 cmd.append('-c')
476 cmd.append(line)
477
478 # If line is None then we are typically deleting an entire
479 # context ('no router ospf' for example)
480 else:
481
482 if delete:
483
484 # Only put the 'no' on the last sub-context
485 for ctx_key in ctx_keys:
486 cmd.append('-c')
487
488 if ctx_key == ctx_keys[-1]:
489 cmd.append('no %s' % ctx_key)
490 else:
491 cmd.append('%s' % ctx_key)
492 else:
493 for ctx_key in ctx_keys:
494 cmd.append('-c')
495 cmd.append(ctx_key)
496
497 return cmd
498
4a2587c6
DW
499
500def line_for_vtysh_file(ctx_keys, line, delete):
501 """
e20dc2ba 502 Return the command as it would appear in frr.conf
4a2587c6
DW
503 """
504 cmd = []
505
506 if line:
507 for (i, ctx_key) in enumerate(ctx_keys):
508 cmd.append(' ' * i + ctx_key)
509
510 line = line.lstrip()
511 indent = len(ctx_keys) * ' '
512
513 if delete:
514 if line.startswith('no '):
515 cmd.append('%s%s' % (indent, line[3:]))
516 else:
517 cmd.append('%sno %s' % (indent, line))
518
519 else:
520 cmd.append(indent + line)
521
522 # If line is None then we are typically deleting an entire
523 # context ('no router ospf' for example)
524 else:
525 if delete:
526
527 # Only put the 'no' on the last sub-context
528 for ctx_key in ctx_keys:
529
530 if ctx_key == ctx_keys[-1]:
531 cmd.append('no %s' % ctx_key)
532 else:
533 cmd.append('%s' % ctx_key)
534 else:
535 for ctx_key in ctx_keys:
536 cmd.append(ctx_key)
537
538 return '\n' + '\n'.join(cmd)
539
540
2fc76430
DS
541def get_normalized_ipv6_line(line):
542 """
d8e4c438 543 Return a normalized IPv6 line as produced by frr,
2fc76430 544 with all letters in lower case and trailing and leading
bb972e44
DD
545 zeros removed, and only the network portion present if
546 the IPv6 word is a network
2fc76430
DS
547 """
548 norm_line = ""
549 words = line.split(' ')
550 for word in words:
551 if ":" in word:
bb972e44
DD
552 norm_word = None
553 if "/" in word:
554 try:
555 v6word = IPNetwork(word)
556 norm_word = '%s/%s' % (v6word.network, v6word.prefixlen)
557 except ValueError:
558 pass
559 if not norm_word:
560 try:
561 norm_word = '%s' % IPv6Address(word)
0845b872 562 except ValueError:
bb972e44 563 norm_word = word
2fc76430
DS
564 else:
565 norm_word = word
566 norm_line = norm_line + " " + norm_word
567
568 return norm_line.strip()
569
4a2587c6 570
9fe88bc7
DW
571def line_exist(lines, target_ctx_keys, target_line):
572 for (ctx_keys, line) in lines:
573 if ctx_keys == target_ctx_keys and line == target_line:
574 return True
575 return False
576
577
9b166171 578def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
9fe88bc7
DW
579
580 # Quite possibly the most confusing (while accurate) variable names in history
581 lines_to_add_to_del = []
582 lines_to_del_to_del = []
583
584 for (ctx_keys, line) in lines_to_del:
9b166171
DW
585 deleted = False
586
926ea62e 587 if ctx_keys[0].startswith('router bgp') and line and line.startswith('neighbor '):
9b166171
DW
588 """
589 BGP changed how it displays swpX peers that are part of peer-group. Older
d8e4c438 590 versions of frr would display these on separate lines:
9b166171
DW
591 neighbor swp1 interface
592 neighbor swp1 peer-group FOO
593
594 but today we display via a single line
595 neighbor swp1 interface peer-group FOO
596
d8e4c438 597 This change confuses frr-reload.py so check to see if we are deleting
9b166171
DW
598 neighbor swp1 interface peer-group FOO
599
600 and adding
601 neighbor swp1 interface
602 neighbor swp1 peer-group FOO
603
604 If so then chop the del line and the corresponding add lines
605 """
606
9fe88bc7 607 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
9b166171 608 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
9fe88bc7 609
9b166171
DW
610 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
611 swpx_interface = None
612 swpx_peergroup = None
613
614 if re_swpx_int_peergroup:
615 swpx = re_swpx_int_peergroup.group(1)
616 peergroup = re_swpx_int_peergroup.group(2)
617 swpx_interface = "neighbor %s interface" % swpx
618 elif re_swpx_int_v6only_peergroup:
619 swpx = re_swpx_int_v6only_peergroup.group(1)
620 peergroup = re_swpx_int_v6only_peergroup.group(2)
621 swpx_interface = "neighbor %s interface v6only" % swpx
9fe88bc7 622
9b166171 623 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
9fe88bc7
DW
624 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
625 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
b1e0634c 626 tmp_ctx_keys = tuple(list(ctx_keys))
9b166171
DW
627
628 if not found_add_swpx_peergroup:
629 tmp_ctx_keys = list(ctx_keys)
630 tmp_ctx_keys.append('address-family ipv4 unicast')
631 tmp_ctx_keys = tuple(tmp_ctx_keys)
632 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
633
634 if not found_add_swpx_peergroup:
635 tmp_ctx_keys = list(ctx_keys)
636 tmp_ctx_keys.append('address-family ipv6 unicast')
637 tmp_ctx_keys = tuple(tmp_ctx_keys)
638 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
9fe88bc7
DW
639
640 if found_add_swpx_interface and found_add_swpx_peergroup:
9b166171 641 deleted = True
9fe88bc7
DW
642 lines_to_del_to_del.append((ctx_keys, line))
643 lines_to_add_to_del.append((ctx_keys, swpx_interface))
9b166171
DW
644 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
645
b3a39dc5
DD
646 """
647 In 3.0.1 we changed how we display neighbor interface command. Older
d8e4c438 648 versions of frr would display the following:
b3a39dc5
DD
649 neighbor swp1 interface
650 neighbor swp1 remote-as external
651 neighbor swp1 capability extended-nexthop
652
653 but today we display via a single line
654 neighbor swp1 interface remote-as external
655
656 and capability extended-nexthop is no longer needed because we
657 automatically enable it when the neighbor is of type interface.
658
d8e4c438 659 This change confuses frr-reload.py so check to see if we are deleting
b3a39dc5
DD
660 neighbor swp1 interface remote-as (external|internal|ASNUM)
661
662 and adding
663 neighbor swp1 interface
664 neighbor swp1 remote-as (external|internal|ASNUM)
665 neighbor swp1 capability extended-nexthop
666
667 If so then chop the del line and the corresponding add lines
668 """
669 re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
670 re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
671
672 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
673 swpx_interface = None
674 swpx_remoteas = None
675
676 if re_swpx_int_remoteas:
677 swpx = re_swpx_int_remoteas.group(1)
678 remoteas = re_swpx_int_remoteas.group(2)
679 swpx_interface = "neighbor %s interface" % swpx
680 elif re_swpx_int_v6only_remoteas:
681 swpx = re_swpx_int_v6only_remoteas.group(1)
682 remoteas = re_swpx_int_v6only_remoteas.group(2)
683 swpx_interface = "neighbor %s interface v6only" % swpx
684
685 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
686 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
687 found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
688 tmp_ctx_keys = tuple(list(ctx_keys))
689
690 if found_add_swpx_interface and found_add_swpx_remoteas:
691 deleted = True
692 lines_to_del_to_del.append((ctx_keys, line))
693 lines_to_add_to_del.append((ctx_keys, swpx_interface))
694 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
695
78e31f46
DD
696 '''
697 In 3.0, we made bgp bestpath multipath as-relax command
698 automatically assume no-as-set since the lack of this option caused
699 weird routing problems and this problem was peculiar to this
700 implementation. When the running config is shown in relases after
701 3.0, the no-as-set is not shown as its the default. This causes
702 reload to unnecessarily unapply this option to only apply it back
703 again, causing unnecessary session resets. Handle this.
704 '''
705 if ctx_keys[0].startswith('router bgp') and line and 'multipath-relax' in line:
706 re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line)
707 old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set'
708 found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
709
710 if re_asrelax_new and found_asrelax_old:
711 deleted = True
712 lines_to_del_to_del.append((ctx_keys, line))
713 lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
714
715 '''
716 More old-to-new config handling. ip import-table no longer accepts
717 distance, but we honor the old syntax. But 'show running' shows only
718 the new syntax. This causes an unnecessary 'no import-table' followed
719 by the same old 'ip import-table' which causes perturbations in
720 announced routes leading to traffic blackholes. Fix this issue.
721 '''
722 re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0])
723 if re_importtbl:
724 table_num = re_importtbl.group(1)
725 for ctx in lines_to_add:
726 if ctx[0][0].startswith('ip import-table %s distance' % table_num):
78e31f46
DD
727 lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
728 lines_to_add_to_del.append((ctx[0], None))
0bf7cc28
DD
729
730 '''
731 ip/ipv6 prefix-list can be specified without a seq number. However,
732 the running config always adds 'seq x', where x is a number incremented
733 by 5 for every element, to the prefix list. So, ignore such lines as
734 well. Sample prefix-list lines:
735 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
736 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
737 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
738 '''
739 re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
740 ctx_keys[0])
741 if re_ip_pfxlst:
742 tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) +
743 re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) +
744 re_ip_pfxlst.group(6))
745 for ctx in lines_to_add:
746 if ctx[0][0] == tmpline:
747 lines_to_del_to_del.append((ctx_keys, None))
748 lines_to_add_to_del.append(((tmpline,), None))
749
5014d96f
DW
750 if (len(ctx_keys) == 3 and
751 ctx_keys[0].startswith('router bgp') and
752 ctx_keys[1] == 'address-family l2vpn evpn' and
753 ctx_keys[2].startswith('vni')):
754
755 re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False
756
757 if re_route_target:
758 rt = re_route_target.group(1).strip()
759 route_target_import_line = line
760 route_target_export_line = "route-target export %s" % rt
761 route_target_both_line = "route-target both %s" % rt
762
763 found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line)
764 found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line)
765
766 '''
767 If the running configs has
768 route-target import 1:1
769 route-target export 1:1
770
771 and the config we are reloading against has
772 route-target both 1:1
773
774 then we can ignore deleting the import/export and ignore adding the 'both'
775 '''
776 if found_route_target_export_line and found_route_target_both_line:
777 lines_to_del_to_del.append((ctx_keys, route_target_import_line))
778 lines_to_del_to_del.append((ctx_keys, route_target_export_line))
779 lines_to_add_to_del.append((ctx_keys, route_target_both_line))
780
9b166171
DW
781 if not deleted:
782 found_add_line = line_exist(lines_to_add, ctx_keys, line)
783
784 if found_add_line:
785 lines_to_del_to_del.append((ctx_keys, line))
786 lines_to_add_to_del.append((ctx_keys, line))
787 else:
788 '''
789 We have commands that used to be displayed in the global part
790 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
791
792 # old way
793 router bgp 64900
794 neighbor ISL advertisement-interval 0
795
796 vs.
797
798 # new way
799 router bgp 64900
800 address-family ipv4 unicast
801 neighbor ISL advertisement-interval 0
802
803 Look to see if we are deleting it in one format just to add it back in the other
804 '''
805 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
806 tmp_ctx_keys = list(ctx_keys)[:-1]
807 tmp_ctx_keys = tuple(tmp_ctx_keys)
808
809 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
810
811 if found_add_line:
812 lines_to_del_to_del.append((ctx_keys, line))
813 lines_to_add_to_del.append((tmp_ctx_keys, line))
9fe88bc7
DW
814
815 for (ctx_keys, line) in lines_to_del_to_del:
816 lines_to_del.remove((ctx_keys, line))
817
818 for (ctx_keys, line) in lines_to_add_to_del:
819 lines_to_add.remove((ctx_keys, line))
820
821 return (lines_to_add, lines_to_del)
822
823
2fc76430
DS
824def compare_context_objects(newconf, running):
825 """
826 Create a context diff for the two specified contexts
827 """
828
829 # Compare the two Config objects to find the lines that we need to add/del
830 lines_to_add = []
831 lines_to_del = []
926ea62e 832 delete_bgpd = False
2fc76430
DS
833
834 # Find contexts that are in newconf but not in running
835 # Find contexts that are in running but not in newconf
836 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
837
838 if running_ctx_keys not in newconf.contexts:
839
ab5f8310
DW
840 # We check that the len is 1 here so that we only look at ('router bgp 10')
841 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
926ea62e 842 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
ab5f8310
DW
843 # running but not in newconf.
844 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
926ea62e
DW
845 delete_bgpd = True
846 lines_to_del.append((running_ctx_keys, None))
847
1a8c43f1 848 # We cannot do 'no interface' in FRR, and so deal with it
768bf950
DD
849 elif running_ctx_keys[0].startswith('interface'):
850 for line in running_ctx.lines:
851 lines_to_del.append((running_ctx_keys, line))
852
926ea62e
DW
853 # If this is an address-family under 'router bgp' and we are already deleting the
854 # entire 'router bgp' context then ignore this sub-context
855 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
76f69d1c 856 continue
514665b9 857
5014d96f
DW
858 # Delete an entire vni sub-context under "address-family l2vpn evpn"
859 elif ("router bgp" in running_ctx_keys[0] and
860 len(running_ctx_keys) > 2 and
861 running_ctx_keys[1].startswith('address-family l2vpn evpn') and
862 running_ctx_keys[2].startswith('vni ')):
863 lines_to_del.append((running_ctx_keys, None))
864
afa2e8e1
DW
865 elif ("router bgp" in running_ctx_keys[0] and
866 len(running_ctx_keys) > 1 and
867 running_ctx_keys[1].startswith('address-family')):
868 # There's no 'no address-family' support and so we have to
869 # delete each line individually again
870 for line in running_ctx.lines:
871 lines_to_del.append((running_ctx_keys, line))
872
2fc76430 873 # Non-global context
926ea62e 874 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
875 lines_to_del.append((running_ctx_keys, None))
876
877 # Global context
878 else:
879 for line in running_ctx.lines:
880 lines_to_del.append((running_ctx_keys, line))
881
882 # Find the lines within each context to add
883 # Find the lines within each context to del
884 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
885
886 if newconf_ctx_keys in running.contexts:
887 running_ctx = running.contexts[newconf_ctx_keys]
888
889 for line in newconf_ctx.lines:
890 if line not in running_ctx.dlines:
891 lines_to_add.append((newconf_ctx_keys, line))
892
893 for line in running_ctx.lines:
894 if line not in newconf_ctx.dlines:
895 lines_to_del.append((newconf_ctx_keys, line))
896
897 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
898
899 if newconf_ctx_keys not in running.contexts:
900 lines_to_add.append((newconf_ctx_keys, None))
901
902 for line in newconf_ctx.lines:
903 lines_to_add.append((newconf_ctx_keys, line))
904
9b166171 905 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
9fe88bc7 906
926ea62e 907 return (lines_to_add, lines_to_del)
2fc76430
DS
908
909if __name__ == '__main__':
910 # Command line options
d8e4c438 911 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
2fc76430
DS
912 parser.add_argument('--input', help='Read running config from file instead of "show running"')
913 group = parser.add_mutually_exclusive_group(required=True)
914 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
915 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
916 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
cc146ecc 917 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
d8e4c438 918 parser.add_argument('filename', help='Location of new frr config file')
e20dc2ba 919 parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
2fc76430
DS
920 args = parser.parse_args()
921
922 # Logging
923 # For --test log to stdout
d8e4c438 924 # For --reload log to /var/log/frr/frr-reload.log
cc146ecc 925 if args.test or args.stdout:
c50aceee 926 logging.basicConfig(level=logging.INFO,
2fc76430 927 format='%(asctime)s %(levelname)5s: %(message)s')
926ea62e
DW
928
929 # Color the errors and warnings in red
930 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
931 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
932
2fc76430 933 elif args.reload:
d8e4c438
DS
934 if not os.path.isdir('/var/log/frr/'):
935 os.makedirs('/var/log/frr/')
2fc76430 936
d8e4c438 937 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
c50aceee 938 level=logging.INFO,
2fc76430
DS
939 format='%(asctime)s %(levelname)5s: %(message)s')
940
941 # argparse should prevent this from happening but just to be safe...
942 else:
943 raise Exception('Must specify --reload or --test')
a782e613 944 log = logging.getLogger(__name__)
2fc76430 945
76f69d1c
DW
946 # Verify the new config file is valid
947 if not os.path.isfile(args.filename):
825be4c2
DW
948 msg = "Filename %s does not exist" % args.filename
949 print msg
950 log.error(msg)
76f69d1c
DW
951 sys.exit(1)
952
953 if not os.path.getsize(args.filename):
825be4c2
DW
954 msg = "Filename %s is an empty file" % args.filename
955 print msg
956 log.error(msg)
76f69d1c
DW
957 sys.exit(1)
958
76f69d1c 959 # Verify that 'service integrated-vtysh-config' is configured
d8e4c438 960 vtysh_filename = '/etc/frr/vtysh.conf'
6ac9179c 961 service_integrated_vtysh_config = True
76f69d1c 962
f850d14d
DW
963 if os.path.isfile(vtysh_filename):
964 with open(vtysh_filename, 'r') as fh:
965 for line in fh.readlines():
966 line = line.strip()
76f69d1c 967
6ac9179c
DD
968 if line == 'no service integrated-vtysh-config':
969 service_integrated_vtysh_config = False
f850d14d 970 break
76f69d1c
DW
971
972 if not service_integrated_vtysh_config:
825be4c2
DW
973 msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
974 print msg
975 log.error(msg)
76f69d1c 976 sys.exit(1)
2fc76430 977
c50aceee 978 if args.debug:
a782e613 979 log.setLevel(logging.DEBUG)
c50aceee 980
a782e613 981 log.info('Called via "%s"', str(args))
c50aceee 982
2fc76430
DS
983 # Create a Config object from the config generated by newconf
984 newconf = Config()
985 newconf.load_from_file(args.filename)
825be4c2 986 reload_ok = True
2fc76430
DS
987
988 if args.test:
989
990 # Create a Config object from the running config
991 running = Config()
992
993 if args.input:
994 running.load_from_file(args.input)
995 else:
996 running.load_from_show_running()
997
926ea62e 998 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
4a2587c6 999 lines_to_configure = []
2fc76430
DS
1000
1001 if lines_to_del:
1002 print "\nLines To Delete"
1003 print "==============="
1004
1005 for (ctx_keys, line) in lines_to_del:
1006
1007 if line == '!':
1008 continue
1009
4a2587c6
DW
1010 cmd = line_for_vtysh_file(ctx_keys, line, True)
1011 lines_to_configure.append(cmd)
9fe88bc7 1012 print cmd
2fc76430
DS
1013
1014 if lines_to_add:
1015 print "\nLines To Add"
1016 print "============"
1017
1018 for (ctx_keys, line) in lines_to_add:
1019
1020 if line == '!':
1021 continue
1022
4a2587c6
DW
1023 cmd = line_for_vtysh_file(ctx_keys, line, False)
1024 lines_to_configure.append(cmd)
9fe88bc7 1025 print cmd
2fc76430 1026
2fc76430
DS
1027 elif args.reload:
1028
d8e4c438 1029 log.debug('New Frr Config\n%s', newconf.get_lines())
2fc76430
DS
1030
1031 # This looks a little odd but we have to do this twice...here is why
1032 # If the user had this running bgp config:
4a2587c6 1033 #
2fc76430
DS
1034 # router bgp 10
1035 # neighbor 1.1.1.1 remote-as 50
1036 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 1037 #
2fc76430 1038 # and this config in the newconf config file
4a2587c6 1039 #
2fc76430
DS
1040 # router bgp 10
1041 # neighbor 1.1.1.1 remote-as 999
1042 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
1043 #
1044 #
2fc76430
DS
1045 # Then the script will do
1046 # - no neighbor 1.1.1.1 remote-as 50
1047 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 1048 #
2fc76430
DS
1049 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1050 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1051 # configs again to put this line back.
1052
1a8c43f1 1053 # There are many keywords in FRR that can only appear one time under
2ce26af1
DW
1054 # a context, take "bgp router-id" for example. If the config that we are
1055 # reloading against has the following:
1056 #
1057 # router bgp 10
1058 # bgp router-id 1.1.1.1
1059 # bgp router-id 2.2.2.2
1060 #
1061 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1062 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1063 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1064 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1065 # second pass to include all of the "adds" from the first pass.
1066 lines_to_add_first_pass = []
1067
2fc76430
DS
1068 for x in range(2):
1069 running = Config()
1070 running.load_from_show_running()
d8e4c438 1071 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 1072
926ea62e 1073 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2fc76430 1074
2ce26af1
DW
1075 if x == 0:
1076 lines_to_add_first_pass = lines_to_add
1077 else:
1078 lines_to_add.extend(lines_to_add_first_pass)
1079
53bddc22 1080 # Only do deletes on the first pass. The reason being if we
1a8c43f1 1081 # configure a bgp neighbor via "neighbor swp1 interface" FRR
53bddc22
DW
1082 # will automatically add:
1083 #
1084 # interface swp1
1085 # ipv6 nd ra-interval 10
1086 # no ipv6 nd suppress-ra
1087 # !
1088 #
1089 # but those lines aren't in the config we are reloading against so
1090 # on the 2nd pass they will show up in lines_to_del. This could
1091 # apply to other scenarios as well where configuring FOO adds BAR
1092 # to the config.
1093 if lines_to_del and x == 0:
2fc76430
DS
1094 for (ctx_keys, line) in lines_to_del:
1095
1096 if line == '!':
1097 continue
1098
4a2587c6
DW
1099 # 'no' commands are tricky, we can't just put them in a file and
1100 # vtysh -f that file. See the next comment for an explanation
1101 # of their quirks
2fc76430
DS
1102 cmd = line_to_vtysh_conft(ctx_keys, line, True)
1103 original_cmd = cmd
1104
d8e4c438 1105 # Some commands in frr are picky about taking a "no" of the entire line.
76f69d1c
DW
1106 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1107 # only the beginning. If we hit one of these command an exception will be
1108 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 1109 #
76f69d1c 1110 # Example:
d8e4c438
DS
1111 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1112 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 1113 # % Unknown command.
d8e4c438 1114 # frr(config-if)# no ip ospf authentication message-digest
76f69d1c 1115 # % Unknown command.
d8e4c438
DS
1116 # frr(config-if)# no ip ospf authentication
1117 # frr(config-if)#
2fc76430
DS
1118
1119 while True:
2fc76430
DS
1120 try:
1121 _ = subprocess.check_output(cmd)
1122
1123 except subprocess.CalledProcessError:
1124
1125 # - Pull the last entry from cmd (this would be
1126 # 'no ip ospf authentication message-digest 1.1.1.1' in
1127 # our example above
1128 # - Split that last entry by whitespace and drop the last word
825be4c2 1129 log.info('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
1130 last_arg = cmd[-1].split(' ')
1131
1132 if len(last_arg) <= 2:
a782e613 1133 log.error('"%s" we failed to remove this command', original_cmd)
2fc76430
DS
1134 break
1135
1136 new_last_arg = last_arg[0:-1]
1137 cmd[-1] = ' '.join(new_last_arg)
1138 else:
a782e613 1139 log.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
1140 break
1141
2fc76430 1142 if lines_to_add:
4a2587c6
DW
1143 lines_to_configure = []
1144
2fc76430
DS
1145 for (ctx_keys, line) in lines_to_add:
1146
1147 if line == '!':
1148 continue
1149
4a2587c6
DW
1150 cmd = line_for_vtysh_file(ctx_keys, line, False)
1151 lines_to_configure.append(cmd)
1152
1153 if lines_to_configure:
1154 random_string = ''.join(random.SystemRandom().choice(
1155 string.ascii_uppercase +
1156 string.digits) for _ in range(6))
1157
d8e4c438 1158 filename = "/var/run/frr/reload-%s.txt" % random_string
a782e613 1159 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
4a2587c6
DW
1160
1161 with open(filename, 'w') as fh:
1162 for line in lines_to_configure:
1163 fh.write(line + '\n')
825be4c2
DW
1164
1165 output = subprocess.check_output(['/usr/bin/vtysh', '-f', filename])
1166
1167 # exit non-zero if we see these errors
1168 for x in ('BGP instance name and AS number mismatch',
1169 'BGP instance is already running',
1170 '% not a local address'):
1171 for line in output.splitlines():
1172 if x in line:
1173 msg = "ERROR: %s" % x
1174 log.error(msg)
1175 print msg
1176 reload_ok = False
1177
4a2587c6 1178 os.unlink(filename)
2fc76430 1179
4b78098d 1180 # Make these changes persistent
e20dc2ba 1181 if args.overwrite or args.filename != '/etc/frr/frr.conf':
926ea62e 1182 subprocess.call(['/usr/bin/vtysh', '-c', 'write'])
825be4c2
DW
1183
1184 if not reload_ok:
1185 sys.exit(1)