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