]>
Commit | Line | Data |
---|---|---|
2fc76430 | 1 | #!/usr/bin/python |
50e24903 DS |
2 | # Quagga Reloader |
3 | # Copyright (C) 2014 Cumulus Networks, Inc. | |
4 | # | |
5 | # This file is part of Quagga. | |
6 | # | |
7 | # Quagga is free software; you can redistribute it and/or modify it | |
8 | # under the terms of the GNU General Public License as published by the | |
9 | # Free Software Foundation; either version 2, or (at your option) any | |
10 | # later version. | |
11 | # | |
12 | # Quagga is distributed in the hope that it will be useful, but | |
13 | # WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
15 | # General Public License for more details. | |
16 | # | |
17 | # You should have received a copy of the GNU General Public License | |
18 | # along with Quagga; see the file COPYING. If not, write to the Free | |
19 | # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA | |
20 | # 02111-1307, USA. | |
21 | # | |
2fc76430 DS |
22 | """ |
23 | This program | |
24 | - reads a quagga configuration text file | |
25 | - reads quagga's current running configuration via "vtysh -c 'show running'" | |
26 | - compares the two configs and determines what commands to execute to | |
27 | synchronize quagga's running configuration with the configuation in the | |
28 | text file | |
29 | """ | |
30 | ||
31 | import argparse | |
32 | import copy | |
33 | import logging | |
34 | import os | |
4a2587c6 | 35 | import random |
9fe88bc7 | 36 | import re |
4a2587c6 | 37 | import string |
2fc76430 DS |
38 | import subprocess |
39 | import sys | |
40 | from collections import OrderedDict | |
41 | from ipaddr import IPv6Address | |
4a2587c6 DW |
42 | from pprint import pformat |
43 | ||
2fc76430 | 44 | |
a782e613 DW |
45 | log = logging.getLogger(__name__) |
46 | ||
47 | ||
276887bb DW |
48 | class VtyshMarkException(Exception): |
49 | pass | |
50 | ||
51 | ||
2fc76430 | 52 | class Context(object): |
4a2587c6 | 53 | |
2fc76430 DS |
54 | """ |
55 | A Context object represents a section of quagga configuration such as: | |
56 | ! | |
57 | interface swp3 | |
58 | description swp3 -> r8's swp1 | |
59 | ipv6 nd suppress-ra | |
60 | link-detect | |
61 | ! | |
62 | ||
63 | or a single line context object such as this: | |
64 | ||
65 | ip forwarding | |
66 | ||
67 | """ | |
68 | ||
69 | def __init__(self, keys, lines): | |
70 | self.keys = keys | |
71 | self.lines = lines | |
72 | ||
73 | # Keep a dictionary of the lines, this is to make it easy to tell if a | |
74 | # line exists in this Context | |
75 | self.dlines = OrderedDict() | |
76 | ||
77 | for ligne in lines: | |
78 | self.dlines[ligne] = True | |
79 | ||
80 | def add_lines(self, lines): | |
81 | """ | |
82 | Add lines to specified context | |
83 | """ | |
84 | ||
85 | self.lines.extend(lines) | |
86 | ||
87 | for ligne in lines: | |
88 | self.dlines[ligne] = True | |
89 | ||
90 | ||
91 | class Config(object): | |
4a2587c6 | 92 | |
2fc76430 DS |
93 | """ |
94 | A quagga configuration is stored in a Config object. A Config object | |
95 | contains a dictionary of Context objects where the Context keys | |
96 | ('router ospf' for example) are our dictionary key. | |
97 | """ | |
98 | ||
99 | def __init__(self): | |
100 | self.lines = [] | |
101 | self.contexts = OrderedDict() | |
102 | ||
103 | def load_from_file(self, filename): | |
104 | """ | |
105 | Read configuration from specified file and slurp it into internal memory | |
106 | The internal representation has been marked appropriately by passing it | |
107 | through vtysh with the -m parameter | |
108 | """ | |
a782e613 | 109 | log.info('Loading Config object from file %s', filename) |
2fc76430 DS |
110 | |
111 | try: | |
4a2587c6 | 112 | file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename]) |
2fc76430 | 113 | except subprocess.CalledProcessError as e: |
276887bb | 114 | raise VtyshMarkException(str(e)) |
2fc76430 DS |
115 | |
116 | for line in file_output.split('\n'): | |
117 | line = line.strip() | |
118 | if ":" in line: | |
119 | qv6_line = get_normalized_ipv6_line(line) | |
120 | self.lines.append(qv6_line) | |
121 | else: | |
122 | self.lines.append(line) | |
123 | ||
124 | self.load_contexts() | |
125 | ||
126 | def load_from_show_running(self): | |
127 | """ | |
128 | Read running configuration and slurp it into internal memory | |
129 | The internal representation has been marked appropriately by passing it | |
130 | through vtysh with the -m parameter | |
131 | """ | |
a782e613 | 132 | log.info('Loading Config object from vtysh show running') |
2fc76430 DS |
133 | |
134 | try: | |
4a2587c6 DW |
135 | config_text = subprocess.check_output( |
136 | "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -", | |
137 | shell=True) | |
2fc76430 | 138 | except subprocess.CalledProcessError as e: |
276887bb | 139 | raise VtyshMarkException(str(e)) |
2fc76430 | 140 | |
2fc76430 DS |
141 | for line in config_text.split('\n'): |
142 | line = line.strip() | |
143 | ||
144 | if (line == 'Building configuration...' or | |
145 | line == 'Current configuration:' or | |
4a2587c6 | 146 | not line): |
2fc76430 DS |
147 | continue |
148 | ||
149 | self.lines.append(line) | |
150 | ||
151 | self.load_contexts() | |
152 | ||
153 | def get_lines(self): | |
154 | """ | |
155 | Return the lines read in from the configuration | |
156 | """ | |
157 | ||
158 | return '\n'.join(self.lines) | |
159 | ||
160 | def get_contexts(self): | |
161 | """ | |
162 | Return the parsed context as strings for display, log etc. | |
163 | """ | |
164 | ||
165 | for (_, ctx) in sorted(self.contexts.iteritems()): | |
166 | print str(ctx) + '\n' | |
167 | ||
168 | def save_contexts(self, key, lines): | |
169 | """ | |
170 | Save the provided key and lines as a context | |
171 | """ | |
172 | ||
173 | if not key: | |
174 | return | |
175 | ||
176 | if lines: | |
177 | if tuple(key) not in self.contexts: | |
178 | ctx = Context(tuple(key), lines) | |
179 | self.contexts[tuple(key)] = ctx | |
180 | else: | |
181 | ctx = self.contexts[tuple(key)] | |
182 | ctx.add_lines(lines) | |
183 | ||
184 | else: | |
185 | if tuple(key) not in self.contexts: | |
186 | ctx = Context(tuple(key), []) | |
187 | self.contexts[tuple(key)] = ctx | |
188 | ||
189 | def load_contexts(self): | |
190 | """ | |
191 | Parse the configuration and create contexts for each appropriate block | |
192 | """ | |
193 | ||
194 | current_context_lines = [] | |
195 | ctx_keys = [] | |
196 | ||
197 | ''' | |
198 | The end of a context is flagged via the 'end' keyword: | |
199 | ||
200 | ! | |
201 | interface swp52 | |
202 | ipv6 nd suppress-ra | |
203 | link-detect | |
204 | ! | |
205 | end | |
206 | router bgp 10 | |
207 | bgp router-id 10.0.0.1 | |
208 | bgp log-neighbor-changes | |
209 | no bgp default ipv4-unicast | |
210 | neighbor EBGP peer-group | |
211 | neighbor EBGP advertisement-interval 1 | |
212 | neighbor EBGP timers connect 10 | |
213 | neighbor 2001:40:1:4::6 remote-as 40 | |
214 | neighbor 2001:40:1:8::a remote-as 40 | |
215 | ! | |
216 | end | |
217 | address-family ipv6 | |
218 | neighbor IBGPv6 activate | |
219 | neighbor 2001:10::2 peer-group IBGPv6 | |
220 | neighbor 2001:10::3 peer-group IBGPv6 | |
221 | exit-address-family | |
222 | ! | |
223 | end | |
224 | router ospf | |
225 | ospf router-id 10.0.0.1 | |
226 | log-adjacency-changes detail | |
227 | timers throttle spf 0 50 5000 | |
228 | ! | |
229 | end | |
230 | ''' | |
231 | ||
232 | # The code assumes that its working on the output from the "vtysh -m" | |
233 | # command. That provides the appropriate markers to signify end of | |
234 | # a context. This routine uses that to build the contexts for the | |
235 | # config. | |
236 | # | |
237 | # There are single line contexts such as "log file /media/node/zebra.log" | |
238 | # and multi-line contexts such as "router ospf" and subcontexts | |
239 | # within a context such as "address-family" within "router bgp" | |
240 | # In each of these cases, the first line of the context becomes the | |
241 | # key of the context. So "router bgp 10" is the key for the non-address | |
242 | # family part of bgp, "router bgp 10, address-family ipv6 unicast" is | |
243 | # the key for the subcontext and so on. | |
2fc76430 DS |
244 | ctx_keys = [] |
245 | main_ctx_key = [] | |
246 | new_ctx = True | |
2fc76430 DS |
247 | |
248 | # the keywords that we know are single line contexts. bgp in this case | |
249 | # is not the main router bgp block, but enabling multi-instance | |
2fed5dcd DW |
250 | oneline_ctx_keywords = ("access-list ", |
251 | "bgp ", | |
252 | "debug ", | |
253 | "dump ", | |
254 | "enable ", | |
255 | "hostname ", | |
256 | "ip ", | |
257 | "ipv6 ", | |
258 | "log ", | |
259 | "password ", | |
260 | "ptm-enable", | |
261 | "router-id ", | |
262 | "service ", | |
263 | "table ", | |
264 | "username ", | |
265 | "zebra ") | |
266 | ||
2fc76430 DS |
267 | for line in self.lines: |
268 | ||
269 | if not line: | |
270 | continue | |
271 | ||
272 | if line.startswith('!') or line.startswith('#'): | |
273 | continue | |
274 | ||
275 | # one line contexts | |
276 | if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords): | |
277 | self.save_contexts(ctx_keys, current_context_lines) | |
278 | ||
279 | # Start a new context | |
280 | main_ctx_key = [] | |
4a2587c6 | 281 | ctx_keys = [line, ] |
2fc76430 DS |
282 | current_context_lines = [] |
283 | ||
a782e613 | 284 | log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
2fc76430 DS |
285 | self.save_contexts(ctx_keys, current_context_lines) |
286 | new_ctx = True | |
287 | ||
288 | elif line == "end": | |
289 | self.save_contexts(ctx_keys, current_context_lines) | |
a782e613 | 290 | log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys) |
2fc76430 DS |
291 | |
292 | # Start a new context | |
293 | new_ctx = True | |
294 | main_ctx_key = [] | |
295 | ctx_keys = [] | |
296 | current_context_lines = [] | |
297 | ||
298 | elif line == "exit-address-family" or line == "exit": | |
299 | # if this exit is for address-family ipv4 unicast, ignore the pop | |
300 | if main_ctx_key: | |
301 | self.save_contexts(ctx_keys, current_context_lines) | |
302 | ||
303 | # Start a new context | |
304 | ctx_keys = copy.deepcopy(main_ctx_key) | |
305 | current_context_lines = [] | |
a782e613 | 306 | log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys) |
2fc76430 DS |
307 | |
308 | elif new_ctx is True: | |
309 | if not main_ctx_key: | |
4a2587c6 | 310 | ctx_keys = [line, ] |
2fc76430 DS |
311 | else: |
312 | ctx_keys = copy.deepcopy(main_ctx_key) | |
313 | main_ctx_key = [] | |
314 | ||
315 | current_context_lines = [] | |
316 | new_ctx = False | |
a782e613 | 317 | log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
2fc76430 DS |
318 | |
319 | elif "address-family " in line: | |
320 | main_ctx_key = [] | |
321 | ||
0b960b4d DW |
322 | # Save old context first |
323 | self.save_contexts(ctx_keys, current_context_lines) | |
324 | current_context_lines = [] | |
325 | main_ctx_key = copy.deepcopy(ctx_keys) | |
a782e613 | 326 | log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line) |
2fc76430 | 327 | |
0b960b4d DW |
328 | if line == "address-family ipv6": |
329 | ctx_keys.append("address-family ipv6 unicast") | |
330 | elif line == "address-family ipv4": | |
331 | ctx_keys.append("address-family ipv4 unicast") | |
332 | else: | |
333 | ctx_keys.append(line) | |
2fc76430 DS |
334 | |
335 | else: | |
336 | # Continuing in an existing context, add non-commented lines to it | |
337 | current_context_lines.append(line) | |
a782e613 | 338 | log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys) |
2fc76430 DS |
339 | |
340 | # Save the context of the last one | |
341 | self.save_contexts(ctx_keys, current_context_lines) | |
342 | ||
4a2587c6 | 343 | |
2fc76430 DS |
344 | def line_to_vtysh_conft(ctx_keys, line, delete): |
345 | """ | |
4a2587c6 | 346 | Return the vtysh command for the specified context line |
2fc76430 DS |
347 | """ |
348 | ||
349 | cmd = [] | |
350 | cmd.append('vtysh') | |
351 | cmd.append('-c') | |
352 | cmd.append('conf t') | |
353 | ||
354 | if line: | |
355 | for ctx_key in ctx_keys: | |
356 | cmd.append('-c') | |
357 | cmd.append(ctx_key) | |
358 | ||
359 | line = line.lstrip() | |
360 | ||
361 | if delete: | |
362 | cmd.append('-c') | |
363 | ||
364 | if line.startswith('no '): | |
365 | cmd.append('%s' % line[3:]) | |
366 | else: | |
367 | cmd.append('no %s' % line) | |
368 | ||
369 | else: | |
370 | cmd.append('-c') | |
371 | cmd.append(line) | |
372 | ||
373 | # If line is None then we are typically deleting an entire | |
374 | # context ('no router ospf' for example) | |
375 | else: | |
376 | ||
377 | if delete: | |
378 | ||
379 | # Only put the 'no' on the last sub-context | |
380 | for ctx_key in ctx_keys: | |
381 | cmd.append('-c') | |
382 | ||
383 | if ctx_key == ctx_keys[-1]: | |
384 | cmd.append('no %s' % ctx_key) | |
385 | else: | |
386 | cmd.append('%s' % ctx_key) | |
387 | else: | |
388 | for ctx_key in ctx_keys: | |
389 | cmd.append('-c') | |
390 | cmd.append(ctx_key) | |
391 | ||
392 | return cmd | |
393 | ||
4a2587c6 DW |
394 | |
395 | def line_for_vtysh_file(ctx_keys, line, delete): | |
396 | """ | |
397 | Return the command as it would appear in Quagga.conf | |
398 | """ | |
399 | cmd = [] | |
400 | ||
401 | if line: | |
402 | for (i, ctx_key) in enumerate(ctx_keys): | |
403 | cmd.append(' ' * i + ctx_key) | |
404 | ||
405 | line = line.lstrip() | |
406 | indent = len(ctx_keys) * ' ' | |
407 | ||
408 | if delete: | |
409 | if line.startswith('no '): | |
410 | cmd.append('%s%s' % (indent, line[3:])) | |
411 | else: | |
412 | cmd.append('%sno %s' % (indent, line)) | |
413 | ||
414 | else: | |
415 | cmd.append(indent + line) | |
416 | ||
417 | # If line is None then we are typically deleting an entire | |
418 | # context ('no router ospf' for example) | |
419 | else: | |
420 | if delete: | |
421 | ||
422 | # Only put the 'no' on the last sub-context | |
423 | for ctx_key in ctx_keys: | |
424 | ||
425 | if ctx_key == ctx_keys[-1]: | |
426 | cmd.append('no %s' % ctx_key) | |
427 | else: | |
428 | cmd.append('%s' % ctx_key) | |
429 | else: | |
430 | for ctx_key in ctx_keys: | |
431 | cmd.append(ctx_key) | |
432 | ||
433 | return '\n' + '\n'.join(cmd) | |
434 | ||
435 | ||
2fc76430 DS |
436 | def get_normalized_ipv6_line(line): |
437 | """ | |
438 | Return a normalized IPv6 line as produced by quagga, | |
439 | with all letters in lower case and trailing and leading | |
440 | zeros removed | |
441 | """ | |
442 | norm_line = "" | |
443 | words = line.split(' ') | |
444 | for word in words: | |
445 | if ":" in word: | |
446 | try: | |
447 | norm_word = str(IPv6Address(word)).lower() | |
448 | except: | |
449 | norm_word = word | |
450 | else: | |
451 | norm_word = word | |
452 | norm_line = norm_line + " " + norm_word | |
453 | ||
454 | return norm_line.strip() | |
455 | ||
4a2587c6 | 456 | |
9fe88bc7 DW |
457 | def line_exist(lines, target_ctx_keys, target_line): |
458 | for (ctx_keys, line) in lines: | |
459 | if ctx_keys == target_ctx_keys and line == target_line: | |
460 | return True | |
461 | return False | |
462 | ||
463 | ||
9b166171 | 464 | def ignore_delete_re_add_lines(lines_to_add, lines_to_del): |
9fe88bc7 DW |
465 | |
466 | # Quite possibly the most confusing (while accurate) variable names in history | |
467 | lines_to_add_to_del = [] | |
468 | lines_to_del_to_del = [] | |
469 | ||
470 | for (ctx_keys, line) in lines_to_del: | |
9b166171 DW |
471 | deleted = False |
472 | ||
926ea62e | 473 | if ctx_keys[0].startswith('router bgp') and line and line.startswith('neighbor '): |
9b166171 DW |
474 | """ |
475 | BGP changed how it displays swpX peers that are part of peer-group. Older | |
476 | versions of quagga would display these on separate lines: | |
477 | neighbor swp1 interface | |
478 | neighbor swp1 peer-group FOO | |
479 | ||
480 | but today we display via a single line | |
481 | neighbor swp1 interface peer-group FOO | |
482 | ||
483 | This change confuses quagga-reload.py so check to see if we are deleting | |
484 | neighbor swp1 interface peer-group FOO | |
485 | ||
486 | and adding | |
487 | neighbor swp1 interface | |
488 | neighbor swp1 peer-group FOO | |
489 | ||
490 | If so then chop the del line and the corresponding add lines | |
491 | """ | |
492 | ||
9fe88bc7 | 493 | re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line) |
9b166171 | 494 | re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line) |
9fe88bc7 | 495 | |
9b166171 DW |
496 | if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup: |
497 | swpx_interface = None | |
498 | swpx_peergroup = None | |
499 | ||
500 | if re_swpx_int_peergroup: | |
501 | swpx = re_swpx_int_peergroup.group(1) | |
502 | peergroup = re_swpx_int_peergroup.group(2) | |
503 | swpx_interface = "neighbor %s interface" % swpx | |
504 | elif re_swpx_int_v6only_peergroup: | |
505 | swpx = re_swpx_int_v6only_peergroup.group(1) | |
506 | peergroup = re_swpx_int_v6only_peergroup.group(2) | |
507 | swpx_interface = "neighbor %s interface v6only" % swpx | |
9fe88bc7 | 508 | |
9b166171 | 509 | swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup) |
9fe88bc7 DW |
510 | found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) |
511 | found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup) | |
b1e0634c | 512 | tmp_ctx_keys = tuple(list(ctx_keys)) |
9b166171 DW |
513 | |
514 | if not found_add_swpx_peergroup: | |
515 | tmp_ctx_keys = list(ctx_keys) | |
516 | tmp_ctx_keys.append('address-family ipv4 unicast') | |
517 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
518 | found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) | |
519 | ||
520 | if not found_add_swpx_peergroup: | |
521 | tmp_ctx_keys = list(ctx_keys) | |
522 | tmp_ctx_keys.append('address-family ipv6 unicast') | |
523 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
524 | found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) | |
9fe88bc7 DW |
525 | |
526 | if found_add_swpx_interface and found_add_swpx_peergroup: | |
9b166171 | 527 | deleted = True |
9fe88bc7 DW |
528 | lines_to_del_to_del.append((ctx_keys, line)) |
529 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
9b166171 DW |
530 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup)) |
531 | ||
b3a39dc5 DD |
532 | """ |
533 | In 3.0.1 we changed how we display neighbor interface command. Older | |
534 | versions of quagga would display the following: | |
535 | neighbor swp1 interface | |
536 | neighbor swp1 remote-as external | |
537 | neighbor swp1 capability extended-nexthop | |
538 | ||
539 | but today we display via a single line | |
540 | neighbor swp1 interface remote-as external | |
541 | ||
542 | and capability extended-nexthop is no longer needed because we | |
543 | automatically enable it when the neighbor is of type interface. | |
544 | ||
545 | This change confuses quagga-reload.py so check to see if we are deleting | |
546 | neighbor swp1 interface remote-as (external|internal|ASNUM) | |
547 | ||
548 | and adding | |
549 | neighbor swp1 interface | |
550 | neighbor swp1 remote-as (external|internal|ASNUM) | |
551 | neighbor swp1 capability extended-nexthop | |
552 | ||
553 | If so then chop the del line and the corresponding add lines | |
554 | """ | |
555 | re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line) | |
556 | re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line) | |
557 | ||
558 | if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas: | |
559 | swpx_interface = None | |
560 | swpx_remoteas = None | |
561 | ||
562 | if re_swpx_int_remoteas: | |
563 | swpx = re_swpx_int_remoteas.group(1) | |
564 | remoteas = re_swpx_int_remoteas.group(2) | |
565 | swpx_interface = "neighbor %s interface" % swpx | |
566 | elif re_swpx_int_v6only_remoteas: | |
567 | swpx = re_swpx_int_v6only_remoteas.group(1) | |
568 | remoteas = re_swpx_int_v6only_remoteas.group(2) | |
569 | swpx_interface = "neighbor %s interface v6only" % swpx | |
570 | ||
571 | swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas) | |
572 | found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) | |
573 | found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas) | |
574 | tmp_ctx_keys = tuple(list(ctx_keys)) | |
575 | ||
576 | if found_add_swpx_interface and found_add_swpx_remoteas: | |
577 | deleted = True | |
578 | lines_to_del_to_del.append((ctx_keys, line)) | |
579 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
580 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas)) | |
581 | ||
9b166171 DW |
582 | if not deleted: |
583 | found_add_line = line_exist(lines_to_add, ctx_keys, line) | |
584 | ||
585 | if found_add_line: | |
586 | lines_to_del_to_del.append((ctx_keys, line)) | |
587 | lines_to_add_to_del.append((ctx_keys, line)) | |
588 | else: | |
589 | ''' | |
590 | We have commands that used to be displayed in the global part | |
591 | of 'router bgp' that are now displayed under 'address-family ipv4 unicast' | |
592 | ||
593 | # old way | |
594 | router bgp 64900 | |
595 | neighbor ISL advertisement-interval 0 | |
596 | ||
597 | vs. | |
598 | ||
599 | # new way | |
600 | router bgp 64900 | |
601 | address-family ipv4 unicast | |
602 | neighbor ISL advertisement-interval 0 | |
603 | ||
604 | Look to see if we are deleting it in one format just to add it back in the other | |
605 | ''' | |
606 | if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast': | |
607 | tmp_ctx_keys = list(ctx_keys)[:-1] | |
608 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
609 | ||
610 | found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line) | |
611 | ||
612 | if found_add_line: | |
613 | lines_to_del_to_del.append((ctx_keys, line)) | |
614 | lines_to_add_to_del.append((tmp_ctx_keys, line)) | |
9fe88bc7 DW |
615 | |
616 | for (ctx_keys, line) in lines_to_del_to_del: | |
617 | lines_to_del.remove((ctx_keys, line)) | |
618 | ||
619 | for (ctx_keys, line) in lines_to_add_to_del: | |
620 | lines_to_add.remove((ctx_keys, line)) | |
621 | ||
622 | return (lines_to_add, lines_to_del) | |
623 | ||
624 | ||
2fc76430 DS |
625 | def compare_context_objects(newconf, running): |
626 | """ | |
627 | Create a context diff for the two specified contexts | |
628 | """ | |
629 | ||
630 | # Compare the two Config objects to find the lines that we need to add/del | |
631 | lines_to_add = [] | |
632 | lines_to_del = [] | |
926ea62e | 633 | delete_bgpd = False |
2fc76430 DS |
634 | |
635 | # Find contexts that are in newconf but not in running | |
636 | # Find contexts that are in running but not in newconf | |
637 | for (running_ctx_keys, running_ctx) in running.contexts.iteritems(): | |
638 | ||
639 | if running_ctx_keys not in newconf.contexts: | |
640 | ||
ab5f8310 DW |
641 | # We check that the len is 1 here so that we only look at ('router bgp 10') |
642 | # and not ('router bgp 10', 'address-family ipv4 unicast'). The | |
926ea62e | 643 | # latter could cause a false delete_bgpd positive if ipv4 unicast is in |
ab5f8310 DW |
644 | # running but not in newconf. |
645 | if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1: | |
926ea62e DW |
646 | delete_bgpd = True |
647 | lines_to_del.append((running_ctx_keys, None)) | |
648 | ||
649 | # If this is an address-family under 'router bgp' and we are already deleting the | |
650 | # entire 'router bgp' context then ignore this sub-context | |
651 | elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd: | |
76f69d1c | 652 | continue |
514665b9 | 653 | |
2fc76430 | 654 | # Non-global context |
926ea62e | 655 | elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys): |
2fc76430 DS |
656 | lines_to_del.append((running_ctx_keys, None)) |
657 | ||
658 | # Global context | |
659 | else: | |
660 | for line in running_ctx.lines: | |
661 | lines_to_del.append((running_ctx_keys, line)) | |
662 | ||
663 | # Find the lines within each context to add | |
664 | # Find the lines within each context to del | |
665 | for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems(): | |
666 | ||
667 | if newconf_ctx_keys in running.contexts: | |
668 | running_ctx = running.contexts[newconf_ctx_keys] | |
669 | ||
670 | for line in newconf_ctx.lines: | |
671 | if line not in running_ctx.dlines: | |
672 | lines_to_add.append((newconf_ctx_keys, line)) | |
673 | ||
674 | for line in running_ctx.lines: | |
675 | if line not in newconf_ctx.dlines: | |
676 | lines_to_del.append((newconf_ctx_keys, line)) | |
677 | ||
678 | for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems(): | |
679 | ||
680 | if newconf_ctx_keys not in running.contexts: | |
681 | lines_to_add.append((newconf_ctx_keys, None)) | |
682 | ||
683 | for line in newconf_ctx.lines: | |
684 | lines_to_add.append((newconf_ctx_keys, line)) | |
685 | ||
9b166171 | 686 | (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del) |
9fe88bc7 | 687 | |
926ea62e | 688 | return (lines_to_add, lines_to_del) |
2fc76430 DS |
689 | |
690 | if __name__ == '__main__': | |
691 | # Command line options | |
692 | parser = argparse.ArgumentParser(description='Dynamically apply diff in quagga configs') | |
693 | parser.add_argument('--input', help='Read running config from file instead of "show running"') | |
694 | group = parser.add_mutually_exclusive_group(required=True) | |
695 | group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False) | |
696 | group.add_argument('--test', action='store_true', help='Show the deltas', default=False) | |
697 | parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False) | |
cc146ecc | 698 | parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False) |
2fc76430 DS |
699 | parser.add_argument('filename', help='Location of new quagga config file') |
700 | args = parser.parse_args() | |
701 | ||
702 | # Logging | |
703 | # For --test log to stdout | |
704 | # For --reload log to /var/log/quagga/quagga-reload.log | |
cc146ecc | 705 | if args.test or args.stdout: |
c50aceee | 706 | logging.basicConfig(level=logging.INFO, |
2fc76430 | 707 | format='%(asctime)s %(levelname)5s: %(message)s') |
926ea62e DW |
708 | |
709 | # Color the errors and warnings in red | |
710 | logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR)) | |
711 | logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING)) | |
712 | ||
2fc76430 DS |
713 | elif args.reload: |
714 | if not os.path.isdir('/var/log/quagga/'): | |
715 | os.makedirs('/var/log/quagga/') | |
716 | ||
717 | logging.basicConfig(filename='/var/log/quagga/quagga-reload.log', | |
c50aceee | 718 | level=logging.INFO, |
2fc76430 DS |
719 | format='%(asctime)s %(levelname)5s: %(message)s') |
720 | ||
721 | # argparse should prevent this from happening but just to be safe... | |
722 | else: | |
723 | raise Exception('Must specify --reload or --test') | |
a782e613 | 724 | log = logging.getLogger(__name__) |
2fc76430 | 725 | |
76f69d1c DW |
726 | # Verify the new config file is valid |
727 | if not os.path.isfile(args.filename): | |
728 | print "Filename %s does not exist" % args.filename | |
729 | sys.exit(1) | |
730 | ||
731 | if not os.path.getsize(args.filename): | |
732 | print "Filename %s is an empty file" % args.filename | |
733 | sys.exit(1) | |
734 | ||
76f69d1c DW |
735 | # Verify that 'service integrated-vtysh-config' is configured |
736 | vtysh_filename = '/etc/quagga/vtysh.conf' | |
6ac9179c | 737 | service_integrated_vtysh_config = True |
76f69d1c | 738 | |
f850d14d DW |
739 | if os.path.isfile(vtysh_filename): |
740 | with open(vtysh_filename, 'r') as fh: | |
741 | for line in fh.readlines(): | |
742 | line = line.strip() | |
76f69d1c | 743 | |
6ac9179c DD |
744 | if line == 'no service integrated-vtysh-config': |
745 | service_integrated_vtysh_config = False | |
f850d14d | 746 | break |
76f69d1c DW |
747 | |
748 | if not service_integrated_vtysh_config: | |
749 | print "'service integrated-vtysh-config' is not configured, this is required for 'service quagga reload'" | |
750 | sys.exit(1) | |
2fc76430 | 751 | |
c50aceee | 752 | if args.debug: |
a782e613 | 753 | log.setLevel(logging.DEBUG) |
c50aceee | 754 | |
a782e613 | 755 | log.info('Called via "%s"', str(args)) |
c50aceee | 756 | |
2fc76430 DS |
757 | # Create a Config object from the config generated by newconf |
758 | newconf = Config() | |
759 | newconf.load_from_file(args.filename) | |
2fc76430 DS |
760 | |
761 | if args.test: | |
762 | ||
763 | # Create a Config object from the running config | |
764 | running = Config() | |
765 | ||
766 | if args.input: | |
767 | running.load_from_file(args.input) | |
768 | else: | |
769 | running.load_from_show_running() | |
770 | ||
926ea62e | 771 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
4a2587c6 | 772 | lines_to_configure = [] |
2fc76430 DS |
773 | |
774 | if lines_to_del: | |
775 | print "\nLines To Delete" | |
776 | print "===============" | |
777 | ||
778 | for (ctx_keys, line) in lines_to_del: | |
779 | ||
780 | if line == '!': | |
781 | continue | |
782 | ||
4a2587c6 DW |
783 | cmd = line_for_vtysh_file(ctx_keys, line, True) |
784 | lines_to_configure.append(cmd) | |
9fe88bc7 | 785 | print cmd |
2fc76430 DS |
786 | |
787 | if lines_to_add: | |
788 | print "\nLines To Add" | |
789 | print "============" | |
790 | ||
791 | for (ctx_keys, line) in lines_to_add: | |
792 | ||
793 | if line == '!': | |
794 | continue | |
795 | ||
4a2587c6 DW |
796 | cmd = line_for_vtysh_file(ctx_keys, line, False) |
797 | lines_to_configure.append(cmd) | |
9fe88bc7 | 798 | print cmd |
2fc76430 | 799 | |
2fc76430 DS |
800 | elif args.reload: |
801 | ||
a782e613 | 802 | log.debug('New Quagga Config\n%s', newconf.get_lines()) |
2fc76430 DS |
803 | |
804 | # This looks a little odd but we have to do this twice...here is why | |
805 | # If the user had this running bgp config: | |
4a2587c6 | 806 | # |
2fc76430 DS |
807 | # router bgp 10 |
808 | # neighbor 1.1.1.1 remote-as 50 | |
809 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 | 810 | # |
2fc76430 | 811 | # and this config in the newconf config file |
4a2587c6 | 812 | # |
2fc76430 DS |
813 | # router bgp 10 |
814 | # neighbor 1.1.1.1 remote-as 999 | |
815 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 DW |
816 | # |
817 | # | |
2fc76430 DS |
818 | # Then the script will do |
819 | # - no neighbor 1.1.1.1 remote-as 50 | |
820 | # - neighbor 1.1.1.1 remote-as 999 | |
4a2587c6 | 821 | # |
2fc76430 DS |
822 | # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove |
823 | # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the | |
824 | # configs again to put this line back. | |
825 | ||
826 | for x in range(2): | |
827 | running = Config() | |
828 | running.load_from_show_running() | |
a782e613 | 829 | log.debug('Running Quagga Config (Pass #%d)\n%s', x, running.get_lines()) |
2fc76430 | 830 | |
926ea62e | 831 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
2fc76430 DS |
832 | |
833 | if lines_to_del: | |
834 | for (ctx_keys, line) in lines_to_del: | |
835 | ||
836 | if line == '!': | |
837 | continue | |
838 | ||
4a2587c6 DW |
839 | # 'no' commands are tricky, we can't just put them in a file and |
840 | # vtysh -f that file. See the next comment for an explanation | |
841 | # of their quirks | |
2fc76430 DS |
842 | cmd = line_to_vtysh_conft(ctx_keys, line, True) |
843 | original_cmd = cmd | |
844 | ||
76f69d1c DW |
845 | # Some commands in quagga are picky about taking a "no" of the entire line. |
846 | # OSPF is bad about this, you can't "no" the entire line, you have to "no" | |
847 | # only the beginning. If we hit one of these command an exception will be | |
848 | # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again. | |
4a2587c6 | 849 | # |
76f69d1c | 850 | # Example: |
4a2587c6 DW |
851 | # quagga(config-if)# ip ospf authentication message-digest 1.1.1.1 |
852 | # quagga(config-if)# no ip ospf authentication message-digest 1.1.1.1 | |
76f69d1c | 853 | # % Unknown command. |
4a2587c6 | 854 | # quagga(config-if)# no ip ospf authentication message-digest |
76f69d1c | 855 | # % Unknown command. |
4a2587c6 DW |
856 | # quagga(config-if)# no ip ospf authentication |
857 | # quagga(config-if)# | |
2fc76430 DS |
858 | |
859 | while True: | |
2fc76430 DS |
860 | try: |
861 | _ = subprocess.check_output(cmd) | |
862 | ||
863 | except subprocess.CalledProcessError: | |
864 | ||
865 | # - Pull the last entry from cmd (this would be | |
866 | # 'no ip ospf authentication message-digest 1.1.1.1' in | |
867 | # our example above | |
868 | # - Split that last entry by whitespace and drop the last word | |
a782e613 | 869 | log.warning('Failed to execute %s', ' '.join(cmd)) |
2fc76430 DS |
870 | last_arg = cmd[-1].split(' ') |
871 | ||
872 | if len(last_arg) <= 2: | |
a782e613 | 873 | log.error('"%s" we failed to remove this command', original_cmd) |
2fc76430 DS |
874 | break |
875 | ||
876 | new_last_arg = last_arg[0:-1] | |
877 | cmd[-1] = ' '.join(new_last_arg) | |
878 | else: | |
a782e613 | 879 | log.info('Executed "%s"', ' '.join(cmd)) |
2fc76430 DS |
880 | break |
881 | ||
2fc76430 | 882 | if lines_to_add: |
4a2587c6 DW |
883 | lines_to_configure = [] |
884 | ||
2fc76430 DS |
885 | for (ctx_keys, line) in lines_to_add: |
886 | ||
887 | if line == '!': | |
888 | continue | |
889 | ||
4a2587c6 DW |
890 | cmd = line_for_vtysh_file(ctx_keys, line, False) |
891 | lines_to_configure.append(cmd) | |
892 | ||
893 | if lines_to_configure: | |
894 | random_string = ''.join(random.SystemRandom().choice( | |
895 | string.ascii_uppercase + | |
896 | string.digits) for _ in range(6)) | |
897 | ||
898 | filename = "/var/run/quagga/reload-%s.txt" % random_string | |
a782e613 | 899 | log.info("%s content\n%s" % (filename, pformat(lines_to_configure))) |
4a2587c6 DW |
900 | |
901 | with open(filename, 'w') as fh: | |
902 | for line in lines_to_configure: | |
903 | fh.write(line + '\n') | |
904 | subprocess.call(['/usr/bin/vtysh', '-f', filename]) | |
905 | os.unlink(filename) | |
2fc76430 | 906 | |
926ea62e DW |
907 | # Make these changes persistent |
908 | subprocess.call(['/usr/bin/vtysh', '-c', 'write']) |