]> git.proxmox.com Git - mirror_frr.git/blob - tools/frr-reload.py
Merge pull request #47 from donaldsharp/valgrind
[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 if not deleted:
583 found_add_line = line_exist(lines_to_add, ctx_keys, line)
584
585 if found_add_line:
586 lines_to_del_to_del.append((ctx_keys, line))
587 lines_to_add_to_del.append((ctx_keys, line))
588 else:
589 '''
590 We have commands that used to be displayed in the global part
591 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
592
593 # old way
594 router bgp 64900
595 neighbor ISL advertisement-interval 0
596
597 vs.
598
599 # new way
600 router bgp 64900
601 address-family ipv4 unicast
602 neighbor ISL advertisement-interval 0
603
604 Look to see if we are deleting it in one format just to add it back in the other
605 '''
606 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
607 tmp_ctx_keys = list(ctx_keys)[:-1]
608 tmp_ctx_keys = tuple(tmp_ctx_keys)
609
610 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
611
612 if found_add_line:
613 lines_to_del_to_del.append((ctx_keys, line))
614 lines_to_add_to_del.append((tmp_ctx_keys, line))
615
616 for (ctx_keys, line) in lines_to_del_to_del:
617 lines_to_del.remove((ctx_keys, line))
618
619 for (ctx_keys, line) in lines_to_add_to_del:
620 lines_to_add.remove((ctx_keys, line))
621
622 return (lines_to_add, lines_to_del)
623
624
625 def compare_context_objects(newconf, running):
626 """
627 Create a context diff for the two specified contexts
628 """
629
630 # Compare the two Config objects to find the lines that we need to add/del
631 lines_to_add = []
632 lines_to_del = []
633 delete_bgpd = False
634
635 # Find contexts that are in newconf but not in running
636 # Find contexts that are in running but not in newconf
637 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
638
639 if running_ctx_keys not in newconf.contexts:
640
641 # We check that the len is 1 here so that we only look at ('router bgp 10')
642 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
643 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
644 # running but not in newconf.
645 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
646 delete_bgpd = True
647 lines_to_del.append((running_ctx_keys, None))
648
649 # If this is an address-family under 'router bgp' and we are already deleting the
650 # entire 'router bgp' context then ignore this sub-context
651 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
652 continue
653
654 # Non-global context
655 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
656 lines_to_del.append((running_ctx_keys, None))
657
658 # Global context
659 else:
660 for line in running_ctx.lines:
661 lines_to_del.append((running_ctx_keys, line))
662
663 # Find the lines within each context to add
664 # Find the lines within each context to del
665 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
666
667 if newconf_ctx_keys in running.contexts:
668 running_ctx = running.contexts[newconf_ctx_keys]
669
670 for line in newconf_ctx.lines:
671 if line not in running_ctx.dlines:
672 lines_to_add.append((newconf_ctx_keys, line))
673
674 for line in running_ctx.lines:
675 if line not in newconf_ctx.dlines:
676 lines_to_del.append((newconf_ctx_keys, line))
677
678 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
679
680 if newconf_ctx_keys not in running.contexts:
681 lines_to_add.append((newconf_ctx_keys, None))
682
683 for line in newconf_ctx.lines:
684 lines_to_add.append((newconf_ctx_keys, line))
685
686 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
687
688 return (lines_to_add, lines_to_del)
689
690 if __name__ == '__main__':
691 # Command line options
692 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
693 parser.add_argument('--input', help='Read running config from file instead of "show running"')
694 group = parser.add_mutually_exclusive_group(required=True)
695 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
696 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
697 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
698 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
699 parser.add_argument('filename', help='Location of new frr config file')
700 args = parser.parse_args()
701
702 # Logging
703 # For --test log to stdout
704 # For --reload log to /var/log/frr/frr-reload.log
705 if args.test or args.stdout:
706 logging.basicConfig(level=logging.INFO,
707 format='%(asctime)s %(levelname)5s: %(message)s')
708
709 # Color the errors and warnings in red
710 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
711 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
712
713 elif args.reload:
714 if not os.path.isdir('/var/log/frr/'):
715 os.makedirs('/var/log/frr/')
716
717 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
718 level=logging.INFO,
719 format='%(asctime)s %(levelname)5s: %(message)s')
720
721 # argparse should prevent this from happening but just to be safe...
722 else:
723 raise Exception('Must specify --reload or --test')
724 log = logging.getLogger(__name__)
725
726 # Verify the new config file is valid
727 if not os.path.isfile(args.filename):
728 print "Filename %s does not exist" % args.filename
729 sys.exit(1)
730
731 if not os.path.getsize(args.filename):
732 print "Filename %s is an empty file" % args.filename
733 sys.exit(1)
734
735 # Verify that 'service integrated-vtysh-config' is configured
736 vtysh_filename = '/etc/frr/vtysh.conf'
737 service_integrated_vtysh_config = True
738
739 if os.path.isfile(vtysh_filename):
740 with open(vtysh_filename, 'r') as fh:
741 for line in fh.readlines():
742 line = line.strip()
743
744 if line == 'no service integrated-vtysh-config':
745 service_integrated_vtysh_config = False
746 break
747
748 if not service_integrated_vtysh_config:
749 print "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
750 sys.exit(1)
751
752 if args.debug:
753 log.setLevel(logging.DEBUG)
754
755 log.info('Called via "%s"', str(args))
756
757 # Create a Config object from the config generated by newconf
758 newconf = Config()
759 newconf.load_from_file(args.filename)
760
761 if args.test:
762
763 # Create a Config object from the running config
764 running = Config()
765
766 if args.input:
767 running.load_from_file(args.input)
768 else:
769 running.load_from_show_running()
770
771 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
772 lines_to_configure = []
773
774 if lines_to_del:
775 print "\nLines To Delete"
776 print "==============="
777
778 for (ctx_keys, line) in lines_to_del:
779
780 if line == '!':
781 continue
782
783 cmd = line_for_vtysh_file(ctx_keys, line, True)
784 lines_to_configure.append(cmd)
785 print cmd
786
787 if lines_to_add:
788 print "\nLines To Add"
789 print "============"
790
791 for (ctx_keys, line) in lines_to_add:
792
793 if line == '!':
794 continue
795
796 cmd = line_for_vtysh_file(ctx_keys, line, False)
797 lines_to_configure.append(cmd)
798 print cmd
799
800 elif args.reload:
801
802 log.debug('New Frr Config\n%s', newconf.get_lines())
803
804 # This looks a little odd but we have to do this twice...here is why
805 # If the user had this running bgp config:
806 #
807 # router bgp 10
808 # neighbor 1.1.1.1 remote-as 50
809 # neighbor 1.1.1.1 route-map FOO out
810 #
811 # and this config in the newconf config file
812 #
813 # router bgp 10
814 # neighbor 1.1.1.1 remote-as 999
815 # neighbor 1.1.1.1 route-map FOO out
816 #
817 #
818 # Then the script will do
819 # - no neighbor 1.1.1.1 remote-as 50
820 # - neighbor 1.1.1.1 remote-as 999
821 #
822 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
823 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
824 # configs again to put this line back.
825
826 for x in range(2):
827 running = Config()
828 running.load_from_show_running()
829 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
830
831 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
832
833 if lines_to_del:
834 for (ctx_keys, line) in lines_to_del:
835
836 if line == '!':
837 continue
838
839 # 'no' commands are tricky, we can't just put them in a file and
840 # vtysh -f that file. See the next comment for an explanation
841 # of their quirks
842 cmd = line_to_vtysh_conft(ctx_keys, line, True)
843 original_cmd = cmd
844
845 # Some commands in frr are picky about taking a "no" of the entire line.
846 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
847 # only the beginning. If we hit one of these command an exception will be
848 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
849 #
850 # Example:
851 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
852 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
853 # % Unknown command.
854 # frr(config-if)# no ip ospf authentication message-digest
855 # % Unknown command.
856 # frr(config-if)# no ip ospf authentication
857 # frr(config-if)#
858
859 while True:
860 try:
861 _ = subprocess.check_output(cmd)
862
863 except subprocess.CalledProcessError:
864
865 # - Pull the last entry from cmd (this would be
866 # 'no ip ospf authentication message-digest 1.1.1.1' in
867 # our example above
868 # - Split that last entry by whitespace and drop the last word
869 log.warning('Failed to execute %s', ' '.join(cmd))
870 last_arg = cmd[-1].split(' ')
871
872 if len(last_arg) <= 2:
873 log.error('"%s" we failed to remove this command', original_cmd)
874 break
875
876 new_last_arg = last_arg[0:-1]
877 cmd[-1] = ' '.join(new_last_arg)
878 else:
879 log.info('Executed "%s"', ' '.join(cmd))
880 break
881
882 if lines_to_add:
883 lines_to_configure = []
884
885 for (ctx_keys, line) in lines_to_add:
886
887 if line == '!':
888 continue
889
890 cmd = line_for_vtysh_file(ctx_keys, line, False)
891 lines_to_configure.append(cmd)
892
893 if lines_to_configure:
894 random_string = ''.join(random.SystemRandom().choice(
895 string.ascii_uppercase +
896 string.digits) for _ in range(6))
897
898 filename = "/var/run/frr/reload-%s.txt" % random_string
899 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
900
901 with open(filename, 'w') as fh:
902 for line in lines_to_configure:
903 fh.write(line + '\n')
904 subprocess.call(['/usr/bin/vtysh', '-f', filename])
905 os.unlink(filename)
906
907 # Make these changes persistent
908 subprocess.call(['/usr/bin/vtysh', '-c', 'write'])