]>
Commit | Line | Data |
---|---|---|
2fc76430 | 1 | #!/usr/bin/python |
d8e4c438 | 2 | # Frr Reloader |
50e24903 DS |
3 | # Copyright (C) 2014 Cumulus Networks, Inc. |
4 | # | |
d8e4c438 | 5 | # This file is part of Frr. |
50e24903 | 6 | # |
d8e4c438 | 7 | # Frr is free software; you can redistribute it and/or modify it |
50e24903 DS |
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 | # | |
d8e4c438 | 12 | # Frr is distributed in the hope that it will be useful, but |
50e24903 DS |
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 | |
d8e4c438 | 18 | # along with Frr; see the file COPYING. If not, write to the Free |
50e24903 DS |
19 | # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA |
20 | # 02111-1307, USA. | |
21 | # | |
2fc76430 DS |
22 | """ |
23 | This program | |
d8e4c438 DS |
24 | - reads a frr configuration text file |
25 | - reads frr's current running configuration via "vtysh -c 'show running'" | |
2fc76430 | 26 | - compares the two configs and determines what commands to execute to |
d8e4c438 | 27 | synchronize frr's running configuration with the configuation in the |
2fc76430 DS |
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 | |
bb972e44 | 41 | from ipaddr import IPv6Address, IPNetwork |
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 | 54 | """ |
d8e4c438 | 55 | A Context object represents a section of frr configuration such as: |
2fc76430 DS |
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 | 93 | """ |
d8e4c438 | 94 | A frr configuration is stored in a Config object. A Config object |
2fc76430 DS |
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: | |
ec3fd957 DW |
112 | file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename], |
113 | stderr=subprocess.STDOUT) | |
2fc76430 | 114 | except subprocess.CalledProcessError as e: |
ec3fd957 DW |
115 | ve = VtyshMarkException(e) |
116 | ve.output = e.output | |
117 | raise ve | |
2fc76430 DS |
118 | |
119 | for line in file_output.split('\n'): | |
120 | line = line.strip() | |
121 | if ":" in line: | |
122 | qv6_line = get_normalized_ipv6_line(line) | |
123 | self.lines.append(qv6_line) | |
124 | else: | |
125 | self.lines.append(line) | |
126 | ||
127 | self.load_contexts() | |
128 | ||
129 | def load_from_show_running(self): | |
130 | """ | |
131 | Read running configuration and slurp it into internal memory | |
132 | The internal representation has been marked appropriately by passing it | |
133 | through vtysh with the -m parameter | |
134 | """ | |
a782e613 | 135 | log.info('Loading Config object from vtysh show running') |
2fc76430 DS |
136 | |
137 | try: | |
4a2587c6 DW |
138 | config_text = subprocess.check_output( |
139 | "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -", | |
ec3fd957 | 140 | shell=True, stderr=subprocess.STDOUT) |
2fc76430 | 141 | except subprocess.CalledProcessError as e: |
ec3fd957 DW |
142 | ve = VtyshMarkException(e) |
143 | ve.output = e.output | |
144 | raise ve | |
2fc76430 | 145 | |
2fc76430 DS |
146 | for line in config_text.split('\n'): |
147 | line = line.strip() | |
148 | ||
149 | if (line == 'Building configuration...' or | |
150 | line == 'Current configuration:' or | |
4a2587c6 | 151 | not line): |
2fc76430 DS |
152 | continue |
153 | ||
154 | self.lines.append(line) | |
155 | ||
156 | self.load_contexts() | |
157 | ||
158 | def get_lines(self): | |
159 | """ | |
160 | Return the lines read in from the configuration | |
161 | """ | |
162 | ||
163 | return '\n'.join(self.lines) | |
164 | ||
165 | def get_contexts(self): | |
166 | """ | |
167 | Return the parsed context as strings for display, log etc. | |
168 | """ | |
169 | ||
170 | for (_, ctx) in sorted(self.contexts.iteritems()): | |
171 | print str(ctx) + '\n' | |
172 | ||
173 | def save_contexts(self, key, lines): | |
174 | """ | |
175 | Save the provided key and lines as a context | |
176 | """ | |
177 | ||
178 | if not key: | |
179 | return | |
180 | ||
bb972e44 DD |
181 | ''' |
182 | IP addresses specified in "network" statements, "ip prefix-lists" | |
183 | etc. can differ in the host part of the specification the user | |
184 | provides and what the running config displays. For example, user | |
185 | can specify 11.1.1.1/24, and the running config displays this as | |
186 | 11.1.1.0/24. Ensure we don't do a needless operation for such | |
187 | lines. IS-IS & OSPFv3 have no "network" support. | |
188 | ''' | |
4d760f42 | 189 | re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0]) |
bb972e44 DD |
190 | if re_key_rt: |
191 | addr = re_key_rt.group(2) | |
192 | if '/' in addr: | |
193 | try: | |
194 | newaddr = IPNetwork(addr) | |
4d760f42 DD |
195 | key[0] = '%s route %s/%s%s' % (re_key_rt.group(1), |
196 | newaddr.network, | |
197 | newaddr.prefixlen, | |
198 | re_key_rt.group(3)) | |
bb972e44 DD |
199 | except ValueError: |
200 | pass | |
201 | ||
202 | re_key_rt = re.match( | |
203 | r'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$', | |
204 | key[0] | |
205 | ) | |
206 | if re_key_rt: | |
207 | addr = re_key_rt.group(4) | |
208 | if '/' in addr: | |
209 | try: | |
210 | newaddr = '%s/%s' % (IPNetwork(addr).network, | |
211 | IPNetwork(addr).prefixlen) | |
212 | except ValueError: | |
213 | newaddr = addr | |
0845b872 DD |
214 | else: |
215 | newaddr = addr | |
bb972e44 DD |
216 | |
217 | legestr = re_key_rt.group(5) | |
218 | re_lege = re.search(r'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr) | |
219 | if re_lege: | |
220 | legestr = '%sge %s le %s%s' % (re_lege.group(1), | |
221 | re_lege.group(3), | |
222 | re_lege.group(2), | |
223 | re_lege.group(4)) | |
224 | re_lege = re.search(r'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr) | |
225 | ||
226 | if (re_lege and ((re_key_rt.group(1) == "ip" and | |
227 | re_lege.group(3) == "32") or | |
228 | (re_key_rt.group(1) == "ipv6" and | |
229 | re_lege.group(3) == "128"))): | |
230 | legestr = '%sge %s%s' % (re_lege.group(1), | |
231 | re_lege.group(2), | |
232 | re_lege.group(4)) | |
233 | ||
234 | key[0] = '%s prefix-list%s%s %s%s' % (re_key_rt.group(1), | |
235 | re_key_rt.group(2), | |
236 | re_key_rt.group(3), | |
237 | newaddr, | |
238 | legestr) | |
239 | ||
240 | if lines and key[0].startswith('router bgp'): | |
241 | newlines = [] | |
242 | for line in lines: | |
243 | re_net = re.match(r'network\s+([A-Fa-f:.0-9/]+)(.*)$', line) | |
244 | if re_net: | |
245 | addr = re_net.group(1) | |
246 | if '/' not in addr and key[0].startswith('router bgp'): | |
247 | # This is most likely an error because with no | |
248 | # prefixlen, BGP treats the prefixlen as 8 | |
249 | addr = addr + '/8' | |
250 | ||
251 | try: | |
252 | newaddr = IPNetwork(addr) | |
253 | line = 'network %s/%s %s' % (newaddr.network, | |
254 | newaddr.prefixlen, | |
255 | re_net.group(2)) | |
256 | newlines.append(line) | |
0845b872 | 257 | except ValueError: |
bb972e44 DD |
258 | # Really this should be an error. Whats a network |
259 | # without an IP Address following it ? | |
260 | newlines.append(line) | |
261 | else: | |
262 | newlines.append(line) | |
263 | lines = newlines | |
264 | ||
265 | ''' | |
266 | More fixups in user specification and what running config shows. | |
267 | "null0" in routes must be replaced by Null0, and "blackhole" must | |
268 | be replaced by Null0 as well. | |
269 | ''' | |
270 | if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and | |
271 | 'null0' in key[0] or 'blackhole' in key[0]): | |
272 | key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0]) | |
273 | key[0] = re.sub(r'\s+blackhole(\s*$)', ' Null0', key[0]) | |
274 | ||
2fc76430 DS |
275 | if lines: |
276 | if tuple(key) not in self.contexts: | |
277 | ctx = Context(tuple(key), lines) | |
278 | self.contexts[tuple(key)] = ctx | |
279 | else: | |
280 | ctx = self.contexts[tuple(key)] | |
281 | ctx.add_lines(lines) | |
282 | ||
283 | else: | |
284 | if tuple(key) not in self.contexts: | |
285 | ctx = Context(tuple(key), []) | |
286 | self.contexts[tuple(key)] = ctx | |
287 | ||
288 | def load_contexts(self): | |
289 | """ | |
290 | Parse the configuration and create contexts for each appropriate block | |
291 | """ | |
292 | ||
293 | current_context_lines = [] | |
294 | ctx_keys = [] | |
295 | ||
296 | ''' | |
297 | The end of a context is flagged via the 'end' keyword: | |
298 | ||
299 | ! | |
300 | interface swp52 | |
301 | ipv6 nd suppress-ra | |
302 | link-detect | |
303 | ! | |
304 | end | |
305 | router bgp 10 | |
306 | bgp router-id 10.0.0.1 | |
307 | bgp log-neighbor-changes | |
308 | no bgp default ipv4-unicast | |
309 | neighbor EBGP peer-group | |
310 | neighbor EBGP advertisement-interval 1 | |
311 | neighbor EBGP timers connect 10 | |
312 | neighbor 2001:40:1:4::6 remote-as 40 | |
313 | neighbor 2001:40:1:8::a remote-as 40 | |
314 | ! | |
315 | end | |
316 | address-family ipv6 | |
317 | neighbor IBGPv6 activate | |
318 | neighbor 2001:10::2 peer-group IBGPv6 | |
319 | neighbor 2001:10::3 peer-group IBGPv6 | |
320 | exit-address-family | |
321 | ! | |
7918b335 DW |
322 | end |
323 | address-family evpn | |
324 | neighbor LEAF activate | |
325 | advertise-all-vni | |
326 | vni 10100 | |
327 | rd 65000:10100 | |
328 | route-target import 10.1.1.1:10100 | |
329 | route-target export 10.1.1.1:10100 | |
330 | exit-vni | |
331 | exit-address-family | |
332 | ! | |
2fc76430 DS |
333 | end |
334 | router ospf | |
335 | ospf router-id 10.0.0.1 | |
336 | log-adjacency-changes detail | |
337 | timers throttle spf 0 50 5000 | |
338 | ! | |
339 | end | |
340 | ''' | |
341 | ||
342 | # The code assumes that its working on the output from the "vtysh -m" | |
343 | # command. That provides the appropriate markers to signify end of | |
344 | # a context. This routine uses that to build the contexts for the | |
345 | # config. | |
346 | # | |
347 | # There are single line contexts such as "log file /media/node/zebra.log" | |
348 | # and multi-line contexts such as "router ospf" and subcontexts | |
349 | # within a context such as "address-family" within "router bgp" | |
350 | # In each of these cases, the first line of the context becomes the | |
351 | # key of the context. So "router bgp 10" is the key for the non-address | |
352 | # family part of bgp, "router bgp 10, address-family ipv6 unicast" is | |
353 | # the key for the subcontext and so on. | |
2fc76430 DS |
354 | ctx_keys = [] |
355 | main_ctx_key = [] | |
356 | new_ctx = True | |
2fc76430 DS |
357 | |
358 | # the keywords that we know are single line contexts. bgp in this case | |
359 | # is not the main router bgp block, but enabling multi-instance | |
2fed5dcd | 360 | oneline_ctx_keywords = ("access-list ", |
e80c8c55 | 361 | "agentx", |
2fed5dcd DW |
362 | "bgp ", |
363 | "debug ", | |
364 | "dump ", | |
365 | "enable ", | |
825be4c2 | 366 | "frr ", |
2fed5dcd DW |
367 | "hostname ", |
368 | "ip ", | |
369 | "ipv6 ", | |
370 | "log ", | |
45cb21bf | 371 | "mpls", |
a11209a7 | 372 | "no ", |
2fed5dcd DW |
373 | "password ", |
374 | "ptm-enable", | |
375 | "router-id ", | |
376 | "service ", | |
377 | "table ", | |
378 | "username ", | |
379 | "zebra ") | |
380 | ||
2fc76430 DS |
381 | for line in self.lines: |
382 | ||
383 | if not line: | |
384 | continue | |
385 | ||
386 | if line.startswith('!') or line.startswith('#'): | |
387 | continue | |
388 | ||
389 | # one line contexts | |
390 | if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords): | |
391 | self.save_contexts(ctx_keys, current_context_lines) | |
392 | ||
393 | # Start a new context | |
394 | main_ctx_key = [] | |
4a2587c6 | 395 | ctx_keys = [line, ] |
2fc76430 DS |
396 | current_context_lines = [] |
397 | ||
a782e613 | 398 | log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
2fc76430 DS |
399 | self.save_contexts(ctx_keys, current_context_lines) |
400 | new_ctx = True | |
401 | ||
402 | elif line == "end": | |
403 | self.save_contexts(ctx_keys, current_context_lines) | |
a782e613 | 404 | log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys) |
2fc76430 DS |
405 | |
406 | # Start a new context | |
407 | new_ctx = True | |
408 | main_ctx_key = [] | |
409 | ctx_keys = [] | |
410 | current_context_lines = [] | |
411 | ||
7918b335 | 412 | elif line == "exit-address-family" or line == "exit" or line == "exit-vni": |
2fc76430 DS |
413 | # if this exit is for address-family ipv4 unicast, ignore the pop |
414 | if main_ctx_key: | |
415 | self.save_contexts(ctx_keys, current_context_lines) | |
416 | ||
417 | # Start a new context | |
418 | ctx_keys = copy.deepcopy(main_ctx_key) | |
419 | current_context_lines = [] | |
a782e613 | 420 | log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys) |
2fc76430 DS |
421 | |
422 | elif new_ctx is True: | |
423 | if not main_ctx_key: | |
4a2587c6 | 424 | ctx_keys = [line, ] |
2fc76430 DS |
425 | else: |
426 | ctx_keys = copy.deepcopy(main_ctx_key) | |
427 | main_ctx_key = [] | |
428 | ||
429 | current_context_lines = [] | |
430 | new_ctx = False | |
a782e613 | 431 | log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
2fc76430 | 432 | |
7918b335 DW |
433 | elif "vni " in line: |
434 | main_ctx_key = [] | |
435 | ||
436 | # Save old context first | |
437 | self.save_contexts(ctx_keys, current_context_lines) | |
438 | current_context_lines = [] | |
439 | main_ctx_key = copy.deepcopy(ctx_keys) | |
440 | log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line) | |
441 | ||
442 | ctx_keys.append(line) | |
443 | ||
2fc76430 DS |
444 | elif "address-family " in line: |
445 | main_ctx_key = [] | |
446 | ||
0b960b4d DW |
447 | # Save old context first |
448 | self.save_contexts(ctx_keys, current_context_lines) | |
449 | current_context_lines = [] | |
450 | main_ctx_key = copy.deepcopy(ctx_keys) | |
a782e613 | 451 | log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line) |
2fc76430 | 452 | |
0b960b4d DW |
453 | if line == "address-family ipv6": |
454 | ctx_keys.append("address-family ipv6 unicast") | |
455 | elif line == "address-family ipv4": | |
456 | ctx_keys.append("address-family ipv4 unicast") | |
5014d96f DW |
457 | elif line == "address-family evpn": |
458 | ctx_keys.append("address-family l2vpn evpn") | |
0b960b4d DW |
459 | else: |
460 | ctx_keys.append(line) | |
2fc76430 DS |
461 | |
462 | else: | |
463 | # Continuing in an existing context, add non-commented lines to it | |
464 | current_context_lines.append(line) | |
a782e613 | 465 | log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys) |
2fc76430 DS |
466 | |
467 | # Save the context of the last one | |
468 | self.save_contexts(ctx_keys, current_context_lines) | |
469 | ||
4a2587c6 | 470 | |
2fc76430 DS |
471 | def line_to_vtysh_conft(ctx_keys, line, delete): |
472 | """ | |
4a2587c6 | 473 | Return the vtysh command for the specified context line |
2fc76430 DS |
474 | """ |
475 | ||
476 | cmd = [] | |
477 | cmd.append('vtysh') | |
478 | cmd.append('-c') | |
479 | cmd.append('conf t') | |
480 | ||
481 | if line: | |
482 | for ctx_key in ctx_keys: | |
483 | cmd.append('-c') | |
484 | cmd.append(ctx_key) | |
485 | ||
486 | line = line.lstrip() | |
487 | ||
488 | if delete: | |
489 | cmd.append('-c') | |
490 | ||
491 | if line.startswith('no '): | |
492 | cmd.append('%s' % line[3:]) | |
493 | else: | |
494 | cmd.append('no %s' % line) | |
495 | ||
496 | else: | |
497 | cmd.append('-c') | |
498 | cmd.append(line) | |
499 | ||
500 | # If line is None then we are typically deleting an entire | |
501 | # context ('no router ospf' for example) | |
502 | else: | |
503 | ||
504 | if delete: | |
505 | ||
506 | # Only put the 'no' on the last sub-context | |
507 | for ctx_key in ctx_keys: | |
508 | cmd.append('-c') | |
509 | ||
510 | if ctx_key == ctx_keys[-1]: | |
511 | cmd.append('no %s' % ctx_key) | |
512 | else: | |
513 | cmd.append('%s' % ctx_key) | |
514 | else: | |
515 | for ctx_key in ctx_keys: | |
516 | cmd.append('-c') | |
517 | cmd.append(ctx_key) | |
518 | ||
519 | return cmd | |
520 | ||
4a2587c6 DW |
521 | |
522 | def line_for_vtysh_file(ctx_keys, line, delete): | |
523 | """ | |
e20dc2ba | 524 | Return the command as it would appear in frr.conf |
4a2587c6 DW |
525 | """ |
526 | cmd = [] | |
527 | ||
528 | if line: | |
529 | for (i, ctx_key) in enumerate(ctx_keys): | |
530 | cmd.append(' ' * i + ctx_key) | |
531 | ||
532 | line = line.lstrip() | |
533 | indent = len(ctx_keys) * ' ' | |
534 | ||
535 | if delete: | |
536 | if line.startswith('no '): | |
537 | cmd.append('%s%s' % (indent, line[3:])) | |
538 | else: | |
539 | cmd.append('%sno %s' % (indent, line)) | |
540 | ||
541 | else: | |
542 | cmd.append(indent + line) | |
543 | ||
544 | # If line is None then we are typically deleting an entire | |
545 | # context ('no router ospf' for example) | |
546 | else: | |
547 | if delete: | |
548 | ||
549 | # Only put the 'no' on the last sub-context | |
550 | for ctx_key in ctx_keys: | |
551 | ||
552 | if ctx_key == ctx_keys[-1]: | |
553 | cmd.append('no %s' % ctx_key) | |
554 | else: | |
555 | cmd.append('%s' % ctx_key) | |
556 | else: | |
557 | for ctx_key in ctx_keys: | |
558 | cmd.append(ctx_key) | |
559 | ||
8ad1fe6c DW |
560 | cmd = '\n' + '\n'.join(cmd) |
561 | ||
562 | # There are some commands that are on by default so their "no" form will be | |
563 | # displayed in the config. "no bgp default ipv4-unicast" is one of these. | |
564 | # If we need to remove this line we do so by adding "bgp default ipv4-unicast", | |
565 | # not by doing a "no no bgp default ipv4-unicast" | |
566 | cmd = cmd.replace('no no ', '') | |
567 | ||
568 | return cmd | |
4a2587c6 DW |
569 | |
570 | ||
2fc76430 DS |
571 | def get_normalized_ipv6_line(line): |
572 | """ | |
d8e4c438 | 573 | Return a normalized IPv6 line as produced by frr, |
2fc76430 | 574 | with all letters in lower case and trailing and leading |
bb972e44 DD |
575 | zeros removed, and only the network portion present if |
576 | the IPv6 word is a network | |
2fc76430 DS |
577 | """ |
578 | norm_line = "" | |
579 | words = line.split(' ') | |
580 | for word in words: | |
581 | if ":" in word: | |
bb972e44 DD |
582 | norm_word = None |
583 | if "/" in word: | |
584 | try: | |
585 | v6word = IPNetwork(word) | |
586 | norm_word = '%s/%s' % (v6word.network, v6word.prefixlen) | |
587 | except ValueError: | |
588 | pass | |
589 | if not norm_word: | |
590 | try: | |
591 | norm_word = '%s' % IPv6Address(word) | |
0845b872 | 592 | except ValueError: |
bb972e44 | 593 | norm_word = word |
2fc76430 DS |
594 | else: |
595 | norm_word = word | |
596 | norm_line = norm_line + " " + norm_word | |
597 | ||
598 | return norm_line.strip() | |
599 | ||
4a2587c6 | 600 | |
9fe88bc7 DW |
601 | def line_exist(lines, target_ctx_keys, target_line): |
602 | for (ctx_keys, line) in lines: | |
603 | if ctx_keys == target_ctx_keys and line == target_line: | |
604 | return True | |
605 | return False | |
606 | ||
607 | ||
9b166171 | 608 | def ignore_delete_re_add_lines(lines_to_add, lines_to_del): |
9fe88bc7 DW |
609 | |
610 | # Quite possibly the most confusing (while accurate) variable names in history | |
611 | lines_to_add_to_del = [] | |
612 | lines_to_del_to_del = [] | |
613 | ||
614 | for (ctx_keys, line) in lines_to_del: | |
9b166171 DW |
615 | deleted = False |
616 | ||
926ea62e | 617 | if ctx_keys[0].startswith('router bgp') and line and line.startswith('neighbor '): |
9b166171 DW |
618 | """ |
619 | BGP changed how it displays swpX peers that are part of peer-group. Older | |
d8e4c438 | 620 | versions of frr would display these on separate lines: |
9b166171 DW |
621 | neighbor swp1 interface |
622 | neighbor swp1 peer-group FOO | |
623 | ||
624 | but today we display via a single line | |
625 | neighbor swp1 interface peer-group FOO | |
626 | ||
d8e4c438 | 627 | This change confuses frr-reload.py so check to see if we are deleting |
9b166171 DW |
628 | neighbor swp1 interface peer-group FOO |
629 | ||
630 | and adding | |
631 | neighbor swp1 interface | |
632 | neighbor swp1 peer-group FOO | |
633 | ||
634 | If so then chop the del line and the corresponding add lines | |
635 | """ | |
636 | ||
9fe88bc7 | 637 | re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line) |
9b166171 | 638 | re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line) |
9fe88bc7 | 639 | |
9b166171 DW |
640 | if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup: |
641 | swpx_interface = None | |
642 | swpx_peergroup = None | |
643 | ||
644 | if re_swpx_int_peergroup: | |
645 | swpx = re_swpx_int_peergroup.group(1) | |
646 | peergroup = re_swpx_int_peergroup.group(2) | |
647 | swpx_interface = "neighbor %s interface" % swpx | |
648 | elif re_swpx_int_v6only_peergroup: | |
649 | swpx = re_swpx_int_v6only_peergroup.group(1) | |
650 | peergroup = re_swpx_int_v6only_peergroup.group(2) | |
651 | swpx_interface = "neighbor %s interface v6only" % swpx | |
9fe88bc7 | 652 | |
9b166171 | 653 | swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup) |
9fe88bc7 DW |
654 | found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) |
655 | found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup) | |
b1e0634c | 656 | tmp_ctx_keys = tuple(list(ctx_keys)) |
9b166171 DW |
657 | |
658 | if not found_add_swpx_peergroup: | |
659 | tmp_ctx_keys = list(ctx_keys) | |
660 | tmp_ctx_keys.append('address-family ipv4 unicast') | |
661 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
662 | found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) | |
663 | ||
664 | if not found_add_swpx_peergroup: | |
665 | tmp_ctx_keys = list(ctx_keys) | |
666 | tmp_ctx_keys.append('address-family ipv6 unicast') | |
667 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
668 | found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) | |
9fe88bc7 DW |
669 | |
670 | if found_add_swpx_interface and found_add_swpx_peergroup: | |
9b166171 | 671 | deleted = True |
9fe88bc7 DW |
672 | lines_to_del_to_del.append((ctx_keys, line)) |
673 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
9b166171 DW |
674 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup)) |
675 | ||
b3a39dc5 DD |
676 | """ |
677 | In 3.0.1 we changed how we display neighbor interface command. Older | |
d8e4c438 | 678 | versions of frr would display the following: |
b3a39dc5 DD |
679 | neighbor swp1 interface |
680 | neighbor swp1 remote-as external | |
681 | neighbor swp1 capability extended-nexthop | |
682 | ||
683 | but today we display via a single line | |
684 | neighbor swp1 interface remote-as external | |
685 | ||
686 | and capability extended-nexthop is no longer needed because we | |
687 | automatically enable it when the neighbor is of type interface. | |
688 | ||
d8e4c438 | 689 | This change confuses frr-reload.py so check to see if we are deleting |
b3a39dc5 DD |
690 | neighbor swp1 interface remote-as (external|internal|ASNUM) |
691 | ||
692 | and adding | |
693 | neighbor swp1 interface | |
694 | neighbor swp1 remote-as (external|internal|ASNUM) | |
695 | neighbor swp1 capability extended-nexthop | |
696 | ||
697 | If so then chop the del line and the corresponding add lines | |
698 | """ | |
699 | re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line) | |
700 | re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line) | |
701 | ||
702 | if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas: | |
703 | swpx_interface = None | |
704 | swpx_remoteas = None | |
705 | ||
706 | if re_swpx_int_remoteas: | |
707 | swpx = re_swpx_int_remoteas.group(1) | |
708 | remoteas = re_swpx_int_remoteas.group(2) | |
709 | swpx_interface = "neighbor %s interface" % swpx | |
710 | elif re_swpx_int_v6only_remoteas: | |
711 | swpx = re_swpx_int_v6only_remoteas.group(1) | |
712 | remoteas = re_swpx_int_v6only_remoteas.group(2) | |
713 | swpx_interface = "neighbor %s interface v6only" % swpx | |
714 | ||
715 | swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas) | |
716 | found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) | |
717 | found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas) | |
718 | tmp_ctx_keys = tuple(list(ctx_keys)) | |
719 | ||
720 | if found_add_swpx_interface and found_add_swpx_remoteas: | |
721 | deleted = True | |
722 | lines_to_del_to_del.append((ctx_keys, line)) | |
723 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
724 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas)) | |
725 | ||
78e31f46 DD |
726 | ''' |
727 | In 3.0, we made bgp bestpath multipath as-relax command | |
728 | automatically assume no-as-set since the lack of this option caused | |
729 | weird routing problems and this problem was peculiar to this | |
730 | implementation. When the running config is shown in relases after | |
731 | 3.0, the no-as-set is not shown as its the default. This causes | |
732 | reload to unnecessarily unapply this option to only apply it back | |
733 | again, causing unnecessary session resets. Handle this. | |
734 | ''' | |
735 | if ctx_keys[0].startswith('router bgp') and line and 'multipath-relax' in line: | |
736 | re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line) | |
737 | old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set' | |
738 | found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd) | |
739 | ||
740 | if re_asrelax_new and found_asrelax_old: | |
741 | deleted = True | |
742 | lines_to_del_to_del.append((ctx_keys, line)) | |
743 | lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd)) | |
744 | ||
745 | ''' | |
746 | More old-to-new config handling. ip import-table no longer accepts | |
747 | distance, but we honor the old syntax. But 'show running' shows only | |
748 | the new syntax. This causes an unnecessary 'no import-table' followed | |
749 | by the same old 'ip import-table' which causes perturbations in | |
750 | announced routes leading to traffic blackholes. Fix this issue. | |
751 | ''' | |
752 | re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0]) | |
753 | if re_importtbl: | |
754 | table_num = re_importtbl.group(1) | |
755 | for ctx in lines_to_add: | |
756 | if ctx[0][0].startswith('ip import-table %s distance' % table_num): | |
78e31f46 DD |
757 | lines_to_del_to_del.append((('ip import-table %s' % table_num,), None)) |
758 | lines_to_add_to_del.append((ctx[0], None)) | |
0bf7cc28 DD |
759 | |
760 | ''' | |
761 | ip/ipv6 prefix-list can be specified without a seq number. However, | |
762 | the running config always adds 'seq x', where x is a number incremented | |
763 | by 5 for every element, to the prefix list. So, ignore such lines as | |
764 | well. Sample prefix-list lines: | |
765 | ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32 | |
766 | ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32 | |
767 | ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64 | |
768 | ''' | |
769 | re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$', | |
770 | ctx_keys[0]) | |
771 | if re_ip_pfxlst: | |
772 | tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) + | |
773 | re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) + | |
774 | re_ip_pfxlst.group(6)) | |
775 | for ctx in lines_to_add: | |
776 | if ctx[0][0] == tmpline: | |
777 | lines_to_del_to_del.append((ctx_keys, None)) | |
778 | lines_to_add_to_del.append(((tmpline,), None)) | |
779 | ||
5014d96f DW |
780 | if (len(ctx_keys) == 3 and |
781 | ctx_keys[0].startswith('router bgp') and | |
782 | ctx_keys[1] == 'address-family l2vpn evpn' and | |
783 | ctx_keys[2].startswith('vni')): | |
784 | ||
785 | re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False | |
786 | ||
787 | if re_route_target: | |
788 | rt = re_route_target.group(1).strip() | |
789 | route_target_import_line = line | |
790 | route_target_export_line = "route-target export %s" % rt | |
791 | route_target_both_line = "route-target both %s" % rt | |
792 | ||
793 | found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line) | |
794 | found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line) | |
795 | ||
796 | ''' | |
797 | If the running configs has | |
798 | route-target import 1:1 | |
799 | route-target export 1:1 | |
800 | ||
801 | and the config we are reloading against has | |
802 | route-target both 1:1 | |
803 | ||
804 | then we can ignore deleting the import/export and ignore adding the 'both' | |
805 | ''' | |
806 | if found_route_target_export_line and found_route_target_both_line: | |
807 | lines_to_del_to_del.append((ctx_keys, route_target_import_line)) | |
808 | lines_to_del_to_del.append((ctx_keys, route_target_export_line)) | |
809 | lines_to_add_to_del.append((ctx_keys, route_target_both_line)) | |
810 | ||
9b166171 DW |
811 | if not deleted: |
812 | found_add_line = line_exist(lines_to_add, ctx_keys, line) | |
813 | ||
814 | if found_add_line: | |
815 | lines_to_del_to_del.append((ctx_keys, line)) | |
816 | lines_to_add_to_del.append((ctx_keys, line)) | |
817 | else: | |
818 | ''' | |
819 | We have commands that used to be displayed in the global part | |
820 | of 'router bgp' that are now displayed under 'address-family ipv4 unicast' | |
821 | ||
822 | # old way | |
823 | router bgp 64900 | |
824 | neighbor ISL advertisement-interval 0 | |
825 | ||
826 | vs. | |
827 | ||
828 | # new way | |
829 | router bgp 64900 | |
830 | address-family ipv4 unicast | |
831 | neighbor ISL advertisement-interval 0 | |
832 | ||
833 | Look to see if we are deleting it in one format just to add it back in the other | |
834 | ''' | |
835 | if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast': | |
836 | tmp_ctx_keys = list(ctx_keys)[:-1] | |
837 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
838 | ||
839 | found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line) | |
840 | ||
841 | if found_add_line: | |
842 | lines_to_del_to_del.append((ctx_keys, line)) | |
843 | lines_to_add_to_del.append((tmp_ctx_keys, line)) | |
9fe88bc7 DW |
844 | |
845 | for (ctx_keys, line) in lines_to_del_to_del: | |
846 | lines_to_del.remove((ctx_keys, line)) | |
847 | ||
848 | for (ctx_keys, line) in lines_to_add_to_del: | |
849 | lines_to_add.remove((ctx_keys, line)) | |
850 | ||
851 | return (lines_to_add, lines_to_del) | |
852 | ||
853 | ||
2fc76430 DS |
854 | def compare_context_objects(newconf, running): |
855 | """ | |
856 | Create a context diff for the two specified contexts | |
857 | """ | |
858 | ||
859 | # Compare the two Config objects to find the lines that we need to add/del | |
860 | lines_to_add = [] | |
861 | lines_to_del = [] | |
926ea62e | 862 | delete_bgpd = False |
2fc76430 DS |
863 | |
864 | # Find contexts that are in newconf but not in running | |
865 | # Find contexts that are in running but not in newconf | |
866 | for (running_ctx_keys, running_ctx) in running.contexts.iteritems(): | |
867 | ||
868 | if running_ctx_keys not in newconf.contexts: | |
869 | ||
ab5f8310 DW |
870 | # We check that the len is 1 here so that we only look at ('router bgp 10') |
871 | # and not ('router bgp 10', 'address-family ipv4 unicast'). The | |
926ea62e | 872 | # latter could cause a false delete_bgpd positive if ipv4 unicast is in |
ab5f8310 DW |
873 | # running but not in newconf. |
874 | if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1: | |
926ea62e DW |
875 | delete_bgpd = True |
876 | lines_to_del.append((running_ctx_keys, None)) | |
877 | ||
1a8c43f1 | 878 | # We cannot do 'no interface' in FRR, and so deal with it |
768bf950 DD |
879 | elif running_ctx_keys[0].startswith('interface'): |
880 | for line in running_ctx.lines: | |
881 | lines_to_del.append((running_ctx_keys, line)) | |
882 | ||
926ea62e DW |
883 | # If this is an address-family under 'router bgp' and we are already deleting the |
884 | # entire 'router bgp' context then ignore this sub-context | |
885 | elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd: | |
76f69d1c | 886 | continue |
514665b9 | 887 | |
5014d96f DW |
888 | # Delete an entire vni sub-context under "address-family l2vpn evpn" |
889 | elif ("router bgp" in running_ctx_keys[0] and | |
890 | len(running_ctx_keys) > 2 and | |
891 | running_ctx_keys[1].startswith('address-family l2vpn evpn') and | |
892 | running_ctx_keys[2].startswith('vni ')): | |
893 | lines_to_del.append((running_ctx_keys, None)) | |
894 | ||
afa2e8e1 DW |
895 | elif ("router bgp" in running_ctx_keys[0] and |
896 | len(running_ctx_keys) > 1 and | |
897 | running_ctx_keys[1].startswith('address-family')): | |
898 | # There's no 'no address-family' support and so we have to | |
899 | # delete each line individually again | |
900 | for line in running_ctx.lines: | |
901 | lines_to_del.append((running_ctx_keys, line)) | |
902 | ||
2fc76430 | 903 | # Non-global context |
926ea62e | 904 | elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys): |
2fc76430 DS |
905 | lines_to_del.append((running_ctx_keys, None)) |
906 | ||
7918b335 DW |
907 | elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys): |
908 | lines_to_del.append((running_ctx_keys, None)) | |
909 | ||
2fc76430 DS |
910 | # Global context |
911 | else: | |
912 | for line in running_ctx.lines: | |
913 | lines_to_del.append((running_ctx_keys, line)) | |
914 | ||
915 | # Find the lines within each context to add | |
916 | # Find the lines within each context to del | |
917 | for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems(): | |
918 | ||
919 | if newconf_ctx_keys in running.contexts: | |
920 | running_ctx = running.contexts[newconf_ctx_keys] | |
921 | ||
922 | for line in newconf_ctx.lines: | |
923 | if line not in running_ctx.dlines: | |
924 | lines_to_add.append((newconf_ctx_keys, line)) | |
925 | ||
926 | for line in running_ctx.lines: | |
927 | if line not in newconf_ctx.dlines: | |
928 | lines_to_del.append((newconf_ctx_keys, line)) | |
929 | ||
930 | for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems(): | |
931 | ||
932 | if newconf_ctx_keys not in running.contexts: | |
933 | lines_to_add.append((newconf_ctx_keys, None)) | |
934 | ||
935 | for line in newconf_ctx.lines: | |
936 | lines_to_add.append((newconf_ctx_keys, line)) | |
937 | ||
9b166171 | 938 | (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del) |
9fe88bc7 | 939 | |
926ea62e | 940 | return (lines_to_add, lines_to_del) |
2fc76430 | 941 | |
8ad1fe6c | 942 | |
2fc76430 DS |
943 | if __name__ == '__main__': |
944 | # Command line options | |
d8e4c438 | 945 | parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs') |
2fc76430 DS |
946 | parser.add_argument('--input', help='Read running config from file instead of "show running"') |
947 | group = parser.add_mutually_exclusive_group(required=True) | |
948 | group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False) | |
949 | group.add_argument('--test', action='store_true', help='Show the deltas', default=False) | |
950 | parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False) | |
cc146ecc | 951 | parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False) |
d8e4c438 | 952 | parser.add_argument('filename', help='Location of new frr config file') |
e20dc2ba | 953 | parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False) |
2fc76430 DS |
954 | args = parser.parse_args() |
955 | ||
956 | # Logging | |
957 | # For --test log to stdout | |
d8e4c438 | 958 | # For --reload log to /var/log/frr/frr-reload.log |
cc146ecc | 959 | if args.test or args.stdout: |
c50aceee | 960 | logging.basicConfig(level=logging.INFO, |
2fc76430 | 961 | format='%(asctime)s %(levelname)5s: %(message)s') |
926ea62e DW |
962 | |
963 | # Color the errors and warnings in red | |
964 | logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR)) | |
965 | logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING)) | |
966 | ||
2fc76430 | 967 | elif args.reload: |
d8e4c438 DS |
968 | if not os.path.isdir('/var/log/frr/'): |
969 | os.makedirs('/var/log/frr/') | |
2fc76430 | 970 | |
d8e4c438 | 971 | logging.basicConfig(filename='/var/log/frr/frr-reload.log', |
c50aceee | 972 | level=logging.INFO, |
2fc76430 DS |
973 | format='%(asctime)s %(levelname)5s: %(message)s') |
974 | ||
975 | # argparse should prevent this from happening but just to be safe... | |
976 | else: | |
977 | raise Exception('Must specify --reload or --test') | |
a782e613 | 978 | log = logging.getLogger(__name__) |
2fc76430 | 979 | |
76f69d1c DW |
980 | # Verify the new config file is valid |
981 | if not os.path.isfile(args.filename): | |
825be4c2 DW |
982 | msg = "Filename %s does not exist" % args.filename |
983 | print msg | |
984 | log.error(msg) | |
76f69d1c DW |
985 | sys.exit(1) |
986 | ||
987 | if not os.path.getsize(args.filename): | |
825be4c2 DW |
988 | msg = "Filename %s is an empty file" % args.filename |
989 | print msg | |
990 | log.error(msg) | |
76f69d1c DW |
991 | sys.exit(1) |
992 | ||
76f69d1c | 993 | # Verify that 'service integrated-vtysh-config' is configured |
d8e4c438 | 994 | vtysh_filename = '/etc/frr/vtysh.conf' |
6ac9179c | 995 | service_integrated_vtysh_config = True |
76f69d1c | 996 | |
f850d14d DW |
997 | if os.path.isfile(vtysh_filename): |
998 | with open(vtysh_filename, 'r') as fh: | |
999 | for line in fh.readlines(): | |
1000 | line = line.strip() | |
76f69d1c | 1001 | |
6ac9179c DD |
1002 | if line == 'no service integrated-vtysh-config': |
1003 | service_integrated_vtysh_config = False | |
f850d14d | 1004 | break |
76f69d1c DW |
1005 | |
1006 | if not service_integrated_vtysh_config: | |
825be4c2 DW |
1007 | msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'" |
1008 | print msg | |
1009 | log.error(msg) | |
76f69d1c | 1010 | sys.exit(1) |
2fc76430 | 1011 | |
c50aceee | 1012 | if args.debug: |
a782e613 | 1013 | log.setLevel(logging.DEBUG) |
c50aceee | 1014 | |
a782e613 | 1015 | log.info('Called via "%s"', str(args)) |
c50aceee | 1016 | |
2fc76430 DS |
1017 | # Create a Config object from the config generated by newconf |
1018 | newconf = Config() | |
1019 | newconf.load_from_file(args.filename) | |
825be4c2 | 1020 | reload_ok = True |
2fc76430 DS |
1021 | |
1022 | if args.test: | |
1023 | ||
1024 | # Create a Config object from the running config | |
1025 | running = Config() | |
1026 | ||
1027 | if args.input: | |
1028 | running.load_from_file(args.input) | |
1029 | else: | |
1030 | running.load_from_show_running() | |
1031 | ||
926ea62e | 1032 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
4a2587c6 | 1033 | lines_to_configure = [] |
2fc76430 DS |
1034 | |
1035 | if lines_to_del: | |
1036 | print "\nLines To Delete" | |
1037 | print "===============" | |
1038 | ||
1039 | for (ctx_keys, line) in lines_to_del: | |
1040 | ||
1041 | if line == '!': | |
1042 | continue | |
1043 | ||
4a2587c6 DW |
1044 | cmd = line_for_vtysh_file(ctx_keys, line, True) |
1045 | lines_to_configure.append(cmd) | |
9fe88bc7 | 1046 | print cmd |
2fc76430 DS |
1047 | |
1048 | if lines_to_add: | |
1049 | print "\nLines To Add" | |
1050 | print "============" | |
1051 | ||
1052 | for (ctx_keys, line) in lines_to_add: | |
1053 | ||
1054 | if line == '!': | |
1055 | continue | |
1056 | ||
4a2587c6 DW |
1057 | cmd = line_for_vtysh_file(ctx_keys, line, False) |
1058 | lines_to_configure.append(cmd) | |
9fe88bc7 | 1059 | print cmd |
2fc76430 | 1060 | |
2fc76430 DS |
1061 | elif args.reload: |
1062 | ||
d8e4c438 | 1063 | log.debug('New Frr Config\n%s', newconf.get_lines()) |
2fc76430 DS |
1064 | |
1065 | # This looks a little odd but we have to do this twice...here is why | |
1066 | # If the user had this running bgp config: | |
4a2587c6 | 1067 | # |
2fc76430 DS |
1068 | # router bgp 10 |
1069 | # neighbor 1.1.1.1 remote-as 50 | |
1070 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 | 1071 | # |
2fc76430 | 1072 | # and this config in the newconf config file |
4a2587c6 | 1073 | # |
2fc76430 DS |
1074 | # router bgp 10 |
1075 | # neighbor 1.1.1.1 remote-as 999 | |
1076 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 DW |
1077 | # |
1078 | # | |
2fc76430 DS |
1079 | # Then the script will do |
1080 | # - no neighbor 1.1.1.1 remote-as 50 | |
1081 | # - neighbor 1.1.1.1 remote-as 999 | |
4a2587c6 | 1082 | # |
2fc76430 DS |
1083 | # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove |
1084 | # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the | |
1085 | # configs again to put this line back. | |
1086 | ||
1a8c43f1 | 1087 | # There are many keywords in FRR that can only appear one time under |
2ce26af1 DW |
1088 | # a context, take "bgp router-id" for example. If the config that we are |
1089 | # reloading against has the following: | |
1090 | # | |
1091 | # router bgp 10 | |
1092 | # bgp router-id 1.1.1.1 | |
1093 | # bgp router-id 2.2.2.2 | |
1094 | # | |
1095 | # The final config needs to contain "bgp router-id 2.2.2.2". On the | |
1096 | # first pass we will add "bgp router-id 2.2.2.2" but then on the second | |
1097 | # pass we will see that "bgp router-id 1.1.1.1" is missing and add that | |
1098 | # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the | |
1099 | # second pass to include all of the "adds" from the first pass. | |
1100 | lines_to_add_first_pass = [] | |
1101 | ||
2fc76430 DS |
1102 | for x in range(2): |
1103 | running = Config() | |
1104 | running.load_from_show_running() | |
d8e4c438 | 1105 | log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines()) |
2fc76430 | 1106 | |
926ea62e | 1107 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
2fc76430 | 1108 | |
2ce26af1 DW |
1109 | if x == 0: |
1110 | lines_to_add_first_pass = lines_to_add | |
1111 | else: | |
1112 | lines_to_add.extend(lines_to_add_first_pass) | |
1113 | ||
53bddc22 | 1114 | # Only do deletes on the first pass. The reason being if we |
1a8c43f1 | 1115 | # configure a bgp neighbor via "neighbor swp1 interface" FRR |
53bddc22 DW |
1116 | # will automatically add: |
1117 | # | |
1118 | # interface swp1 | |
1119 | # ipv6 nd ra-interval 10 | |
1120 | # no ipv6 nd suppress-ra | |
1121 | # ! | |
1122 | # | |
1123 | # but those lines aren't in the config we are reloading against so | |
1124 | # on the 2nd pass they will show up in lines_to_del. This could | |
1125 | # apply to other scenarios as well where configuring FOO adds BAR | |
1126 | # to the config. | |
1127 | if lines_to_del and x == 0: | |
2fc76430 DS |
1128 | for (ctx_keys, line) in lines_to_del: |
1129 | ||
1130 | if line == '!': | |
1131 | continue | |
1132 | ||
4a2587c6 DW |
1133 | # 'no' commands are tricky, we can't just put them in a file and |
1134 | # vtysh -f that file. See the next comment for an explanation | |
1135 | # of their quirks | |
2fc76430 DS |
1136 | cmd = line_to_vtysh_conft(ctx_keys, line, True) |
1137 | original_cmd = cmd | |
1138 | ||
d8e4c438 | 1139 | # Some commands in frr are picky about taking a "no" of the entire line. |
76f69d1c DW |
1140 | # OSPF is bad about this, you can't "no" the entire line, you have to "no" |
1141 | # only the beginning. If we hit one of these command an exception will be | |
1142 | # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again. | |
4a2587c6 | 1143 | # |
76f69d1c | 1144 | # Example: |
d8e4c438 DS |
1145 | # frr(config-if)# ip ospf authentication message-digest 1.1.1.1 |
1146 | # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1 | |
76f69d1c | 1147 | # % Unknown command. |
d8e4c438 | 1148 | # frr(config-if)# no ip ospf authentication message-digest |
76f69d1c | 1149 | # % Unknown command. |
d8e4c438 DS |
1150 | # frr(config-if)# no ip ospf authentication |
1151 | # frr(config-if)# | |
2fc76430 DS |
1152 | |
1153 | while True: | |
2fc76430 | 1154 | try: |
478f9ce2 | 1155 | _ = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
2fc76430 DS |
1156 | |
1157 | except subprocess.CalledProcessError: | |
1158 | ||
1159 | # - Pull the last entry from cmd (this would be | |
1160 | # 'no ip ospf authentication message-digest 1.1.1.1' in | |
1161 | # our example above | |
1162 | # - Split that last entry by whitespace and drop the last word | |
825be4c2 | 1163 | log.info('Failed to execute %s', ' '.join(cmd)) |
2fc76430 DS |
1164 | last_arg = cmd[-1].split(' ') |
1165 | ||
1166 | if len(last_arg) <= 2: | |
a782e613 | 1167 | log.error('"%s" we failed to remove this command', original_cmd) |
2fc76430 DS |
1168 | break |
1169 | ||
1170 | new_last_arg = last_arg[0:-1] | |
1171 | cmd[-1] = ' '.join(new_last_arg) | |
1172 | else: | |
a782e613 | 1173 | log.info('Executed "%s"', ' '.join(cmd)) |
2fc76430 DS |
1174 | break |
1175 | ||
2fc76430 | 1176 | if lines_to_add: |
4a2587c6 DW |
1177 | lines_to_configure = [] |
1178 | ||
2fc76430 DS |
1179 | for (ctx_keys, line) in lines_to_add: |
1180 | ||
1181 | if line == '!': | |
1182 | continue | |
1183 | ||
4a2587c6 DW |
1184 | cmd = line_for_vtysh_file(ctx_keys, line, False) |
1185 | lines_to_configure.append(cmd) | |
1186 | ||
1187 | if lines_to_configure: | |
1188 | random_string = ''.join(random.SystemRandom().choice( | |
1189 | string.ascii_uppercase + | |
1190 | string.digits) for _ in range(6)) | |
1191 | ||
d8e4c438 | 1192 | filename = "/var/run/frr/reload-%s.txt" % random_string |
a782e613 | 1193 | log.info("%s content\n%s" % (filename, pformat(lines_to_configure))) |
4a2587c6 DW |
1194 | |
1195 | with open(filename, 'w') as fh: | |
1196 | for line in lines_to_configure: | |
1197 | fh.write(line + '\n') | |
825be4c2 | 1198 | |
596074af | 1199 | try: |
478f9ce2 | 1200 | subprocess.check_output(['/usr/bin/vtysh', '-f', filename], stderr=subprocess.STDOUT) |
596074af DW |
1201 | except subprocess.CalledProcessError as e: |
1202 | log.warning("frr-reload.py failed due to\n%s" % e.output) | |
1203 | reload_ok = False | |
4a2587c6 | 1204 | os.unlink(filename) |
2fc76430 | 1205 | |
4b78098d | 1206 | # Make these changes persistent |
e20dc2ba | 1207 | if args.overwrite or args.filename != '/etc/frr/frr.conf': |
926ea62e | 1208 | subprocess.call(['/usr/bin/vtysh', '-c', 'write']) |
825be4c2 DW |
1209 | |
1210 | if not reload_ok: | |
1211 | sys.exit(1) |