]> git.proxmox.com Git - mirror_frr.git/blame - tools/quagga-reload.py
bgpd: Print the correct table in "show ip bgp x.x.x.x"
[mirror_frr.git] / tools / quagga-reload.py
CommitLineData
2fc76430
DS
1#!/usr/bin/python
2
3"""
4This program
5- reads a quagga configuration text file
6- reads quagga's current running configuration via "vtysh -c 'show running'"
7- compares the two configs and determines what commands to execute to
8 synchronize quagga's running configuration with the configuation in the
9 text file
10"""
11
12import argparse
13import copy
14import logging
15import os
4a2587c6 16import random
9fe88bc7 17import re
4a2587c6 18import string
2fc76430
DS
19import subprocess
20import sys
21from collections import OrderedDict
22from ipaddr import IPv6Address
4a2587c6
DW
23from pprint import pformat
24
2fc76430
DS
25
26class Context(object):
4a2587c6 27
2fc76430
DS
28 """
29 A Context object represents a section of quagga configuration such as:
30!
31interface swp3
32 description swp3 -> r8's swp1
33 ipv6 nd suppress-ra
34 link-detect
35!
36
37or a single line context object such as this:
38
39ip forwarding
40
41 """
42
43 def __init__(self, keys, lines):
44 self.keys = keys
45 self.lines = lines
46
47 # Keep a dictionary of the lines, this is to make it easy to tell if a
48 # line exists in this Context
49 self.dlines = OrderedDict()
50
51 for ligne in lines:
52 self.dlines[ligne] = True
53
54 def add_lines(self, lines):
55 """
56 Add lines to specified context
57 """
58
59 self.lines.extend(lines)
60
61 for ligne in lines:
62 self.dlines[ligne] = True
63
64
65class Config(object):
4a2587c6 66
2fc76430
DS
67 """
68 A quagga configuration is stored in a Config object. A Config object
69 contains a dictionary of Context objects where the Context keys
70 ('router ospf' for example) are our dictionary key.
71 """
72
73 def __init__(self):
74 self.lines = []
75 self.contexts = OrderedDict()
76
77 def load_from_file(self, filename):
78 """
79 Read configuration from specified file and slurp it into internal memory
80 The internal representation has been marked appropriately by passing it
81 through vtysh with the -m parameter
82 """
c50aceee 83 logger.info('Loading Config object from file %s', filename)
2fc76430
DS
84
85 try:
4a2587c6 86 file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename])
2fc76430
DS
87 except subprocess.CalledProcessError as e:
88 logger.error('vtysh marking of config file %s failed with error %s:', filename, str(e))
4a2587c6 89 print "vtysh marking of file %s failed with error: %s" % (filename, str(e))
2fc76430
DS
90 sys.exit(1)
91
92 for line in file_output.split('\n'):
93 line = line.strip()
94 if ":" in line:
95 qv6_line = get_normalized_ipv6_line(line)
96 self.lines.append(qv6_line)
97 else:
98 self.lines.append(line)
99
100 self.load_contexts()
101
102 def load_from_show_running(self):
103 """
104 Read running configuration and slurp it into internal memory
105 The internal representation has been marked appropriately by passing it
106 through vtysh with the -m parameter
107 """
c50aceee 108 logger.info('Loading Config object from vtysh show running')
2fc76430
DS
109
110 try:
4a2587c6
DW
111 config_text = subprocess.check_output(
112 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
113 shell=True)
2fc76430
DS
114 except subprocess.CalledProcessError as e:
115 logger.error('vtysh marking of running config failed with error %s:', str(e))
4a2587c6 116 print "vtysh marking of running config failed with error %s:" % (str(e))
2fc76430
DS
117 sys.exit(1)
118
2fc76430
DS
119 for line in config_text.split('\n'):
120 line = line.strip()
121
122 if (line == 'Building configuration...' or
123 line == 'Current configuration:' or
4a2587c6 124 not line):
2fc76430
DS
125 continue
126
127 self.lines.append(line)
128
129 self.load_contexts()
130
131 def get_lines(self):
132 """
133 Return the lines read in from the configuration
134 """
135
136 return '\n'.join(self.lines)
137
138 def get_contexts(self):
139 """
140 Return the parsed context as strings for display, log etc.
141 """
142
143 for (_, ctx) in sorted(self.contexts.iteritems()):
144 print str(ctx) + '\n'
145
146 def save_contexts(self, key, lines):
147 """
148 Save the provided key and lines as a context
149 """
150
151 if not key:
152 return
153
154 if lines:
155 if tuple(key) not in self.contexts:
156 ctx = Context(tuple(key), lines)
157 self.contexts[tuple(key)] = ctx
158 else:
159 ctx = self.contexts[tuple(key)]
160 ctx.add_lines(lines)
161
162 else:
163 if tuple(key) not in self.contexts:
164 ctx = Context(tuple(key), [])
165 self.contexts[tuple(key)] = ctx
166
167 def load_contexts(self):
168 """
169 Parse the configuration and create contexts for each appropriate block
170 """
171
172 current_context_lines = []
173 ctx_keys = []
174
175 '''
176 The end of a context is flagged via the 'end' keyword:
177
178!
179interface swp52
180 ipv6 nd suppress-ra
181 link-detect
182!
183end
184router bgp 10
185 bgp router-id 10.0.0.1
186 bgp log-neighbor-changes
187 no bgp default ipv4-unicast
188 neighbor EBGP peer-group
189 neighbor EBGP advertisement-interval 1
190 neighbor EBGP timers connect 10
191 neighbor 2001:40:1:4::6 remote-as 40
192 neighbor 2001:40:1:8::a remote-as 40
193!
194end
195 address-family ipv6
196 neighbor IBGPv6 activate
197 neighbor 2001:10::2 peer-group IBGPv6
198 neighbor 2001:10::3 peer-group IBGPv6
199 exit-address-family
200!
201end
202router ospf
203 ospf router-id 10.0.0.1
204 log-adjacency-changes detail
205 timers throttle spf 0 50 5000
206!
207end
208 '''
209
210 # The code assumes that its working on the output from the "vtysh -m"
211 # command. That provides the appropriate markers to signify end of
212 # a context. This routine uses that to build the contexts for the
213 # config.
214 #
215 # There are single line contexts such as "log file /media/node/zebra.log"
216 # and multi-line contexts such as "router ospf" and subcontexts
217 # within a context such as "address-family" within "router bgp"
218 # In each of these cases, the first line of the context becomes the
219 # key of the context. So "router bgp 10" is the key for the non-address
220 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
221 # the key for the subcontext and so on.
2fc76430
DS
222 ctx_keys = []
223 main_ctx_key = []
224 new_ctx = True
2fc76430
DS
225
226 # the keywords that we know are single line contexts. bgp in this case
227 # is not the main router bgp block, but enabling multi-instance
2fed5dcd
DW
228 oneline_ctx_keywords = ("access-list ",
229 "bgp ",
230 "debug ",
231 "dump ",
232 "enable ",
233 "hostname ",
234 "ip ",
235 "ipv6 ",
236 "log ",
237 "password ",
238 "ptm-enable",
239 "router-id ",
240 "service ",
241 "table ",
242 "username ",
243 "zebra ")
244
2fc76430
DS
245 for line in self.lines:
246
247 if not line:
248 continue
249
250 if line.startswith('!') or line.startswith('#'):
251 continue
252
253 # one line contexts
254 if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords):
255 self.save_contexts(ctx_keys, current_context_lines)
256
257 # Start a new context
258 main_ctx_key = []
4a2587c6 259 ctx_keys = [line, ]
2fc76430
DS
260 current_context_lines = []
261
76f69d1c 262 logger.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
2fc76430
DS
263 self.save_contexts(ctx_keys, current_context_lines)
264 new_ctx = True
265
266 elif line == "end":
267 self.save_contexts(ctx_keys, current_context_lines)
76f69d1c 268 logger.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
2fc76430
DS
269
270 # Start a new context
271 new_ctx = True
272 main_ctx_key = []
273 ctx_keys = []
274 current_context_lines = []
275
276 elif line == "exit-address-family" or line == "exit":
277 # if this exit is for address-family ipv4 unicast, ignore the pop
278 if main_ctx_key:
279 self.save_contexts(ctx_keys, current_context_lines)
280
281 # Start a new context
282 ctx_keys = copy.deepcopy(main_ctx_key)
283 current_context_lines = []
76f69d1c 284 logger.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
2fc76430
DS
285
286 elif new_ctx is True:
287 if not main_ctx_key:
4a2587c6 288 ctx_keys = [line, ]
2fc76430
DS
289 else:
290 ctx_keys = copy.deepcopy(main_ctx_key)
291 main_ctx_key = []
292
293 current_context_lines = []
294 new_ctx = False
76f69d1c 295 logger.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
2fc76430
DS
296
297 elif "address-family " in line:
298 main_ctx_key = []
299
0b960b4d
DW
300 # Save old context first
301 self.save_contexts(ctx_keys, current_context_lines)
302 current_context_lines = []
303 main_ctx_key = copy.deepcopy(ctx_keys)
304 logger.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
2fc76430 305
0b960b4d
DW
306 if line == "address-family ipv6":
307 ctx_keys.append("address-family ipv6 unicast")
308 elif line == "address-family ipv4":
309 ctx_keys.append("address-family ipv4 unicast")
310 else:
311 ctx_keys.append(line)
2fc76430
DS
312
313 else:
314 # Continuing in an existing context, add non-commented lines to it
315 current_context_lines.append(line)
76f69d1c 316 logger.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
2fc76430
DS
317
318 # Save the context of the last one
319 self.save_contexts(ctx_keys, current_context_lines)
320
4a2587c6 321
2fc76430
DS
322def line_to_vtysh_conft(ctx_keys, line, delete):
323 """
4a2587c6 324 Return the vtysh command for the specified context line
2fc76430
DS
325 """
326
327 cmd = []
328 cmd.append('vtysh')
329 cmd.append('-c')
330 cmd.append('conf t')
331
332 if line:
333 for ctx_key in ctx_keys:
334 cmd.append('-c')
335 cmd.append(ctx_key)
336
337 line = line.lstrip()
338
339 if delete:
340 cmd.append('-c')
341
342 if line.startswith('no '):
343 cmd.append('%s' % line[3:])
344 else:
345 cmd.append('no %s' % line)
346
347 else:
348 cmd.append('-c')
349 cmd.append(line)
350
351 # If line is None then we are typically deleting an entire
352 # context ('no router ospf' for example)
353 else:
354
355 if delete:
356
357 # Only put the 'no' on the last sub-context
358 for ctx_key in ctx_keys:
359 cmd.append('-c')
360
361 if ctx_key == ctx_keys[-1]:
362 cmd.append('no %s' % ctx_key)
363 else:
364 cmd.append('%s' % ctx_key)
365 else:
366 for ctx_key in ctx_keys:
367 cmd.append('-c')
368 cmd.append(ctx_key)
369
370 return cmd
371
4a2587c6
DW
372
373def line_for_vtysh_file(ctx_keys, line, delete):
374 """
375 Return the command as it would appear in Quagga.conf
376 """
377 cmd = []
378
379 if line:
380 for (i, ctx_key) in enumerate(ctx_keys):
381 cmd.append(' ' * i + ctx_key)
382
383 line = line.lstrip()
384 indent = len(ctx_keys) * ' '
385
386 if delete:
387 if line.startswith('no '):
388 cmd.append('%s%s' % (indent, line[3:]))
389 else:
390 cmd.append('%sno %s' % (indent, line))
391
392 else:
393 cmd.append(indent + line)
394
395 # If line is None then we are typically deleting an entire
396 # context ('no router ospf' for example)
397 else:
398 if delete:
399
400 # Only put the 'no' on the last sub-context
401 for ctx_key in ctx_keys:
402
403 if ctx_key == ctx_keys[-1]:
404 cmd.append('no %s' % ctx_key)
405 else:
406 cmd.append('%s' % ctx_key)
407 else:
408 for ctx_key in ctx_keys:
409 cmd.append(ctx_key)
410
411 return '\n' + '\n'.join(cmd)
412
413
2fc76430
DS
414def get_normalized_ipv6_line(line):
415 """
416 Return a normalized IPv6 line as produced by quagga,
417 with all letters in lower case and trailing and leading
418 zeros removed
419 """
420 norm_line = ""
421 words = line.split(' ')
422 for word in words:
423 if ":" in word:
424 try:
425 norm_word = str(IPv6Address(word)).lower()
426 except:
427 norm_word = word
428 else:
429 norm_word = word
430 norm_line = norm_line + " " + norm_word
431
432 return norm_line.strip()
433
4a2587c6 434
9fe88bc7
DW
435def line_exist(lines, target_ctx_keys, target_line):
436 for (ctx_keys, line) in lines:
437 if ctx_keys == target_ctx_keys and line == target_line:
438 return True
439 return False
440
441
9b166171 442def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
9fe88bc7
DW
443
444 # Quite possibly the most confusing (while accurate) variable names in history
445 lines_to_add_to_del = []
446 lines_to_del_to_del = []
447
448 for (ctx_keys, line) in lines_to_del:
9b166171
DW
449 deleted = False
450
9fe88bc7 451 if ctx_keys[0].startswith('router bgp') and line.startswith('neighbor '):
9b166171
DW
452 """
453 BGP changed how it displays swpX peers that are part of peer-group. Older
454 versions of quagga would display these on separate lines:
455 neighbor swp1 interface
456 neighbor swp1 peer-group FOO
457
458 but today we display via a single line
459 neighbor swp1 interface peer-group FOO
460
461 This change confuses quagga-reload.py so check to see if we are deleting
462 neighbor swp1 interface peer-group FOO
463
464 and adding
465 neighbor swp1 interface
466 neighbor swp1 peer-group FOO
467
468 If so then chop the del line and the corresponding add lines
469 """
470
9fe88bc7 471 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
9b166171 472 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
9fe88bc7 473
9b166171
DW
474 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
475 swpx_interface = None
476 swpx_peergroup = None
477
478 if re_swpx_int_peergroup:
479 swpx = re_swpx_int_peergroup.group(1)
480 peergroup = re_swpx_int_peergroup.group(2)
481 swpx_interface = "neighbor %s interface" % swpx
482 elif re_swpx_int_v6only_peergroup:
483 swpx = re_swpx_int_v6only_peergroup.group(1)
484 peergroup = re_swpx_int_v6only_peergroup.group(2)
485 swpx_interface = "neighbor %s interface v6only" % swpx
9fe88bc7 486
9b166171 487 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
9fe88bc7
DW
488 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
489 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
b1e0634c 490 tmp_ctx_keys = tuple(list(ctx_keys))
9b166171
DW
491
492 if not found_add_swpx_peergroup:
493 tmp_ctx_keys = list(ctx_keys)
494 tmp_ctx_keys.append('address-family ipv4 unicast')
495 tmp_ctx_keys = tuple(tmp_ctx_keys)
496 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
497
498 if not found_add_swpx_peergroup:
499 tmp_ctx_keys = list(ctx_keys)
500 tmp_ctx_keys.append('address-family ipv6 unicast')
501 tmp_ctx_keys = tuple(tmp_ctx_keys)
502 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
9fe88bc7
DW
503
504 if found_add_swpx_interface and found_add_swpx_peergroup:
9b166171 505 deleted = True
9fe88bc7
DW
506 lines_to_del_to_del.append((ctx_keys, line))
507 lines_to_add_to_del.append((ctx_keys, swpx_interface))
9b166171
DW
508 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
509
b3a39dc5
DD
510 """
511 In 3.0.1 we changed how we display neighbor interface command. Older
512 versions of quagga would display the following:
513 neighbor swp1 interface
514 neighbor swp1 remote-as external
515 neighbor swp1 capability extended-nexthop
516
517 but today we display via a single line
518 neighbor swp1 interface remote-as external
519
520 and capability extended-nexthop is no longer needed because we
521 automatically enable it when the neighbor is of type interface.
522
523 This change confuses quagga-reload.py so check to see if we are deleting
524 neighbor swp1 interface remote-as (external|internal|ASNUM)
525
526 and adding
527 neighbor swp1 interface
528 neighbor swp1 remote-as (external|internal|ASNUM)
529 neighbor swp1 capability extended-nexthop
530
531 If so then chop the del line and the corresponding add lines
532 """
533 re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
534 re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
535
536 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
537 swpx_interface = None
538 swpx_remoteas = None
539
540 if re_swpx_int_remoteas:
541 swpx = re_swpx_int_remoteas.group(1)
542 remoteas = re_swpx_int_remoteas.group(2)
543 swpx_interface = "neighbor %s interface" % swpx
544 elif re_swpx_int_v6only_remoteas:
545 swpx = re_swpx_int_v6only_remoteas.group(1)
546 remoteas = re_swpx_int_v6only_remoteas.group(2)
547 swpx_interface = "neighbor %s interface v6only" % swpx
548
549 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
550 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
551 found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
552 tmp_ctx_keys = tuple(list(ctx_keys))
553
554 if found_add_swpx_interface and found_add_swpx_remoteas:
555 deleted = True
556 lines_to_del_to_del.append((ctx_keys, line))
557 lines_to_add_to_del.append((ctx_keys, swpx_interface))
558 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
559
9b166171
DW
560 if not deleted:
561 found_add_line = line_exist(lines_to_add, ctx_keys, line)
562
563 if found_add_line:
564 lines_to_del_to_del.append((ctx_keys, line))
565 lines_to_add_to_del.append((ctx_keys, line))
566 else:
567 '''
568 We have commands that used to be displayed in the global part
569 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
570
571 # old way
572 router bgp 64900
573 neighbor ISL advertisement-interval 0
574
575 vs.
576
577 # new way
578 router bgp 64900
579 address-family ipv4 unicast
580 neighbor ISL advertisement-interval 0
581
582 Look to see if we are deleting it in one format just to add it back in the other
583 '''
584 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
585 tmp_ctx_keys = list(ctx_keys)[:-1]
586 tmp_ctx_keys = tuple(tmp_ctx_keys)
587
588 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
589
590 if found_add_line:
591 lines_to_del_to_del.append((ctx_keys, line))
592 lines_to_add_to_del.append((tmp_ctx_keys, line))
9fe88bc7
DW
593
594 for (ctx_keys, line) in lines_to_del_to_del:
595 lines_to_del.remove((ctx_keys, line))
596
597 for (ctx_keys, line) in lines_to_add_to_del:
598 lines_to_add.remove((ctx_keys, line))
599
600 return (lines_to_add, lines_to_del)
601
602
2fc76430
DS
603def compare_context_objects(newconf, running):
604 """
605 Create a context diff for the two specified contexts
606 """
607
608 # Compare the two Config objects to find the lines that we need to add/del
609 lines_to_add = []
610 lines_to_del = []
514665b9 611 restart_bgpd = False
2fc76430
DS
612
613 # Find contexts that are in newconf but not in running
614 # Find contexts that are in running but not in newconf
615 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
616
617 if running_ctx_keys not in newconf.contexts:
618
76f69d1c 619 # Check if bgp's local ASN has changed. If yes, just restart it
ab5f8310
DW
620 # We check that the len is 1 here so that we only look at ('router bgp 10')
621 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
622 # latter could cause a false restart_bgpd positive if ipv4 unicast is in
623 # running but not in newconf.
624 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
76f69d1c
DW
625 restart_bgpd = True
626 continue
514665b9 627
2fc76430 628 # Non-global context
514665b9 629 if running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
630 lines_to_del.append((running_ctx_keys, None))
631
632 # Global context
633 else:
634 for line in running_ctx.lines:
635 lines_to_del.append((running_ctx_keys, line))
636
637 # Find the lines within each context to add
638 # Find the lines within each context to del
639 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
640
641 if newconf_ctx_keys in running.contexts:
642 running_ctx = running.contexts[newconf_ctx_keys]
643
644 for line in newconf_ctx.lines:
645 if line not in running_ctx.dlines:
646 lines_to_add.append((newconf_ctx_keys, line))
647
648 for line in running_ctx.lines:
649 if line not in newconf_ctx.dlines:
650 lines_to_del.append((newconf_ctx_keys, line))
651
652 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
653
654 if newconf_ctx_keys not in running.contexts:
514665b9 655
76f69d1c
DW
656 # If its "router bgp" and we're restarting bgp, skip doing
657 # anything specific for bgp
658 if "router bgp" in newconf_ctx_keys[0] and restart_bgpd:
659 continue
2fc76430
DS
660 lines_to_add.append((newconf_ctx_keys, None))
661
662 for line in newconf_ctx.lines:
663 lines_to_add.append((newconf_ctx_keys, line))
664
9b166171 665 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
9fe88bc7 666
514665b9 667 return (lines_to_add, lines_to_del, restart_bgpd)
2fc76430
DS
668
669if __name__ == '__main__':
670 # Command line options
671 parser = argparse.ArgumentParser(description='Dynamically apply diff in quagga configs')
672 parser.add_argument('--input', help='Read running config from file instead of "show running"')
673 group = parser.add_mutually_exclusive_group(required=True)
674 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
675 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
676 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
cc146ecc 677 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
2fc76430
DS
678 parser.add_argument('filename', help='Location of new quagga config file')
679 args = parser.parse_args()
680
681 # Logging
682 # For --test log to stdout
683 # For --reload log to /var/log/quagga/quagga-reload.log
cc146ecc 684 if args.test or args.stdout:
c50aceee 685 logging.basicConfig(level=logging.INFO,
2fc76430
DS
686 format='%(asctime)s %(levelname)5s: %(message)s')
687 elif args.reload:
688 if not os.path.isdir('/var/log/quagga/'):
689 os.makedirs('/var/log/quagga/')
690
691 logging.basicConfig(filename='/var/log/quagga/quagga-reload.log',
c50aceee 692 level=logging.INFO,
2fc76430
DS
693 format='%(asctime)s %(levelname)5s: %(message)s')
694
695 # argparse should prevent this from happening but just to be safe...
696 else:
697 raise Exception('Must specify --reload or --test')
698 logger = logging.getLogger(__name__)
699
76f69d1c
DW
700 # Verify the new config file is valid
701 if not os.path.isfile(args.filename):
702 print "Filename %s does not exist" % args.filename
703 sys.exit(1)
704
705 if not os.path.getsize(args.filename):
706 print "Filename %s is an empty file" % args.filename
707 sys.exit(1)
708
76f69d1c
DW
709 # Verify that 'service integrated-vtysh-config' is configured
710 vtysh_filename = '/etc/quagga/vtysh.conf'
76f69d1c
DW
711 service_integrated_vtysh_config = False
712
f850d14d
DW
713 if os.path.isfile(vtysh_filename):
714 with open(vtysh_filename, 'r') as fh:
715 for line in fh.readlines():
716 line = line.strip()
76f69d1c 717
f850d14d
DW
718 if line == 'service integrated-vtysh-config':
719 service_integrated_vtysh_config = True
720 break
76f69d1c
DW
721
722 if not service_integrated_vtysh_config:
723 print "'service integrated-vtysh-config' is not configured, this is required for 'service quagga reload'"
724 sys.exit(1)
2fc76430 725
c50aceee
DW
726 if args.debug:
727 logger.setLevel(logging.DEBUG)
728
729 logger.info('Called via "%s"', str(args))
730
2fc76430
DS
731 # Create a Config object from the config generated by newconf
732 newconf = Config()
733 newconf.load_from_file(args.filename)
2fc76430
DS
734
735 if args.test:
736
737 # Create a Config object from the running config
738 running = Config()
739
740 if args.input:
741 running.load_from_file(args.input)
742 else:
743 running.load_from_show_running()
744
514665b9 745 (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running)
4a2587c6 746 lines_to_configure = []
2fc76430
DS
747
748 if lines_to_del:
749 print "\nLines To Delete"
750 print "==============="
751
752 for (ctx_keys, line) in lines_to_del:
753
754 if line == '!':
755 continue
756
4a2587c6
DW
757 cmd = line_for_vtysh_file(ctx_keys, line, True)
758 lines_to_configure.append(cmd)
9fe88bc7 759 print cmd
2fc76430
DS
760
761 if lines_to_add:
762 print "\nLines To Add"
763 print "============"
764
765 for (ctx_keys, line) in lines_to_add:
766
767 if line == '!':
768 continue
769
4a2587c6
DW
770 cmd = line_for_vtysh_file(ctx_keys, line, False)
771 lines_to_configure.append(cmd)
9fe88bc7 772 print cmd
2fc76430 773
76f69d1c 774 if restart_bgp:
4a2587c6 775 print "BGP local AS changed, bgpd would restart"
2fc76430
DS
776
777 elif args.reload:
778
c50aceee 779 logger.debug('New Quagga Config\n%s', newconf.get_lines())
2fc76430
DS
780
781 # This looks a little odd but we have to do this twice...here is why
782 # If the user had this running bgp config:
4a2587c6 783 #
2fc76430
DS
784 # router bgp 10
785 # neighbor 1.1.1.1 remote-as 50
786 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 787 #
2fc76430 788 # and this config in the newconf config file
4a2587c6 789 #
2fc76430
DS
790 # router bgp 10
791 # neighbor 1.1.1.1 remote-as 999
792 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
793 #
794 #
2fc76430
DS
795 # Then the script will do
796 # - no neighbor 1.1.1.1 remote-as 50
797 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 798 #
2fc76430
DS
799 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
800 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
801 # configs again to put this line back.
802
803 for x in range(2):
804 running = Config()
805 running.load_from_show_running()
c50aceee 806 logger.debug('Running Quagga Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 807
514665b9 808 (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running)
2fc76430
DS
809
810 if lines_to_del:
811 for (ctx_keys, line) in lines_to_del:
812
813 if line == '!':
814 continue
815
4a2587c6
DW
816 # 'no' commands are tricky, we can't just put them in a file and
817 # vtysh -f that file. See the next comment for an explanation
818 # of their quirks
2fc76430
DS
819 cmd = line_to_vtysh_conft(ctx_keys, line, True)
820 original_cmd = cmd
821
76f69d1c
DW
822 # Some commands in quagga are picky about taking a "no" of the entire line.
823 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
824 # only the beginning. If we hit one of these command an exception will be
825 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 826 #
76f69d1c 827 # Example:
4a2587c6
DW
828 # quagga(config-if)# ip ospf authentication message-digest 1.1.1.1
829 # quagga(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 830 # % Unknown command.
4a2587c6 831 # quagga(config-if)# no ip ospf authentication message-digest
76f69d1c 832 # % Unknown command.
4a2587c6
DW
833 # quagga(config-if)# no ip ospf authentication
834 # quagga(config-if)#
2fc76430
DS
835
836 while True:
2fc76430
DS
837 try:
838 _ = subprocess.check_output(cmd)
839
840 except subprocess.CalledProcessError:
841
842 # - Pull the last entry from cmd (this would be
843 # 'no ip ospf authentication message-digest 1.1.1.1' in
844 # our example above
845 # - Split that last entry by whitespace and drop the last word
c50aceee 846 logger.warning('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
847 last_arg = cmd[-1].split(' ')
848
849 if len(last_arg) <= 2:
850 logger.error('"%s" we failed to remove this command', original_cmd)
851 break
852
853 new_last_arg = last_arg[0:-1]
854 cmd[-1] = ' '.join(new_last_arg)
855 else:
c50aceee 856 logger.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
857 break
858
2fc76430 859 if lines_to_add:
4a2587c6
DW
860 lines_to_configure = []
861
2fc76430
DS
862 for (ctx_keys, line) in lines_to_add:
863
864 if line == '!':
865 continue
866
4a2587c6
DW
867 cmd = line_for_vtysh_file(ctx_keys, line, False)
868 lines_to_configure.append(cmd)
869
870 if lines_to_configure:
871 random_string = ''.join(random.SystemRandom().choice(
872 string.ascii_uppercase +
873 string.digits) for _ in range(6))
874
875 filename = "/var/run/quagga/reload-%s.txt" % random_string
876 logger.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
877
878 with open(filename, 'w') as fh:
879 for line in lines_to_configure:
880 fh.write(line + '\n')
881 subprocess.call(['/usr/bin/vtysh', '-f', filename])
882 os.unlink(filename)
2fc76430 883
76f69d1c 884 if restart_bgp:
651415bd
DS
885 subprocess.call(['sudo', 'systemctl', 'reset-failed', 'quagga'])
886 subprocess.call(['sudo', 'systemctl', '--no-block', 'restart', 'quagga'])