]> git.proxmox.com Git - mirror_frr.git/blame - tools/quagga-reload.py
zebra: Allow vrfs to be defined and displayed before netlink vrf add
[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
442def ignore_bgp_swpx_peergroup(lines_to_add, lines_to_del):
443 '''
444 BGP changed how it displays swpX peers that are part of peer-group. Older
445 versions of quagga would display these on separate lines:
446 neighbor swp1 interface
447 neighbor swp1 peer-group FOO
448
449 but today we display via a single line
450 neighbor swp1 interface peer-group FOO
451
452 This change confuses quagga-reload.py so check to see if we are deleting
453 neighbor swp1 interface peer-group FOO
454
455 and adding
456 neighbor swp1 interface
457 neighbor swp1 peer-group FOO
458
459 If so then chop the del line and the corresponding add lines
460 '''
461
462 # Quite possibly the most confusing (while accurate) variable names in history
463 lines_to_add_to_del = []
464 lines_to_del_to_del = []
465
466 for (ctx_keys, line) in lines_to_del:
467 if ctx_keys[0].startswith('router bgp') and line.startswith('neighbor '):
468 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
469
470 if re_swpx_int_peergroup:
471 swpx = re_swpx_int_peergroup.group(1)
472 peergroup = re_swpx_int_peergroup.group(2)
473 swpx_interface = "neighbor %s interface" % swpx
474 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
475
476 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
477 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
478
479 if found_add_swpx_interface and found_add_swpx_peergroup:
480 lines_to_del_to_del.append((ctx_keys, line))
481 lines_to_add_to_del.append((ctx_keys, swpx_interface))
482 lines_to_add_to_del.append((ctx_keys, swpx_peergroup))
483
484 for (ctx_keys, line) in lines_to_del_to_del:
485 lines_to_del.remove((ctx_keys, line))
486
487 for (ctx_keys, line) in lines_to_add_to_del:
488 lines_to_add.remove((ctx_keys, line))
489
490 return (lines_to_add, lines_to_del)
491
492
2fc76430
DS
493def compare_context_objects(newconf, running):
494 """
495 Create a context diff for the two specified contexts
496 """
497
498 # Compare the two Config objects to find the lines that we need to add/del
499 lines_to_add = []
500 lines_to_del = []
514665b9 501 restart_bgpd = False
2fc76430
DS
502
503 # Find contexts that are in newconf but not in running
504 # Find contexts that are in running but not in newconf
505 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
506
507 if running_ctx_keys not in newconf.contexts:
508
76f69d1c
DW
509 # Check if bgp's local ASN has changed. If yes, just restart it
510 if "router bgp" in running_ctx_keys[0]:
511 restart_bgpd = True
512 continue
514665b9 513
2fc76430 514 # Non-global context
514665b9 515 if running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
516 lines_to_del.append((running_ctx_keys, None))
517
518 # Global context
519 else:
520 for line in running_ctx.lines:
521 lines_to_del.append((running_ctx_keys, line))
522
523 # Find the lines within each context to add
524 # Find the lines within each context to del
525 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
526
527 if newconf_ctx_keys in running.contexts:
528 running_ctx = running.contexts[newconf_ctx_keys]
529
530 for line in newconf_ctx.lines:
531 if line not in running_ctx.dlines:
532 lines_to_add.append((newconf_ctx_keys, line))
533
534 for line in running_ctx.lines:
535 if line not in newconf_ctx.dlines:
536 lines_to_del.append((newconf_ctx_keys, line))
537
538 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
539
540 if newconf_ctx_keys not in running.contexts:
514665b9 541
76f69d1c
DW
542 # If its "router bgp" and we're restarting bgp, skip doing
543 # anything specific for bgp
544 if "router bgp" in newconf_ctx_keys[0] and restart_bgpd:
545 continue
2fc76430
DS
546 lines_to_add.append((newconf_ctx_keys, None))
547
548 for line in newconf_ctx.lines:
549 lines_to_add.append((newconf_ctx_keys, line))
550
9fe88bc7
DW
551 (lines_to_add, lines_to_del) = ignore_bgp_swpx_peergroup(lines_to_add, lines_to_del)
552
514665b9 553 return (lines_to_add, lines_to_del, restart_bgpd)
2fc76430
DS
554
555if __name__ == '__main__':
556 # Command line options
557 parser = argparse.ArgumentParser(description='Dynamically apply diff in quagga configs')
558 parser.add_argument('--input', help='Read running config from file instead of "show running"')
559 group = parser.add_mutually_exclusive_group(required=True)
560 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
561 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
562 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
563 parser.add_argument('filename', help='Location of new quagga config file')
564 args = parser.parse_args()
565
566 # Logging
567 # For --test log to stdout
568 # For --reload log to /var/log/quagga/quagga-reload.log
569 if args.test:
c50aceee 570 logging.basicConfig(level=logging.INFO,
2fc76430
DS
571 format='%(asctime)s %(levelname)5s: %(message)s')
572 elif args.reload:
573 if not os.path.isdir('/var/log/quagga/'):
574 os.makedirs('/var/log/quagga/')
575
576 logging.basicConfig(filename='/var/log/quagga/quagga-reload.log',
c50aceee 577 level=logging.INFO,
2fc76430
DS
578 format='%(asctime)s %(levelname)5s: %(message)s')
579
580 # argparse should prevent this from happening but just to be safe...
581 else:
582 raise Exception('Must specify --reload or --test')
583 logger = logging.getLogger(__name__)
584
76f69d1c
DW
585 # Verify the new config file is valid
586 if not os.path.isfile(args.filename):
587 print "Filename %s does not exist" % args.filename
588 sys.exit(1)
589
590 if not os.path.getsize(args.filename):
591 print "Filename %s is an empty file" % args.filename
592 sys.exit(1)
593
76f69d1c
DW
594 # Verify that 'service integrated-vtysh-config' is configured
595 vtysh_filename = '/etc/quagga/vtysh.conf'
76f69d1c
DW
596 service_integrated_vtysh_config = False
597
f850d14d
DW
598 if os.path.isfile(vtysh_filename):
599 with open(vtysh_filename, 'r') as fh:
600 for line in fh.readlines():
601 line = line.strip()
76f69d1c 602
f850d14d
DW
603 if line == 'service integrated-vtysh-config':
604 service_integrated_vtysh_config = True
605 break
76f69d1c
DW
606
607 if not service_integrated_vtysh_config:
608 print "'service integrated-vtysh-config' is not configured, this is required for 'service quagga reload'"
609 sys.exit(1)
2fc76430 610
c50aceee
DW
611 if args.debug:
612 logger.setLevel(logging.DEBUG)
613
614 logger.info('Called via "%s"', str(args))
615
2fc76430
DS
616 # Create a Config object from the config generated by newconf
617 newconf = Config()
618 newconf.load_from_file(args.filename)
2fc76430
DS
619
620 if args.test:
621
622 # Create a Config object from the running config
623 running = Config()
624
625 if args.input:
626 running.load_from_file(args.input)
627 else:
628 running.load_from_show_running()
629
514665b9 630 (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running)
4a2587c6 631 lines_to_configure = []
2fc76430
DS
632
633 if lines_to_del:
634 print "\nLines To Delete"
635 print "==============="
636
637 for (ctx_keys, line) in lines_to_del:
638
639 if line == '!':
640 continue
641
4a2587c6
DW
642 cmd = line_for_vtysh_file(ctx_keys, line, True)
643 lines_to_configure.append(cmd)
9fe88bc7 644 print cmd
2fc76430
DS
645
646 if lines_to_add:
647 print "\nLines To Add"
648 print "============"
649
650 for (ctx_keys, line) in lines_to_add:
651
652 if line == '!':
653 continue
654
4a2587c6
DW
655 cmd = line_for_vtysh_file(ctx_keys, line, False)
656 lines_to_configure.append(cmd)
9fe88bc7 657 print cmd
2fc76430 658
76f69d1c 659 if restart_bgp:
4a2587c6 660 print "BGP local AS changed, bgpd would restart"
2fc76430
DS
661
662 elif args.reload:
663
c50aceee 664 logger.debug('New Quagga Config\n%s', newconf.get_lines())
2fc76430
DS
665
666 # This looks a little odd but we have to do this twice...here is why
667 # If the user had this running bgp config:
4a2587c6 668 #
2fc76430
DS
669 # router bgp 10
670 # neighbor 1.1.1.1 remote-as 50
671 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 672 #
2fc76430 673 # and this config in the newconf config file
4a2587c6 674 #
2fc76430
DS
675 # router bgp 10
676 # neighbor 1.1.1.1 remote-as 999
677 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
678 #
679 #
2fc76430
DS
680 # Then the script will do
681 # - no neighbor 1.1.1.1 remote-as 50
682 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 683 #
2fc76430
DS
684 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
685 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
686 # configs again to put this line back.
687
688 for x in range(2):
689 running = Config()
690 running.load_from_show_running()
c50aceee 691 logger.debug('Running Quagga Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 692
514665b9 693 (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running)
2fc76430
DS
694
695 if lines_to_del:
696 for (ctx_keys, line) in lines_to_del:
697
698 if line == '!':
699 continue
700
4a2587c6
DW
701 # 'no' commands are tricky, we can't just put them in a file and
702 # vtysh -f that file. See the next comment for an explanation
703 # of their quirks
2fc76430
DS
704 cmd = line_to_vtysh_conft(ctx_keys, line, True)
705 original_cmd = cmd
706
76f69d1c
DW
707 # Some commands in quagga are picky about taking a "no" of the entire line.
708 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
709 # only the beginning. If we hit one of these command an exception will be
710 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 711 #
76f69d1c 712 # Example:
4a2587c6
DW
713 # quagga(config-if)# ip ospf authentication message-digest 1.1.1.1
714 # quagga(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 715 # % Unknown command.
4a2587c6 716 # quagga(config-if)# no ip ospf authentication message-digest
76f69d1c 717 # % Unknown command.
4a2587c6
DW
718 # quagga(config-if)# no ip ospf authentication
719 # quagga(config-if)#
2fc76430
DS
720
721 while True:
2fc76430
DS
722 try:
723 _ = subprocess.check_output(cmd)
724
725 except subprocess.CalledProcessError:
726
727 # - Pull the last entry from cmd (this would be
728 # 'no ip ospf authentication message-digest 1.1.1.1' in
729 # our example above
730 # - Split that last entry by whitespace and drop the last word
c50aceee 731 logger.warning('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
732 last_arg = cmd[-1].split(' ')
733
734 if len(last_arg) <= 2:
735 logger.error('"%s" we failed to remove this command', original_cmd)
736 break
737
738 new_last_arg = last_arg[0:-1]
739 cmd[-1] = ' '.join(new_last_arg)
740 else:
c50aceee 741 logger.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
742 break
743
2fc76430 744 if lines_to_add:
4a2587c6
DW
745 lines_to_configure = []
746
2fc76430
DS
747 for (ctx_keys, line) in lines_to_add:
748
749 if line == '!':
750 continue
751
4a2587c6
DW
752 cmd = line_for_vtysh_file(ctx_keys, line, False)
753 lines_to_configure.append(cmd)
754
755 if lines_to_configure:
756 random_string = ''.join(random.SystemRandom().choice(
757 string.ascii_uppercase +
758 string.digits) for _ in range(6))
759
760 filename = "/var/run/quagga/reload-%s.txt" % random_string
761 logger.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
762
763 with open(filename, 'w') as fh:
764 for line in lines_to_configure:
765 fh.write(line + '\n')
766 subprocess.call(['/usr/bin/vtysh', '-f', filename])
767 os.unlink(filename)
2fc76430 768
76f69d1c 769 if restart_bgp:
651415bd
DS
770 subprocess.call(['sudo', 'systemctl', 'reset-failed', 'quagga'])
771 subprocess.call(['sudo', 'systemctl', '--no-block', 'restart', 'quagga'])