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