]> git.proxmox.com Git - mirror_frr.git/blob - tools/frr-reload.py
Merge branch 'pull/134' with changes
[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 "hostname ",
350 "ip ",
351 "ipv6 ",
352 "log ",
353 "password ",
354 "ptm-enable",
355 "router-id ",
356 "service ",
357 "table ",
358 "username ",
359 "zebra ")
360
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 = []
375 ctx_keys = [line, ]
376 current_context_lines = []
377
378 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
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)
384 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
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 = []
400 log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
401
402 elif new_ctx is True:
403 if not main_ctx_key:
404 ctx_keys = [line, ]
405 else:
406 ctx_keys = copy.deepcopy(main_ctx_key)
407 main_ctx_key = []
408
409 current_context_lines = []
410 new_ctx = False
411 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
412
413 elif "address-family " in line:
414 main_ctx_key = []
415
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)
420 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
421
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)
428
429 else:
430 # Continuing in an existing context, add non-commented lines to it
431 current_context_lines.append(line)
432 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
433
434 # Save the context of the last one
435 self.save_contexts(ctx_keys, current_context_lines)
436
437
438 def line_to_vtysh_conft(ctx_keys, line, delete):
439 """
440 Return the vtysh command for the specified context line
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
488
489 def line_for_vtysh_file(ctx_keys, line, delete):
490 """
491 Return the command as it would appear in Frr.conf
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
530 def get_normalized_ipv6_line(line):
531 """
532 Return a normalized IPv6 line as produced by frr,
533 with all letters in lower case and trailing and leading
534 zeros removed, and only the network portion present if
535 the IPv6 word is a network
536 """
537 norm_line = ""
538 words = line.split(' ')
539 for word in words:
540 if ":" in word:
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)
551 except ValueError:
552 norm_word = word
553 else:
554 norm_word = word
555 norm_line = norm_line + " " + norm_word
556
557 return norm_line.strip()
558
559
560 def 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
567 def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
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:
574 deleted = False
575
576 if ctx_keys[0].startswith('router bgp') and line and line.startswith('neighbor '):
577 """
578 BGP changed how it displays swpX peers that are part of peer-group. Older
579 versions of frr would display these on separate lines:
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
586 This change confuses frr-reload.py so check to see if we are deleting
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
596 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
597 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
598
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
611
612 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
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)
615 tmp_ctx_keys = tuple(list(ctx_keys))
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)
628
629 if found_add_swpx_interface and found_add_swpx_peergroup:
630 deleted = True
631 lines_to_del_to_del.append((ctx_keys, line))
632 lines_to_add_to_del.append((ctx_keys, swpx_interface))
633 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
634
635 """
636 In 3.0.1 we changed how we display neighbor interface command. Older
637 versions of frr would display the following:
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
648 This change confuses frr-reload.py so check to see if we are deleting
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
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):
716 lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
717 lines_to_add_to_del.append((ctx[0], None))
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
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))
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
782 def 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 = []
790 delete_bgpd = False
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
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
800 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
801 # running but not in newconf.
802 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
803 delete_bgpd = True
804 lines_to_del.append((running_ctx_keys, None))
805
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
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:
814 continue
815
816 # Non-global context
817 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
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
848 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
849
850 return (lines_to_add, lines_to_del)
851
852 if __name__ == '__main__':
853 # Command line options
854 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
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)
860 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
861 parser.add_argument('filename', help='Location of new frr config file')
862 parser.add_argument('--overwrite', action='store_true', help='Overwrite Quagga.conf with running config output', default=False)
863 args = parser.parse_args()
864
865 # Logging
866 # For --test log to stdout
867 # For --reload log to /var/log/frr/frr-reload.log
868 if args.test or args.stdout:
869 logging.basicConfig(level=logging.INFO,
870 format='%(asctime)s %(levelname)5s: %(message)s')
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
876 elif args.reload:
877 if not os.path.isdir('/var/log/frr/'):
878 os.makedirs('/var/log/frr/')
879
880 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
881 level=logging.INFO,
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')
887 log = logging.getLogger(__name__)
888
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
898 # Verify that 'service integrated-vtysh-config' is configured
899 vtysh_filename = '/etc/frr/vtysh.conf'
900 service_integrated_vtysh_config = True
901
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()
906
907 if line == 'no service integrated-vtysh-config':
908 service_integrated_vtysh_config = False
909 break
910
911 if not service_integrated_vtysh_config:
912 print "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
913 sys.exit(1)
914
915 if args.debug:
916 log.setLevel(logging.DEBUG)
917
918 log.info('Called via "%s"', str(args))
919
920 # Create a Config object from the config generated by newconf
921 newconf = Config()
922 newconf.load_from_file(args.filename)
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
934 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
935 lines_to_configure = []
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
946 cmd = line_for_vtysh_file(ctx_keys, line, True)
947 lines_to_configure.append(cmd)
948 print cmd
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
959 cmd = line_for_vtysh_file(ctx_keys, line, False)
960 lines_to_configure.append(cmd)
961 print cmd
962
963 elif args.reload:
964
965 log.debug('New Frr Config\n%s', newconf.get_lines())
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:
969 #
970 # router bgp 10
971 # neighbor 1.1.1.1 remote-as 50
972 # neighbor 1.1.1.1 route-map FOO out
973 #
974 # and this config in the newconf config file
975 #
976 # router bgp 10
977 # neighbor 1.1.1.1 remote-as 999
978 # neighbor 1.1.1.1 route-map FOO out
979 #
980 #
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
984 #
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()
992 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
993
994 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
995
996 if lines_to_del:
997 for (ctx_keys, line) in lines_to_del:
998
999 if line == '!':
1000 continue
1001
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
1005 cmd = line_to_vtysh_conft(ctx_keys, line, True)
1006 original_cmd = cmd
1007
1008 # Some commands in frr are picky about taking a "no" of the entire line.
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.
1012 #
1013 # Example:
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
1016 # % Unknown command.
1017 # frr(config-if)# no ip ospf authentication message-digest
1018 # % Unknown command.
1019 # frr(config-if)# no ip ospf authentication
1020 # frr(config-if)#
1021
1022 while True:
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
1032 log.warning('Failed to execute %s', ' '.join(cmd))
1033 last_arg = cmd[-1].split(' ')
1034
1035 if len(last_arg) <= 2:
1036 log.error('"%s" we failed to remove this command', original_cmd)
1037 break
1038
1039 new_last_arg = last_arg[0:-1]
1040 cmd[-1] = ' '.join(new_last_arg)
1041 else:
1042 log.info('Executed "%s"', ' '.join(cmd))
1043 break
1044
1045 if lines_to_add:
1046 lines_to_configure = []
1047
1048 for (ctx_keys, line) in lines_to_add:
1049
1050 if line == '!':
1051 continue
1052
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
1061 filename = "/var/run/frr/reload-%s.txt" % random_string
1062 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
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)
1069
1070 # Make these changes persistent
1071 if args.overwrite or args.filename != '/etc/quagga/Quagga.conf':
1072 subprocess.call(['/usr/bin/vtysh', '-c', 'write'])