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