]>
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 | |
4a2587c6 | 16 | import random |
9fe88bc7 | 17 | import re |
4a2587c6 | 18 | import string |
2fc76430 DS |
19 | import subprocess |
20 | import sys | |
21 | from collections import OrderedDict | |
22 | from ipaddr import IPv6Address | |
4a2587c6 DW |
23 | from pprint import pformat |
24 | ||
2fc76430 DS |
25 | |
26 | class Context(object): | |
4a2587c6 | 27 | |
2fc76430 DS |
28 | """ |
29 | A Context object represents a section of quagga configuration such as: | |
30 | ! | |
31 | interface swp3 | |
32 | description swp3 -> r8's swp1 | |
33 | ipv6 nd suppress-ra | |
34 | link-detect | |
35 | ! | |
36 | ||
37 | or a single line context object such as this: | |
38 | ||
39 | ip 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 | ||
65 | class 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 | ! | |
179 | interface swp52 | |
180 | ipv6 nd suppress-ra | |
181 | link-detect | |
182 | ! | |
183 | end | |
184 | router 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 | ! | |
194 | end | |
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 | ! | |
201 | end | |
202 | router ospf | |
203 | ospf router-id 10.0.0.1 | |
204 | log-adjacency-changes detail | |
205 | timers throttle spf 0 50 5000 | |
206 | ! | |
207 | end | |
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 |
322 | def 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 | |
373 | def 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 |
414 | def 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 |
435 | def 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 | 442 | def 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 |
553 | def 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 | |
615 | if __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']) |