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