]> git.proxmox.com Git - mirror_frr.git/blob - tools/frr-reload.py
tools: frr-reload.py should exit non-zero when "set src x.x.x.x" fails
[mirror_frr.git] / tools / frr-reload.py
1 #!/usr/bin/python
2 # Frr Reloader
3 # Copyright (C) 2014 Cumulus Networks, Inc.
4 #
5 # This file is part of Frr.
6 #
7 # Frr is free software; you can redistribute it and/or modify it
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 #
12 # Frr is distributed in the hope that it will be useful, but
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
18 # along with Frr; see the file COPYING. If not, write to the Free
19 # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
20 # 02111-1307, USA.
21 #
22 """
23 This program
24 - reads a frr configuration text file
25 - reads frr's current running configuration via "vtysh -c 'show running'"
26 - compares the two configs and determines what commands to execute to
27 synchronize frr's running configuration with the configuation in the
28 text file
29 """
30
31 import argparse
32 import copy
33 import logging
34 import os
35 import random
36 import re
37 import string
38 import subprocess
39 import sys
40 from collections import OrderedDict
41 from ipaddr import IPv6Address, IPNetwork
42 from pprint import pformat
43
44
45 log = logging.getLogger(__name__)
46
47
48 class VtyshMarkException(Exception):
49 pass
50
51
52 class Context(object):
53
54 """
55 A Context object represents a section of frr configuration such as:
56 !
57 interface swp3
58 description swp3 -> r8's swp1
59 ipv6 nd suppress-ra
60 link-detect
61 !
62
63 or a single line context object such as this:
64
65 ip 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
91 class Config(object):
92
93 """
94 A frr configuration is stored in a Config object. A Config object
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 """
109 log.info('Loading Config object from file %s', filename)
110
111 try:
112 file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename])
113 except subprocess.CalledProcessError as e:
114 raise VtyshMarkException(str(e))
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 """
132 log.info('Loading Config object from vtysh show running')
133
134 try:
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)
138 except subprocess.CalledProcessError as e:
139 raise VtyshMarkException(str(e))
140
141 for line in config_text.split('\n'):
142 line = line.strip()
143
144 if (line == 'Building configuration...' or
145 line == 'Current configuration:' or
146 not line):
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
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|ipv6)\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] = '%s route %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 else:
210 newaddr = addr
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)
252 except ValueError:
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
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 !
295 interface swp52
296 ipv6 nd suppress-ra
297 link-detect
298 !
299 end
300 router 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 !
310 end
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 !
317 end
318 router ospf
319 ospf router-id 10.0.0.1
320 log-adjacency-changes detail
321 timers throttle spf 0 50 5000
322 !
323 end
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.
338 ctx_keys = []
339 main_ctx_key = []
340 new_ctx = True
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
344 oneline_ctx_keywords = ("access-list ",
345 "bgp ",
346 "debug ",
347 "dump ",
348 "enable ",
349 "frr ",
350 "hostname ",
351 "ip ",
352 "ipv6 ",
353 "log ",
354 "password ",
355 "ptm-enable",
356 "router-id ",
357 "service ",
358 "table ",
359 "username ",
360 "zebra ")
361
362 for line in self.lines:
363
364 if not line:
365 continue
366
367 if line.startswith('!') or line.startswith('#'):
368 continue
369
370 # one line contexts
371 if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords):
372 self.save_contexts(ctx_keys, current_context_lines)
373
374 # Start a new context
375 main_ctx_key = []
376 ctx_keys = [line, ]
377 current_context_lines = []
378
379 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
380 self.save_contexts(ctx_keys, current_context_lines)
381 new_ctx = True
382
383 elif line == "end":
384 self.save_contexts(ctx_keys, current_context_lines)
385 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
386
387 # Start a new context
388 new_ctx = True
389 main_ctx_key = []
390 ctx_keys = []
391 current_context_lines = []
392
393 elif line == "exit-address-family" or line == "exit":
394 # if this exit is for address-family ipv4 unicast, ignore the pop
395 if main_ctx_key:
396 self.save_contexts(ctx_keys, current_context_lines)
397
398 # Start a new context
399 ctx_keys = copy.deepcopy(main_ctx_key)
400 current_context_lines = []
401 log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
402
403 elif new_ctx is True:
404 if not main_ctx_key:
405 ctx_keys = [line, ]
406 else:
407 ctx_keys = copy.deepcopy(main_ctx_key)
408 main_ctx_key = []
409
410 current_context_lines = []
411 new_ctx = False
412 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
413
414 elif "address-family " in line:
415 main_ctx_key = []
416
417 # Save old context first
418 self.save_contexts(ctx_keys, current_context_lines)
419 current_context_lines = []
420 main_ctx_key = copy.deepcopy(ctx_keys)
421 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
422
423 if line == "address-family ipv6":
424 ctx_keys.append("address-family ipv6 unicast")
425 elif line == "address-family ipv4":
426 ctx_keys.append("address-family ipv4 unicast")
427 else:
428 ctx_keys.append(line)
429
430 else:
431 # Continuing in an existing context, add non-commented lines to it
432 current_context_lines.append(line)
433 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
434
435 # Save the context of the last one
436 self.save_contexts(ctx_keys, current_context_lines)
437
438
439 def line_to_vtysh_conft(ctx_keys, line, delete):
440 """
441 Return the vtysh command for the specified context line
442 """
443
444 cmd = []
445 cmd.append('vtysh')
446 cmd.append('-c')
447 cmd.append('conf t')
448
449 if line:
450 for ctx_key in ctx_keys:
451 cmd.append('-c')
452 cmd.append(ctx_key)
453
454 line = line.lstrip()
455
456 if delete:
457 cmd.append('-c')
458
459 if line.startswith('no '):
460 cmd.append('%s' % line[3:])
461 else:
462 cmd.append('no %s' % line)
463
464 else:
465 cmd.append('-c')
466 cmd.append(line)
467
468 # If line is None then we are typically deleting an entire
469 # context ('no router ospf' for example)
470 else:
471
472 if delete:
473
474 # Only put the 'no' on the last sub-context
475 for ctx_key in ctx_keys:
476 cmd.append('-c')
477
478 if ctx_key == ctx_keys[-1]:
479 cmd.append('no %s' % ctx_key)
480 else:
481 cmd.append('%s' % ctx_key)
482 else:
483 for ctx_key in ctx_keys:
484 cmd.append('-c')
485 cmd.append(ctx_key)
486
487 return cmd
488
489
490 def line_for_vtysh_file(ctx_keys, line, delete):
491 """
492 Return the command as it would appear in frr.conf
493 """
494 cmd = []
495
496 if line:
497 for (i, ctx_key) in enumerate(ctx_keys):
498 cmd.append(' ' * i + ctx_key)
499
500 line = line.lstrip()
501 indent = len(ctx_keys) * ' '
502
503 if delete:
504 if line.startswith('no '):
505 cmd.append('%s%s' % (indent, line[3:]))
506 else:
507 cmd.append('%sno %s' % (indent, line))
508
509 else:
510 cmd.append(indent + line)
511
512 # If line is None then we are typically deleting an entire
513 # context ('no router ospf' for example)
514 else:
515 if delete:
516
517 # Only put the 'no' on the last sub-context
518 for ctx_key in ctx_keys:
519
520 if ctx_key == ctx_keys[-1]:
521 cmd.append('no %s' % ctx_key)
522 else:
523 cmd.append('%s' % ctx_key)
524 else:
525 for ctx_key in ctx_keys:
526 cmd.append(ctx_key)
527
528 return '\n' + '\n'.join(cmd)
529
530
531 def get_normalized_ipv6_line(line):
532 """
533 Return a normalized IPv6 line as produced by frr,
534 with all letters in lower case and trailing and leading
535 zeros removed, and only the network portion present if
536 the IPv6 word is a network
537 """
538 norm_line = ""
539 words = line.split(' ')
540 for word in words:
541 if ":" in word:
542 norm_word = None
543 if "/" in word:
544 try:
545 v6word = IPNetwork(word)
546 norm_word = '%s/%s' % (v6word.network, v6word.prefixlen)
547 except ValueError:
548 pass
549 if not norm_word:
550 try:
551 norm_word = '%s' % IPv6Address(word)
552 except ValueError:
553 norm_word = word
554 else:
555 norm_word = word
556 norm_line = norm_line + " " + norm_word
557
558 return norm_line.strip()
559
560
561 def line_exist(lines, target_ctx_keys, target_line):
562 for (ctx_keys, line) in lines:
563 if ctx_keys == target_ctx_keys and line == target_line:
564 return True
565 return False
566
567
568 def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
569
570 # Quite possibly the most confusing (while accurate) variable names in history
571 lines_to_add_to_del = []
572 lines_to_del_to_del = []
573
574 for (ctx_keys, line) in lines_to_del:
575 deleted = False
576
577 if ctx_keys[0].startswith('router bgp') and line and line.startswith('neighbor '):
578 """
579 BGP changed how it displays swpX peers that are part of peer-group. Older
580 versions of frr would display these on separate lines:
581 neighbor swp1 interface
582 neighbor swp1 peer-group FOO
583
584 but today we display via a single line
585 neighbor swp1 interface peer-group FOO
586
587 This change confuses frr-reload.py so check to see if we are deleting
588 neighbor swp1 interface peer-group FOO
589
590 and adding
591 neighbor swp1 interface
592 neighbor swp1 peer-group FOO
593
594 If so then chop the del line and the corresponding add lines
595 """
596
597 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
598 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
599
600 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
601 swpx_interface = None
602 swpx_peergroup = None
603
604 if re_swpx_int_peergroup:
605 swpx = re_swpx_int_peergroup.group(1)
606 peergroup = re_swpx_int_peergroup.group(2)
607 swpx_interface = "neighbor %s interface" % swpx
608 elif re_swpx_int_v6only_peergroup:
609 swpx = re_swpx_int_v6only_peergroup.group(1)
610 peergroup = re_swpx_int_v6only_peergroup.group(2)
611 swpx_interface = "neighbor %s interface v6only" % swpx
612
613 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
614 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
615 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
616 tmp_ctx_keys = tuple(list(ctx_keys))
617
618 if not found_add_swpx_peergroup:
619 tmp_ctx_keys = list(ctx_keys)
620 tmp_ctx_keys.append('address-family ipv4 unicast')
621 tmp_ctx_keys = tuple(tmp_ctx_keys)
622 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
623
624 if not found_add_swpx_peergroup:
625 tmp_ctx_keys = list(ctx_keys)
626 tmp_ctx_keys.append('address-family ipv6 unicast')
627 tmp_ctx_keys = tuple(tmp_ctx_keys)
628 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
629
630 if found_add_swpx_interface and found_add_swpx_peergroup:
631 deleted = True
632 lines_to_del_to_del.append((ctx_keys, line))
633 lines_to_add_to_del.append((ctx_keys, swpx_interface))
634 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
635
636 """
637 In 3.0.1 we changed how we display neighbor interface command. Older
638 versions of frr would display the following:
639 neighbor swp1 interface
640 neighbor swp1 remote-as external
641 neighbor swp1 capability extended-nexthop
642
643 but today we display via a single line
644 neighbor swp1 interface remote-as external
645
646 and capability extended-nexthop is no longer needed because we
647 automatically enable it when the neighbor is of type interface.
648
649 This change confuses frr-reload.py so check to see if we are deleting
650 neighbor swp1 interface remote-as (external|internal|ASNUM)
651
652 and adding
653 neighbor swp1 interface
654 neighbor swp1 remote-as (external|internal|ASNUM)
655 neighbor swp1 capability extended-nexthop
656
657 If so then chop the del line and the corresponding add lines
658 """
659 re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
660 re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
661
662 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
663 swpx_interface = None
664 swpx_remoteas = None
665
666 if re_swpx_int_remoteas:
667 swpx = re_swpx_int_remoteas.group(1)
668 remoteas = re_swpx_int_remoteas.group(2)
669 swpx_interface = "neighbor %s interface" % swpx
670 elif re_swpx_int_v6only_remoteas:
671 swpx = re_swpx_int_v6only_remoteas.group(1)
672 remoteas = re_swpx_int_v6only_remoteas.group(2)
673 swpx_interface = "neighbor %s interface v6only" % swpx
674
675 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
676 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
677 found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
678 tmp_ctx_keys = tuple(list(ctx_keys))
679
680 if found_add_swpx_interface and found_add_swpx_remoteas:
681 deleted = True
682 lines_to_del_to_del.append((ctx_keys, line))
683 lines_to_add_to_del.append((ctx_keys, swpx_interface))
684 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
685
686 '''
687 In 3.0, we made bgp bestpath multipath as-relax command
688 automatically assume no-as-set since the lack of this option caused
689 weird routing problems and this problem was peculiar to this
690 implementation. When the running config is shown in relases after
691 3.0, the no-as-set is not shown as its the default. This causes
692 reload to unnecessarily unapply this option to only apply it back
693 again, causing unnecessary session resets. Handle this.
694 '''
695 if ctx_keys[0].startswith('router bgp') and line and 'multipath-relax' in line:
696 re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line)
697 old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set'
698 found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
699
700 if re_asrelax_new and found_asrelax_old:
701 deleted = True
702 lines_to_del_to_del.append((ctx_keys, line))
703 lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
704
705 '''
706 More old-to-new config handling. ip import-table no longer accepts
707 distance, but we honor the old syntax. But 'show running' shows only
708 the new syntax. This causes an unnecessary 'no import-table' followed
709 by the same old 'ip import-table' which causes perturbations in
710 announced routes leading to traffic blackholes. Fix this issue.
711 '''
712 re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0])
713 if re_importtbl:
714 table_num = re_importtbl.group(1)
715 for ctx in lines_to_add:
716 if ctx[0][0].startswith('ip import-table %s distance' % table_num):
717 lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
718 lines_to_add_to_del.append((ctx[0], None))
719
720 '''
721 ip/ipv6 prefix-list can be specified without a seq number. However,
722 the running config always adds 'seq x', where x is a number incremented
723 by 5 for every element, to the prefix list. So, ignore such lines as
724 well. Sample prefix-list lines:
725 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
726 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
727 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
728 '''
729 re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
730 ctx_keys[0])
731 if re_ip_pfxlst:
732 tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) +
733 re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) +
734 re_ip_pfxlst.group(6))
735 for ctx in lines_to_add:
736 if ctx[0][0] == tmpline:
737 lines_to_del_to_del.append((ctx_keys, None))
738 lines_to_add_to_del.append(((tmpline,), None))
739
740 if not deleted:
741 found_add_line = line_exist(lines_to_add, ctx_keys, line)
742
743 if found_add_line:
744 lines_to_del_to_del.append((ctx_keys, line))
745 lines_to_add_to_del.append((ctx_keys, line))
746 else:
747 '''
748 We have commands that used to be displayed in the global part
749 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
750
751 # old way
752 router bgp 64900
753 neighbor ISL advertisement-interval 0
754
755 vs.
756
757 # new way
758 router bgp 64900
759 address-family ipv4 unicast
760 neighbor ISL advertisement-interval 0
761
762 Look to see if we are deleting it in one format just to add it back in the other
763 '''
764 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
765 tmp_ctx_keys = list(ctx_keys)[:-1]
766 tmp_ctx_keys = tuple(tmp_ctx_keys)
767
768 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
769
770 if found_add_line:
771 lines_to_del_to_del.append((ctx_keys, line))
772 lines_to_add_to_del.append((tmp_ctx_keys, line))
773
774 for (ctx_keys, line) in lines_to_del_to_del:
775 lines_to_del.remove((ctx_keys, line))
776
777 for (ctx_keys, line) in lines_to_add_to_del:
778 lines_to_add.remove((ctx_keys, line))
779
780 return (lines_to_add, lines_to_del)
781
782
783 def compare_context_objects(newconf, running):
784 """
785 Create a context diff for the two specified contexts
786 """
787
788 # Compare the two Config objects to find the lines that we need to add/del
789 lines_to_add = []
790 lines_to_del = []
791 delete_bgpd = False
792
793 # Find contexts that are in newconf but not in running
794 # Find contexts that are in running but not in newconf
795 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
796
797 if running_ctx_keys not in newconf.contexts:
798
799 # We check that the len is 1 here so that we only look at ('router bgp 10')
800 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
801 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
802 # running but not in newconf.
803 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
804 delete_bgpd = True
805 lines_to_del.append((running_ctx_keys, None))
806
807 # We cannot do 'no interface' in quagga, and so deal with it
808 elif running_ctx_keys[0].startswith('interface'):
809 for line in running_ctx.lines:
810 lines_to_del.append((running_ctx_keys, line))
811
812 # If this is an address-family under 'router bgp' and we are already deleting the
813 # entire 'router bgp' context then ignore this sub-context
814 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
815 continue
816
817 # Non-global context
818 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
819 lines_to_del.append((running_ctx_keys, None))
820
821 # Global context
822 else:
823 for line in running_ctx.lines:
824 lines_to_del.append((running_ctx_keys, line))
825
826 # Find the lines within each context to add
827 # Find the lines within each context to del
828 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
829
830 if newconf_ctx_keys in running.contexts:
831 running_ctx = running.contexts[newconf_ctx_keys]
832
833 for line in newconf_ctx.lines:
834 if line not in running_ctx.dlines:
835 lines_to_add.append((newconf_ctx_keys, line))
836
837 for line in running_ctx.lines:
838 if line not in newconf_ctx.dlines:
839 lines_to_del.append((newconf_ctx_keys, line))
840
841 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
842
843 if newconf_ctx_keys not in running.contexts:
844 lines_to_add.append((newconf_ctx_keys, None))
845
846 for line in newconf_ctx.lines:
847 lines_to_add.append((newconf_ctx_keys, line))
848
849 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
850
851 return (lines_to_add, lines_to_del)
852
853 if __name__ == '__main__':
854 # Command line options
855 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
856 parser.add_argument('--input', help='Read running config from file instead of "show running"')
857 group = parser.add_mutually_exclusive_group(required=True)
858 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
859 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
860 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
861 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
862 parser.add_argument('filename', help='Location of new frr config file')
863 parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
864 args = parser.parse_args()
865
866 # Logging
867 # For --test log to stdout
868 # For --reload log to /var/log/frr/frr-reload.log
869 if args.test or args.stdout:
870 logging.basicConfig(level=logging.INFO,
871 format='%(asctime)s %(levelname)5s: %(message)s')
872
873 # Color the errors and warnings in red
874 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
875 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
876
877 elif args.reload:
878 if not os.path.isdir('/var/log/frr/'):
879 os.makedirs('/var/log/frr/')
880
881 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
882 level=logging.INFO,
883 format='%(asctime)s %(levelname)5s: %(message)s')
884
885 # argparse should prevent this from happening but just to be safe...
886 else:
887 raise Exception('Must specify --reload or --test')
888 log = logging.getLogger(__name__)
889
890 # Verify the new config file is valid
891 if not os.path.isfile(args.filename):
892 msg = "Filename %s does not exist" % args.filename
893 print msg
894 log.error(msg)
895 sys.exit(1)
896
897 if not os.path.getsize(args.filename):
898 msg = "Filename %s is an empty file" % args.filename
899 print msg
900 log.error(msg)
901 sys.exit(1)
902
903 # Verify that 'service integrated-vtysh-config' is configured
904 vtysh_filename = '/etc/frr/vtysh.conf'
905 service_integrated_vtysh_config = True
906
907 if os.path.isfile(vtysh_filename):
908 with open(vtysh_filename, 'r') as fh:
909 for line in fh.readlines():
910 line = line.strip()
911
912 if line == 'no service integrated-vtysh-config':
913 service_integrated_vtysh_config = False
914 break
915
916 if not service_integrated_vtysh_config:
917 msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
918 print msg
919 log.error(msg)
920 sys.exit(1)
921
922 if args.debug:
923 log.setLevel(logging.DEBUG)
924
925 log.info('Called via "%s"', str(args))
926
927 # Create a Config object from the config generated by newconf
928 newconf = Config()
929 newconf.load_from_file(args.filename)
930 reload_ok = True
931
932 if args.test:
933
934 # Create a Config object from the running config
935 running = Config()
936
937 if args.input:
938 running.load_from_file(args.input)
939 else:
940 running.load_from_show_running()
941
942 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
943 lines_to_configure = []
944
945 if lines_to_del:
946 print "\nLines To Delete"
947 print "==============="
948
949 for (ctx_keys, line) in lines_to_del:
950
951 if line == '!':
952 continue
953
954 cmd = line_for_vtysh_file(ctx_keys, line, True)
955 lines_to_configure.append(cmd)
956 print cmd
957
958 if lines_to_add:
959 print "\nLines To Add"
960 print "============"
961
962 for (ctx_keys, line) in lines_to_add:
963
964 if line == '!':
965 continue
966
967 cmd = line_for_vtysh_file(ctx_keys, line, False)
968 lines_to_configure.append(cmd)
969 print cmd
970
971 elif args.reload:
972
973 log.debug('New Frr Config\n%s', newconf.get_lines())
974
975 # This looks a little odd but we have to do this twice...here is why
976 # If the user had this running bgp config:
977 #
978 # router bgp 10
979 # neighbor 1.1.1.1 remote-as 50
980 # neighbor 1.1.1.1 route-map FOO out
981 #
982 # and this config in the newconf config file
983 #
984 # router bgp 10
985 # neighbor 1.1.1.1 remote-as 999
986 # neighbor 1.1.1.1 route-map FOO out
987 #
988 #
989 # Then the script will do
990 # - no neighbor 1.1.1.1 remote-as 50
991 # - neighbor 1.1.1.1 remote-as 999
992 #
993 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
994 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
995 # configs again to put this line back.
996
997 for x in range(2):
998 running = Config()
999 running.load_from_show_running()
1000 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
1001
1002 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
1003
1004 if lines_to_del:
1005 for (ctx_keys, line) in lines_to_del:
1006
1007 if line == '!':
1008 continue
1009
1010 # 'no' commands are tricky, we can't just put them in a file and
1011 # vtysh -f that file. See the next comment for an explanation
1012 # of their quirks
1013 cmd = line_to_vtysh_conft(ctx_keys, line, True)
1014 original_cmd = cmd
1015
1016 # Some commands in frr are picky about taking a "no" of the entire line.
1017 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1018 # only the beginning. If we hit one of these command an exception will be
1019 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1020 #
1021 # Example:
1022 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1023 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1024 # % Unknown command.
1025 # frr(config-if)# no ip ospf authentication message-digest
1026 # % Unknown command.
1027 # frr(config-if)# no ip ospf authentication
1028 # frr(config-if)#
1029
1030 while True:
1031 try:
1032 _ = subprocess.check_output(cmd)
1033
1034 except subprocess.CalledProcessError:
1035
1036 # - Pull the last entry from cmd (this would be
1037 # 'no ip ospf authentication message-digest 1.1.1.1' in
1038 # our example above
1039 # - Split that last entry by whitespace and drop the last word
1040 log.info('Failed to execute %s', ' '.join(cmd))
1041 last_arg = cmd[-1].split(' ')
1042
1043 if len(last_arg) <= 2:
1044 log.error('"%s" we failed to remove this command', original_cmd)
1045 break
1046
1047 new_last_arg = last_arg[0:-1]
1048 cmd[-1] = ' '.join(new_last_arg)
1049 else:
1050 log.info('Executed "%s"', ' '.join(cmd))
1051 break
1052
1053 if lines_to_add:
1054 lines_to_configure = []
1055
1056 for (ctx_keys, line) in lines_to_add:
1057
1058 if line == '!':
1059 continue
1060
1061 cmd = line_for_vtysh_file(ctx_keys, line, False)
1062 lines_to_configure.append(cmd)
1063
1064 if lines_to_configure:
1065 random_string = ''.join(random.SystemRandom().choice(
1066 string.ascii_uppercase +
1067 string.digits) for _ in range(6))
1068
1069 filename = "/var/run/frr/reload-%s.txt" % random_string
1070 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
1071
1072 with open(filename, 'w') as fh:
1073 for line in lines_to_configure:
1074 fh.write(line + '\n')
1075
1076 output = subprocess.check_output(['/usr/bin/vtysh', '-f', filename])
1077
1078 # exit non-zero if we see these errors
1079 for x in ('BGP instance name and AS number mismatch',
1080 'BGP instance is already running',
1081 '% not a local address'):
1082 for line in output.splitlines():
1083 if x in line:
1084 msg = "ERROR: %s" % x
1085 log.error(msg)
1086 print msg
1087 reload_ok = False
1088
1089 os.unlink(filename)
1090
1091 # Make these changes persistent
1092 if args.overwrite or args.filename != '/etc/frr/frr.conf':
1093 subprocess.call(['/usr/bin/vtysh', '-c', 'write'])
1094
1095 if not reload_ok:
1096 sys.exit(1)