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