]> git.proxmox.com Git - mirror_frr.git/blame - tools/quagga-reload.py
bgpd: Make ASN optional for `no router bgp`
[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
510 if not deleted:
511 found_add_line = line_exist(lines_to_add, ctx_keys, line)
512
513 if found_add_line:
514 lines_to_del_to_del.append((ctx_keys, line))
515 lines_to_add_to_del.append((ctx_keys, line))
516 else:
517 '''
518 We have commands that used to be displayed in the global part
519 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
520
521 # old way
522 router bgp 64900
523 neighbor ISL advertisement-interval 0
524
525 vs.
526
527 # new way
528 router bgp 64900
529 address-family ipv4 unicast
530 neighbor ISL advertisement-interval 0
531
532 Look to see if we are deleting it in one format just to add it back in the other
533 '''
534 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
535 tmp_ctx_keys = list(ctx_keys)[:-1]
536 tmp_ctx_keys = tuple(tmp_ctx_keys)
537
538 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
539
540 if found_add_line:
541 lines_to_del_to_del.append((ctx_keys, line))
542 lines_to_add_to_del.append((tmp_ctx_keys, line))
9fe88bc7
DW
543
544 for (ctx_keys, line) in lines_to_del_to_del:
545 lines_to_del.remove((ctx_keys, line))
546
547 for (ctx_keys, line) in lines_to_add_to_del:
548 lines_to_add.remove((ctx_keys, line))
549
550 return (lines_to_add, lines_to_del)
551
552
2fc76430
DS
553def compare_context_objects(newconf, running):
554 """
555 Create a context diff for the two specified contexts
556 """
557
558 # Compare the two Config objects to find the lines that we need to add/del
559 lines_to_add = []
560 lines_to_del = []
514665b9 561 restart_bgpd = False
2fc76430
DS
562
563 # Find contexts that are in newconf but not in running
564 # Find contexts that are in running but not in newconf
565 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
566
567 if running_ctx_keys not in newconf.contexts:
568
76f69d1c 569 # Check if bgp's local ASN has changed. If yes, just restart it
ab5f8310
DW
570 # We check that the len is 1 here so that we only look at ('router bgp 10')
571 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
572 # latter could cause a false restart_bgpd positive if ipv4 unicast is in
573 # running but not in newconf.
574 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
76f69d1c
DW
575 restart_bgpd = True
576 continue
514665b9 577
2fc76430 578 # Non-global context
514665b9 579 if running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
580 lines_to_del.append((running_ctx_keys, None))
581
582 # Global context
583 else:
584 for line in running_ctx.lines:
585 lines_to_del.append((running_ctx_keys, line))
586
587 # Find the lines within each context to add
588 # Find the lines within each context to del
589 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
590
591 if newconf_ctx_keys in running.contexts:
592 running_ctx = running.contexts[newconf_ctx_keys]
593
594 for line in newconf_ctx.lines:
595 if line not in running_ctx.dlines:
596 lines_to_add.append((newconf_ctx_keys, line))
597
598 for line in running_ctx.lines:
599 if line not in newconf_ctx.dlines:
600 lines_to_del.append((newconf_ctx_keys, line))
601
602 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
603
604 if newconf_ctx_keys not in running.contexts:
514665b9 605
76f69d1c
DW
606 # If its "router bgp" and we're restarting bgp, skip doing
607 # anything specific for bgp
608 if "router bgp" in newconf_ctx_keys[0] and restart_bgpd:
609 continue
2fc76430
DS
610 lines_to_add.append((newconf_ctx_keys, None))
611
612 for line in newconf_ctx.lines:
613 lines_to_add.append((newconf_ctx_keys, line))
614
9b166171 615 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
9fe88bc7 616
514665b9 617 return (lines_to_add, lines_to_del, restart_bgpd)
2fc76430
DS
618
619if __name__ == '__main__':
620 # Command line options
621 parser = argparse.ArgumentParser(description='Dynamically apply diff in quagga configs')
622 parser.add_argument('--input', help='Read running config from file instead of "show running"')
623 group = parser.add_mutually_exclusive_group(required=True)
624 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
625 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
626 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
cc146ecc 627 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
2fc76430
DS
628 parser.add_argument('filename', help='Location of new quagga config file')
629 args = parser.parse_args()
630
631 # Logging
632 # For --test log to stdout
633 # For --reload log to /var/log/quagga/quagga-reload.log
cc146ecc 634 if args.test or args.stdout:
c50aceee 635 logging.basicConfig(level=logging.INFO,
2fc76430
DS
636 format='%(asctime)s %(levelname)5s: %(message)s')
637 elif args.reload:
638 if not os.path.isdir('/var/log/quagga/'):
639 os.makedirs('/var/log/quagga/')
640
641 logging.basicConfig(filename='/var/log/quagga/quagga-reload.log',
c50aceee 642 level=logging.INFO,
2fc76430
DS
643 format='%(asctime)s %(levelname)5s: %(message)s')
644
645 # argparse should prevent this from happening but just to be safe...
646 else:
647 raise Exception('Must specify --reload or --test')
648 logger = logging.getLogger(__name__)
649
76f69d1c
DW
650 # Verify the new config file is valid
651 if not os.path.isfile(args.filename):
652 print "Filename %s does not exist" % args.filename
653 sys.exit(1)
654
655 if not os.path.getsize(args.filename):
656 print "Filename %s is an empty file" % args.filename
657 sys.exit(1)
658
76f69d1c
DW
659 # Verify that 'service integrated-vtysh-config' is configured
660 vtysh_filename = '/etc/quagga/vtysh.conf'
76f69d1c
DW
661 service_integrated_vtysh_config = False
662
f850d14d
DW
663 if os.path.isfile(vtysh_filename):
664 with open(vtysh_filename, 'r') as fh:
665 for line in fh.readlines():
666 line = line.strip()
76f69d1c 667
f850d14d
DW
668 if line == 'service integrated-vtysh-config':
669 service_integrated_vtysh_config = True
670 break
76f69d1c
DW
671
672 if not service_integrated_vtysh_config:
673 print "'service integrated-vtysh-config' is not configured, this is required for 'service quagga reload'"
674 sys.exit(1)
2fc76430 675
c50aceee
DW
676 if args.debug:
677 logger.setLevel(logging.DEBUG)
678
679 logger.info('Called via "%s"', str(args))
680
2fc76430
DS
681 # Create a Config object from the config generated by newconf
682 newconf = Config()
683 newconf.load_from_file(args.filename)
2fc76430
DS
684
685 if args.test:
686
687 # Create a Config object from the running config
688 running = Config()
689
690 if args.input:
691 running.load_from_file(args.input)
692 else:
693 running.load_from_show_running()
694
514665b9 695 (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running)
4a2587c6 696 lines_to_configure = []
2fc76430
DS
697
698 if lines_to_del:
699 print "\nLines To Delete"
700 print "==============="
701
702 for (ctx_keys, line) in lines_to_del:
703
704 if line == '!':
705 continue
706
4a2587c6
DW
707 cmd = line_for_vtysh_file(ctx_keys, line, True)
708 lines_to_configure.append(cmd)
9fe88bc7 709 print cmd
2fc76430
DS
710
711 if lines_to_add:
712 print "\nLines To Add"
713 print "============"
714
715 for (ctx_keys, line) in lines_to_add:
716
717 if line == '!':
718 continue
719
4a2587c6
DW
720 cmd = line_for_vtysh_file(ctx_keys, line, False)
721 lines_to_configure.append(cmd)
9fe88bc7 722 print cmd
2fc76430 723
76f69d1c 724 if restart_bgp:
4a2587c6 725 print "BGP local AS changed, bgpd would restart"
2fc76430
DS
726
727 elif args.reload:
728
c50aceee 729 logger.debug('New Quagga Config\n%s', newconf.get_lines())
2fc76430
DS
730
731 # This looks a little odd but we have to do this twice...here is why
732 # If the user had this running bgp config:
4a2587c6 733 #
2fc76430
DS
734 # router bgp 10
735 # neighbor 1.1.1.1 remote-as 50
736 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 737 #
2fc76430 738 # and this config in the newconf config file
4a2587c6 739 #
2fc76430
DS
740 # router bgp 10
741 # neighbor 1.1.1.1 remote-as 999
742 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
743 #
744 #
2fc76430
DS
745 # Then the script will do
746 # - no neighbor 1.1.1.1 remote-as 50
747 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 748 #
2fc76430
DS
749 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
750 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
751 # configs again to put this line back.
752
753 for x in range(2):
754 running = Config()
755 running.load_from_show_running()
c50aceee 756 logger.debug('Running Quagga Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 757
514665b9 758 (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running)
2fc76430
DS
759
760 if lines_to_del:
761 for (ctx_keys, line) in lines_to_del:
762
763 if line == '!':
764 continue
765
4a2587c6
DW
766 # 'no' commands are tricky, we can't just put them in a file and
767 # vtysh -f that file. See the next comment for an explanation
768 # of their quirks
2fc76430
DS
769 cmd = line_to_vtysh_conft(ctx_keys, line, True)
770 original_cmd = cmd
771
76f69d1c
DW
772 # Some commands in quagga are picky about taking a "no" of the entire line.
773 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
774 # only the beginning. If we hit one of these command an exception will be
775 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 776 #
76f69d1c 777 # Example:
4a2587c6
DW
778 # quagga(config-if)# ip ospf authentication message-digest 1.1.1.1
779 # quagga(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 780 # % Unknown command.
4a2587c6 781 # quagga(config-if)# no ip ospf authentication message-digest
76f69d1c 782 # % Unknown command.
4a2587c6
DW
783 # quagga(config-if)# no ip ospf authentication
784 # quagga(config-if)#
2fc76430
DS
785
786 while True:
2fc76430
DS
787 try:
788 _ = subprocess.check_output(cmd)
789
790 except subprocess.CalledProcessError:
791
792 # - Pull the last entry from cmd (this would be
793 # 'no ip ospf authentication message-digest 1.1.1.1' in
794 # our example above
795 # - Split that last entry by whitespace and drop the last word
c50aceee 796 logger.warning('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
797 last_arg = cmd[-1].split(' ')
798
799 if len(last_arg) <= 2:
800 logger.error('"%s" we failed to remove this command', original_cmd)
801 break
802
803 new_last_arg = last_arg[0:-1]
804 cmd[-1] = ' '.join(new_last_arg)
805 else:
c50aceee 806 logger.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
807 break
808
2fc76430 809 if lines_to_add:
4a2587c6
DW
810 lines_to_configure = []
811
2fc76430
DS
812 for (ctx_keys, line) in lines_to_add:
813
814 if line == '!':
815 continue
816
4a2587c6
DW
817 cmd = line_for_vtysh_file(ctx_keys, line, False)
818 lines_to_configure.append(cmd)
819
820 if lines_to_configure:
821 random_string = ''.join(random.SystemRandom().choice(
822 string.ascii_uppercase +
823 string.digits) for _ in range(6))
824
825 filename = "/var/run/quagga/reload-%s.txt" % random_string
826 logger.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
827
828 with open(filename, 'w') as fh:
829 for line in lines_to_configure:
830 fh.write(line + '\n')
831 subprocess.call(['/usr/bin/vtysh', '-f', filename])
832 os.unlink(filename)
2fc76430 833
76f69d1c 834 if restart_bgp:
651415bd
DS
835 subprocess.call(['sudo', 'systemctl', 'reset-failed', 'quagga'])
836 subprocess.call(['sudo', 'systemctl', '--no-block', 'restart', 'quagga'])