]>
Commit | Line | Data |
---|---|---|
2fc76430 DS |
1 | #!/usr/bin/python |
2 | ||
3 | """ | |
4 | This 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 | ||
12 | import argparse | |
13 | import copy | |
14 | import logging | |
15 | import os | |
16 | import subprocess | |
17 | import sys | |
18 | from collections import OrderedDict | |
19 | from ipaddr import IPv6Address | |
20 | ||
21 | class Context(object): | |
22 | """ | |
23 | A Context object represents a section of quagga configuration such as: | |
24 | ! | |
25 | interface swp3 | |
26 | description swp3 -> r8's swp1 | |
27 | ipv6 nd suppress-ra | |
28 | link-detect | |
29 | ! | |
30 | ||
31 | or a single line context object such as this: | |
32 | ||
33 | ip forwarding | |
34 | ||
35 | """ | |
36 | ||
37 | def __init__(self, keys, lines): | |
38 | self.keys = keys | |
39 | self.lines = lines | |
40 | ||
41 | # Keep a dictionary of the lines, this is to make it easy to tell if a | |
42 | # line exists in this Context | |
43 | self.dlines = OrderedDict() | |
44 | ||
45 | for ligne in lines: | |
46 | self.dlines[ligne] = True | |
47 | ||
48 | def add_lines(self, lines): | |
49 | """ | |
50 | Add lines to specified context | |
51 | """ | |
52 | ||
53 | self.lines.extend(lines) | |
54 | ||
55 | for ligne in lines: | |
56 | self.dlines[ligne] = True | |
57 | ||
58 | ||
59 | class Config(object): | |
60 | """ | |
61 | A quagga configuration is stored in a Config object. A Config object | |
62 | contains a dictionary of Context objects where the Context keys | |
63 | ('router ospf' for example) are our dictionary key. | |
64 | """ | |
65 | ||
66 | def __init__(self): | |
67 | self.lines = [] | |
68 | self.contexts = OrderedDict() | |
69 | ||
70 | def load_from_file(self, filename): | |
71 | """ | |
72 | Read configuration from specified file and slurp it into internal memory | |
73 | The internal representation has been marked appropriately by passing it | |
74 | through vtysh with the -m parameter | |
75 | """ | |
76f69d1c | 76 | logger.debug('Loading Config object from file %s', filename) |
2fc76430 DS |
77 | |
78 | try: | |
79 | file_output = subprocess.check_output(['vtysh', '-m', '-f', filename]) | |
80 | except subprocess.CalledProcessError as e: | |
81 | logger.error('vtysh marking of config file %s failed with error %s:', filename, str(e)) | |
82 | print "vtysh marking of file %s failed with error: %s" %(filename, str(e)) | |
83 | sys.exit(1) | |
84 | ||
85 | for line in file_output.split('\n'): | |
86 | line = line.strip() | |
87 | if ":" in line: | |
88 | qv6_line = get_normalized_ipv6_line(line) | |
89 | self.lines.append(qv6_line) | |
90 | else: | |
91 | self.lines.append(line) | |
92 | ||
93 | self.load_contexts() | |
94 | ||
95 | def load_from_show_running(self): | |
96 | """ | |
97 | Read running configuration and slurp it into internal memory | |
98 | The internal representation has been marked appropriately by passing it | |
99 | through vtysh with the -m parameter | |
100 | """ | |
76f69d1c | 101 | logger.debug('Loading Config object from vtysh show running') |
2fc76430 DS |
102 | |
103 | try: | |
104 | config_text = subprocess.check_output("vtysh -c 'show run' | tail -n +4 | vtysh -m -f -", shell=True) | |
105 | except subprocess.CalledProcessError as e: | |
106 | logger.error('vtysh marking of running config failed with error %s:', str(e)) | |
107 | print "vtysh marking of running config failed with error %s:" %(str(e)) | |
108 | sys.exit(1) | |
109 | ||
110 | ||
111 | for line in config_text.split('\n'): | |
112 | line = line.strip() | |
113 | ||
114 | if (line == 'Building configuration...' or | |
115 | line == 'Current configuration:' or | |
116 | not line): | |
117 | continue | |
118 | ||
119 | self.lines.append(line) | |
120 | ||
121 | self.load_contexts() | |
122 | ||
123 | def get_lines(self): | |
124 | """ | |
125 | Return the lines read in from the configuration | |
126 | """ | |
127 | ||
128 | return '\n'.join(self.lines) | |
129 | ||
130 | def get_contexts(self): | |
131 | """ | |
132 | Return the parsed context as strings for display, log etc. | |
133 | """ | |
134 | ||
135 | for (_, ctx) in sorted(self.contexts.iteritems()): | |
136 | print str(ctx) + '\n' | |
137 | ||
138 | def save_contexts(self, key, lines): | |
139 | """ | |
140 | Save the provided key and lines as a context | |
141 | """ | |
142 | ||
143 | if not key: | |
144 | return | |
145 | ||
146 | if lines: | |
147 | if tuple(key) not in self.contexts: | |
148 | ctx = Context(tuple(key), lines) | |
149 | self.contexts[tuple(key)] = ctx | |
150 | else: | |
151 | ctx = self.contexts[tuple(key)] | |
152 | ctx.add_lines(lines) | |
153 | ||
154 | else: | |
155 | if tuple(key) not in self.contexts: | |
156 | ctx = Context(tuple(key), []) | |
157 | self.contexts[tuple(key)] = ctx | |
158 | ||
159 | def load_contexts(self): | |
160 | """ | |
161 | Parse the configuration and create contexts for each appropriate block | |
162 | """ | |
163 | ||
164 | current_context_lines = [] | |
165 | ctx_keys = [] | |
166 | ||
167 | ''' | |
168 | The end of a context is flagged via the 'end' keyword: | |
169 | ||
170 | ! | |
171 | interface swp52 | |
172 | ipv6 nd suppress-ra | |
173 | link-detect | |
174 | ! | |
175 | end | |
176 | router bgp 10 | |
177 | bgp router-id 10.0.0.1 | |
178 | bgp log-neighbor-changes | |
179 | no bgp default ipv4-unicast | |
180 | neighbor EBGP peer-group | |
181 | neighbor EBGP advertisement-interval 1 | |
182 | neighbor EBGP timers connect 10 | |
183 | neighbor 2001:40:1:4::6 remote-as 40 | |
184 | neighbor 2001:40:1:8::a remote-as 40 | |
185 | ! | |
186 | end | |
187 | address-family ipv6 | |
188 | neighbor IBGPv6 activate | |
189 | neighbor 2001:10::2 peer-group IBGPv6 | |
190 | neighbor 2001:10::3 peer-group IBGPv6 | |
191 | exit-address-family | |
192 | ! | |
193 | end | |
194 | router ospf | |
195 | ospf router-id 10.0.0.1 | |
196 | log-adjacency-changes detail | |
197 | timers throttle spf 0 50 5000 | |
198 | ! | |
199 | end | |
200 | ''' | |
201 | ||
202 | # The code assumes that its working on the output from the "vtysh -m" | |
203 | # command. That provides the appropriate markers to signify end of | |
204 | # a context. This routine uses that to build the contexts for the | |
205 | # config. | |
206 | # | |
207 | # There are single line contexts such as "log file /media/node/zebra.log" | |
208 | # and multi-line contexts such as "router ospf" and subcontexts | |
209 | # within a context such as "address-family" within "router bgp" | |
210 | # In each of these cases, the first line of the context becomes the | |
211 | # key of the context. So "router bgp 10" is the key for the non-address | |
212 | # family part of bgp, "router bgp 10, address-family ipv6 unicast" is | |
213 | # the key for the subcontext and so on. | |
214 | ||
215 | ctx_keys = [] | |
216 | main_ctx_key = [] | |
217 | new_ctx = True | |
218 | number_of_lines = len(self.lines) | |
219 | ||
220 | # the keywords that we know are single line contexts. bgp in this case | |
221 | # is not the main router bgp block, but enabling multi-instance | |
222 | oneline_ctx_keywords = ("ip ", "ipv6 ", "log ", "hostname ", "zebra ", "ptm-enable", "debug ", "service ", "enable ", "password ", "access-list ", "bgp ") | |
223 | ||
224 | for line in self.lines: | |
225 | ||
226 | if not line: | |
227 | continue | |
228 | ||
229 | if line.startswith('!') or line.startswith('#'): | |
230 | continue | |
231 | ||
232 | # one line contexts | |
233 | if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords): | |
234 | self.save_contexts(ctx_keys, current_context_lines) | |
235 | ||
236 | # Start a new context | |
237 | main_ctx_key = [] | |
238 | ctx_keys = [line,] | |
239 | current_context_lines = [] | |
240 | ||
76f69d1c | 241 | logger.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
2fc76430 DS |
242 | self.save_contexts(ctx_keys, current_context_lines) |
243 | new_ctx = True | |
244 | ||
245 | elif line == "end": | |
246 | self.save_contexts(ctx_keys, current_context_lines) | |
76f69d1c | 247 | logger.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys) |
2fc76430 DS |
248 | |
249 | # Start a new context | |
250 | new_ctx = True | |
251 | main_ctx_key = [] | |
252 | ctx_keys = [] | |
253 | current_context_lines = [] | |
254 | ||
255 | elif line == "exit-address-family" or line == "exit": | |
256 | # if this exit is for address-family ipv4 unicast, ignore the pop | |
257 | if main_ctx_key: | |
258 | self.save_contexts(ctx_keys, current_context_lines) | |
259 | ||
260 | # Start a new context | |
261 | ctx_keys = copy.deepcopy(main_ctx_key) | |
262 | current_context_lines = [] | |
76f69d1c | 263 | logger.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys) |
2fc76430 DS |
264 | |
265 | elif new_ctx is True: | |
266 | if not main_ctx_key: | |
267 | ctx_keys = [line,] | |
268 | else: | |
269 | ctx_keys = copy.deepcopy(main_ctx_key) | |
270 | main_ctx_key = [] | |
271 | ||
272 | current_context_lines = [] | |
273 | new_ctx = False | |
76f69d1c | 274 | logger.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
2fc76430 DS |
275 | |
276 | elif "address-family " in line: | |
277 | main_ctx_key = [] | |
278 | ||
279 | if line != "address-family ipv4 unicast": | |
280 | # Save old context first | |
281 | self.save_contexts(ctx_keys, current_context_lines) | |
282 | current_context_lines = [] | |
283 | main_ctx_key = copy.deepcopy(ctx_keys) | |
76f69d1c | 284 | logger.debug('LINE %-50s: entering sub-context, append to ctx_keys', line) |
2fc76430 | 285 | |
2fc76430 DS |
286 | if line == "address-family ipv6": |
287 | ctx_keys.append("address-family ipv6 unicast") | |
288 | else: | |
289 | ctx_keys.append(line) | |
290 | ||
291 | else: | |
292 | # Continuing in an existing context, add non-commented lines to it | |
293 | current_context_lines.append(line) | |
76f69d1c | 294 | logger.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys) |
2fc76430 DS |
295 | |
296 | # Save the context of the last one | |
297 | self.save_contexts(ctx_keys, current_context_lines) | |
298 | ||
299 | def line_to_vtysh_conft(ctx_keys, line, delete): | |
300 | """ | |
301 | Spit out the vtysh command for the specified context line | |
302 | """ | |
303 | ||
304 | cmd = [] | |
305 | cmd.append('vtysh') | |
306 | cmd.append('-c') | |
307 | cmd.append('conf t') | |
308 | ||
309 | if line: | |
310 | for ctx_key in ctx_keys: | |
311 | cmd.append('-c') | |
312 | cmd.append(ctx_key) | |
313 | ||
314 | line = line.lstrip() | |
315 | ||
316 | if delete: | |
317 | cmd.append('-c') | |
318 | ||
319 | if line.startswith('no '): | |
320 | cmd.append('%s' % line[3:]) | |
321 | else: | |
322 | cmd.append('no %s' % line) | |
323 | ||
324 | else: | |
325 | cmd.append('-c') | |
326 | cmd.append(line) | |
327 | ||
328 | # If line is None then we are typically deleting an entire | |
329 | # context ('no router ospf' for example) | |
330 | else: | |
331 | ||
332 | if delete: | |
333 | ||
334 | # Only put the 'no' on the last sub-context | |
335 | for ctx_key in ctx_keys: | |
336 | cmd.append('-c') | |
337 | ||
338 | if ctx_key == ctx_keys[-1]: | |
339 | cmd.append('no %s' % ctx_key) | |
340 | else: | |
341 | cmd.append('%s' % ctx_key) | |
342 | else: | |
343 | for ctx_key in ctx_keys: | |
344 | cmd.append('-c') | |
345 | cmd.append(ctx_key) | |
346 | ||
347 | return cmd | |
348 | ||
349 | def get_normalized_ipv6_line(line): | |
350 | """ | |
351 | Return a normalized IPv6 line as produced by quagga, | |
352 | with all letters in lower case and trailing and leading | |
353 | zeros removed | |
354 | """ | |
355 | norm_line = "" | |
356 | words = line.split(' ') | |
357 | for word in words: | |
358 | if ":" in word: | |
359 | try: | |
360 | norm_word = str(IPv6Address(word)).lower() | |
361 | except: | |
362 | norm_word = word | |
363 | else: | |
364 | norm_word = word | |
365 | norm_line = norm_line + " " + norm_word | |
366 | ||
367 | return norm_line.strip() | |
368 | ||
369 | def compare_context_objects(newconf, running): | |
370 | """ | |
371 | Create a context diff for the two specified contexts | |
372 | """ | |
373 | ||
374 | # Compare the two Config objects to find the lines that we need to add/del | |
375 | lines_to_add = [] | |
376 | lines_to_del = [] | |
514665b9 | 377 | restart_bgpd = False |
2fc76430 DS |
378 | |
379 | # Find contexts that are in newconf but not in running | |
380 | # Find contexts that are in running but not in newconf | |
381 | for (running_ctx_keys, running_ctx) in running.contexts.iteritems(): | |
382 | ||
383 | if running_ctx_keys not in newconf.contexts: | |
384 | ||
76f69d1c DW |
385 | # Check if bgp's local ASN has changed. If yes, just restart it |
386 | if "router bgp" in running_ctx_keys[0]: | |
387 | restart_bgpd = True | |
388 | continue | |
514665b9 | 389 | |
2fc76430 | 390 | # Non-global context |
514665b9 | 391 | if running_ctx_keys and not any("address-family" in key for key in running_ctx_keys): |
2fc76430 DS |
392 | lines_to_del.append((running_ctx_keys, None)) |
393 | ||
394 | # Global context | |
395 | else: | |
396 | for line in running_ctx.lines: | |
397 | lines_to_del.append((running_ctx_keys, line)) | |
398 | ||
399 | # Find the lines within each context to add | |
400 | # Find the lines within each context to del | |
401 | for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems(): | |
402 | ||
403 | if newconf_ctx_keys in running.contexts: | |
404 | running_ctx = running.contexts[newconf_ctx_keys] | |
405 | ||
406 | for line in newconf_ctx.lines: | |
407 | if line not in running_ctx.dlines: | |
408 | lines_to_add.append((newconf_ctx_keys, line)) | |
409 | ||
410 | for line in running_ctx.lines: | |
411 | if line not in newconf_ctx.dlines: | |
412 | lines_to_del.append((newconf_ctx_keys, line)) | |
413 | ||
414 | for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems(): | |
415 | ||
416 | if newconf_ctx_keys not in running.contexts: | |
514665b9 | 417 | |
76f69d1c DW |
418 | # If its "router bgp" and we're restarting bgp, skip doing |
419 | # anything specific for bgp | |
420 | if "router bgp" in newconf_ctx_keys[0] and restart_bgpd: | |
421 | continue | |
2fc76430 DS |
422 | lines_to_add.append((newconf_ctx_keys, None)) |
423 | ||
424 | for line in newconf_ctx.lines: | |
425 | lines_to_add.append((newconf_ctx_keys, line)) | |
426 | ||
514665b9 | 427 | return (lines_to_add, lines_to_del, restart_bgpd) |
2fc76430 DS |
428 | |
429 | if __name__ == '__main__': | |
430 | # Command line options | |
431 | parser = argparse.ArgumentParser(description='Dynamically apply diff in quagga configs') | |
432 | parser.add_argument('--input', help='Read running config from file instead of "show running"') | |
433 | group = parser.add_mutually_exclusive_group(required=True) | |
434 | group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False) | |
435 | group.add_argument('--test', action='store_true', help='Show the deltas', default=False) | |
436 | parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False) | |
437 | parser.add_argument('filename', help='Location of new quagga config file') | |
438 | args = parser.parse_args() | |
439 | ||
440 | # Logging | |
441 | # For --test log to stdout | |
442 | # For --reload log to /var/log/quagga/quagga-reload.log | |
443 | if args.test: | |
444 | logging.basicConfig(level=logging.DEBUG, | |
445 | format='%(asctime)s %(levelname)5s: %(message)s') | |
446 | elif args.reload: | |
447 | if not os.path.isdir('/var/log/quagga/'): | |
448 | os.makedirs('/var/log/quagga/') | |
449 | ||
450 | logging.basicConfig(filename='/var/log/quagga/quagga-reload.log', | |
451 | level=logging.DEBUG, | |
452 | format='%(asctime)s %(levelname)5s: %(message)s') | |
453 | ||
454 | # argparse should prevent this from happening but just to be safe... | |
455 | else: | |
456 | raise Exception('Must specify --reload or --test') | |
457 | logger = logging.getLogger(__name__) | |
458 | ||
76f69d1c DW |
459 | # Verify the new config file is valid |
460 | if not os.path.isfile(args.filename): | |
461 | print "Filename %s does not exist" % args.filename | |
462 | sys.exit(1) | |
463 | ||
464 | if not os.path.getsize(args.filename): | |
465 | print "Filename %s is an empty file" % args.filename | |
466 | sys.exit(1) | |
467 | ||
468 | ||
469 | # Verify that 'service integrated-vtysh-config' is configured | |
470 | vtysh_filename = '/etc/quagga/vtysh.conf' | |
76f69d1c DW |
471 | service_integrated_vtysh_config = False |
472 | ||
f850d14d DW |
473 | if os.path.isfile(vtysh_filename): |
474 | with open(vtysh_filename, 'r') as fh: | |
475 | for line in fh.readlines(): | |
476 | line = line.strip() | |
76f69d1c | 477 | |
f850d14d DW |
478 | if line == 'service integrated-vtysh-config': |
479 | service_integrated_vtysh_config = True | |
480 | break | |
76f69d1c DW |
481 | |
482 | if not service_integrated_vtysh_config: | |
483 | print "'service integrated-vtysh-config' is not configured, this is required for 'service quagga reload'" | |
484 | sys.exit(1) | |
2fc76430 DS |
485 | |
486 | # Create a Config object from the config generated by newconf | |
487 | newconf = Config() | |
488 | newconf.load_from_file(args.filename) | |
2fc76430 DS |
489 | |
490 | if args.test: | |
491 | ||
492 | # Create a Config object from the running config | |
493 | running = Config() | |
494 | ||
495 | if args.input: | |
496 | running.load_from_file(args.input) | |
497 | else: | |
498 | running.load_from_show_running() | |
499 | ||
514665b9 | 500 | (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running) |
2fc76430 DS |
501 | |
502 | if lines_to_del: | |
503 | print "\nLines To Delete" | |
504 | print "===============" | |
505 | ||
506 | for (ctx_keys, line) in lines_to_del: | |
507 | ||
508 | if line == '!': | |
509 | continue | |
510 | ||
511 | cmd = line_to_vtysh_conft(ctx_keys, line, True) | |
512 | print cmd | |
513 | ||
514 | if lines_to_add: | |
515 | print "\nLines To Add" | |
516 | print "============" | |
517 | ||
518 | for (ctx_keys, line) in lines_to_add: | |
519 | ||
520 | if line == '!': | |
521 | continue | |
522 | ||
523 | cmd = line_to_vtysh_conft(ctx_keys, line, False) | |
524 | print cmd | |
525 | ||
76f69d1c DW |
526 | if restart_bgp: |
527 | print "BGP local AS changed, restarting bgpd\n" | |
2fc76430 DS |
528 | |
529 | elif args.reload: | |
530 | ||
531 | logger.debug('Called via "%s"', str(args)) | |
532 | logger.info('New Quagga Config\n%s', newconf.get_lines()) | |
533 | ||
534 | # This looks a little odd but we have to do this twice...here is why | |
535 | # If the user had this running bgp config: | |
536 | ||
537 | # router bgp 10 | |
538 | # neighbor 1.1.1.1 remote-as 50 | |
539 | # neighbor 1.1.1.1 route-map FOO out | |
540 | ||
541 | # and this config in the newconf config file | |
542 | ||
543 | # router bgp 10 | |
544 | # neighbor 1.1.1.1 remote-as 999 | |
545 | # neighbor 1.1.1.1 route-map FOO out | |
546 | ||
547 | ||
548 | # Then the script will do | |
549 | # - no neighbor 1.1.1.1 remote-as 50 | |
550 | # - neighbor 1.1.1.1 remote-as 999 | |
551 | ||
552 | # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove | |
553 | # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the | |
554 | # configs again to put this line back. | |
555 | ||
556 | for x in range(2): | |
557 | running = Config() | |
558 | running.load_from_show_running() | |
559 | logger.info('Running Quagga Config (Pass #%d)\n%s', x, running.get_lines()) | |
560 | ||
514665b9 | 561 | (lines_to_add, lines_to_del, restart_bgp) = compare_context_objects(newconf, running) |
2fc76430 DS |
562 | |
563 | if lines_to_del: | |
564 | for (ctx_keys, line) in lines_to_del: | |
565 | ||
566 | if line == '!': | |
567 | continue | |
568 | ||
569 | cmd = line_to_vtysh_conft(ctx_keys, line, True) | |
570 | original_cmd = cmd | |
571 | ||
76f69d1c DW |
572 | # Some commands in quagga are picky about taking a "no" of the entire line. |
573 | # OSPF is bad about this, you can't "no" the entire line, you have to "no" | |
574 | # only the beginning. If we hit one of these command an exception will be | |
575 | # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again. | |
576 | # Example: | |
577 | # quagga(config-if)# ip ospf authentication message-digest 1.1.1.1 | |
578 | # quagga(config-if)# no ip ospf authentication message-digest 1.1.1.1 | |
579 | # % Unknown command. | |
580 | # quagga(config-if)# no ip ospf authentication message-digest | |
581 | # % Unknown command. | |
582 | # quagga(config-if)# no ip ospf authentication | |
583 | # quagga(config-if)# | |
2fc76430 DS |
584 | |
585 | while True: | |
586 | ||
587 | logger.info(cmd) | |
588 | ||
589 | try: | |
590 | _ = subprocess.check_output(cmd) | |
591 | ||
592 | except subprocess.CalledProcessError: | |
593 | ||
594 | # - Pull the last entry from cmd (this would be | |
595 | # 'no ip ospf authentication message-digest 1.1.1.1' in | |
596 | # our example above | |
597 | # - Split that last entry by whitespace and drop the last word | |
598 | logger.info('%s failed', str(cmd)) | |
599 | last_arg = cmd[-1].split(' ') | |
600 | ||
601 | if len(last_arg) <= 2: | |
602 | logger.error('"%s" we failed to remove this command', original_cmd) | |
603 | break | |
604 | ||
605 | new_last_arg = last_arg[0:-1] | |
606 | cmd[-1] = ' '.join(new_last_arg) | |
607 | else: | |
608 | logger.info('%s worked', str(cmd)) | |
609 | break | |
610 | ||
611 | ||
612 | if lines_to_add: | |
613 | for (ctx_keys, line) in lines_to_add: | |
614 | ||
615 | if line == '!': | |
616 | continue | |
617 | ||
618 | cmd = line_to_vtysh_conft(ctx_keys, line, False) | |
76f69d1c | 619 | logger.debug(cmd) |
2fc76430 DS |
620 | subprocess.call(cmd) |
621 | ||
76f69d1c DW |
622 | if restart_bgp: |
623 | cmd = ['sudo', 'service', 'quagga', 'restart', 'bgpd'] | |
624 | subprocess.call(cmd) |