]>
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 | ||
1c64265f | 31 | from __future__ import print_function, unicode_literals |
2fc76430 DS |
32 | import argparse |
33 | import copy | |
34 | import logging | |
663ece2f | 35 | import os, os.path |
4a2587c6 | 36 | import random |
9fe88bc7 | 37 | import re |
4a2587c6 | 38 | import string |
2fc76430 DS |
39 | import subprocess |
40 | import sys | |
41 | from collections import OrderedDict | |
1c64265f | 42 | try: |
43 | from ipaddress import IPv6Address, ip_network | |
44 | except ImportError: | |
45 | from ipaddr import IPv6Address, IPNetwork | |
4a2587c6 DW |
46 | from pprint import pformat |
47 | ||
1c64265f | 48 | try: |
49 | dict.iteritems | |
50 | except AttributeError: | |
51 | # Python 3 | |
52 | def iteritems(d): | |
53 | return iter(d.items()) | |
54 | else: | |
55 | # Python 2 | |
56 | def iteritems(d): | |
57 | return d.iteritems() | |
2fc76430 | 58 | |
a782e613 DW |
59 | log = logging.getLogger(__name__) |
60 | ||
61 | ||
663ece2f | 62 | class VtyshException(Exception): |
276887bb DW |
63 | pass |
64 | ||
663ece2f | 65 | class Vtysh(object): |
a0a7dead | 66 | def __init__(self, bindir=None, confdir=None, sockdir=None, pathspace=None): |
663ece2f DL |
67 | self.bindir = bindir |
68 | self.confdir = confdir | |
a0a7dead | 69 | self.pathspace = pathspace |
663ece2f DL |
70 | self.common_args = [os.path.join(bindir or '', 'vtysh')] |
71 | if confdir: | |
72 | self.common_args.extend(['--config_dir', confdir]) | |
fa18c6bb DL |
73 | if sockdir: |
74 | self.common_args.extend(['--vty_socket', sockdir]) | |
a0a7dead DL |
75 | if pathspace: |
76 | self.common_args.extend(['-N', pathspace]) | |
663ece2f DL |
77 | |
78 | def _call(self, args, stdin=None, stdout=None, stderr=None): | |
79 | kwargs = {} | |
80 | if stdin is not None: | |
81 | kwargs['stdin'] = stdin | |
82 | if stdout is not None: | |
83 | kwargs['stdout'] = stdout | |
84 | if stderr is not None: | |
85 | kwargs['stderr'] = stderr | |
86 | return subprocess.Popen(self.common_args + args, **kwargs) | |
87 | ||
88 | def _call_cmd(self, command, stdin=None, stdout=None, stderr=None): | |
89 | if isinstance(command, list): | |
90 | args = [item for sub in command for item in ['-c', sub]] | |
91 | else: | |
92 | args = ['-c', command] | |
93 | return self._call(args, stdin, stdout, stderr) | |
94 | ||
95 | def __call__(self, command): | |
96 | """ | |
97 | Call a CLI command (e.g. "show running-config") | |
98 | ||
99 | Output text is automatically redirected, decoded and returned. | |
100 | Multiple commands may be passed as list. | |
101 | """ | |
102 | proc = self._call_cmd(command, stdout=subprocess.PIPE) | |
103 | stdout, stderr = proc.communicate() | |
104 | if proc.wait() != 0: | |
105 | raise VtyshException('vtysh returned status %d for command "%s"' | |
106 | % (proc.returncode, command)) | |
107 | return stdout.decode('UTF-8') | |
108 | ||
109 | def is_config_available(self): | |
110 | """ | |
111 | Return False if no frr daemon is running or some other vtysh session is | |
112 | in 'configuration terminal' mode which will prevent us from making any | |
113 | configuration changes. | |
114 | """ | |
115 | ||
116 | output = self('configure') | |
117 | ||
118 | if 'VTY configuration is locked by other VTY' in output: | |
119 | print(output) | |
120 | log.error("vtysh 'configure' returned\n%s\n" % (output)) | |
121 | return False | |
122 | ||
123 | return True | |
124 | ||
125 | def exec_file(self, filename): | |
126 | child = self._call(['-f', filename]) | |
127 | if child.wait() != 0: | |
128 | raise VtyshException('vtysh (exec file) exited with status %d' | |
129 | % (child.returncode)) | |
130 | ||
131 | def mark_file(self, filename, stdin=None): | |
132 | kwargs = {} | |
133 | if stdin is not None: | |
134 | kwargs['stdin'] = stdin | |
135 | ||
136 | child = self._call(['-m', '-f', filename], | |
137 | stdout=subprocess.PIPE, **kwargs) | |
138 | try: | |
139 | stdout, stderr = child.communicate() | |
140 | except subprocess.TimeoutExpired: | |
141 | child.kill() | |
142 | stdout, stderr = proc.communicate() | |
143 | raise VtyshException('vtysh call timed out!') | |
144 | ||
145 | if child.wait() != 0: | |
146 | raise VtyshException('vtysh (mark file) exited with status %d:\n%s' | |
147 | % (child.returncode, stderr)) | |
148 | ||
149 | return stdout.decode('UTF-8') | |
150 | ||
151 | def mark_show_run(self, daemon = None): | |
7e7fedcb | 152 | cmd = 'show running-config' |
663ece2f DL |
153 | if daemon: |
154 | cmd += ' %s' % daemon | |
7e7fedcb | 155 | cmd += ' no-header' |
663ece2f DL |
156 | show_run = self._call_cmd(cmd, stdout=subprocess.PIPE) |
157 | mark = self._call(['-m', '-f', '-'], stdin=show_run.stdout, stdout=subprocess.PIPE) | |
158 | ||
159 | show_run.wait() | |
160 | stdout, stderr = mark.communicate() | |
161 | mark.wait() | |
162 | ||
163 | if show_run.returncode != 0: | |
164 | raise VtyshException('vtysh (show running-config) exited with status %d:' | |
165 | % (show_run.returncode)) | |
166 | if mark.returncode != 0: | |
167 | raise VtyshException('vtysh (mark running-config) exited with status %d' | |
168 | % (mark.returncode)) | |
169 | ||
170 | return stdout.decode('UTF-8') | |
276887bb | 171 | |
2fc76430 | 172 | class Context(object): |
4a2587c6 | 173 | |
2fc76430 | 174 | """ |
d8e4c438 | 175 | A Context object represents a section of frr configuration such as: |
2fc76430 DS |
176 | ! |
177 | interface swp3 | |
178 | description swp3 -> r8's swp1 | |
179 | ipv6 nd suppress-ra | |
180 | link-detect | |
181 | ! | |
182 | ||
183 | or a single line context object such as this: | |
184 | ||
185 | ip forwarding | |
186 | ||
187 | """ | |
188 | ||
189 | def __init__(self, keys, lines): | |
190 | self.keys = keys | |
191 | self.lines = lines | |
192 | ||
193 | # Keep a dictionary of the lines, this is to make it easy to tell if a | |
194 | # line exists in this Context | |
195 | self.dlines = OrderedDict() | |
196 | ||
197 | for ligne in lines: | |
198 | self.dlines[ligne] = True | |
199 | ||
200 | def add_lines(self, lines): | |
201 | """ | |
202 | Add lines to specified context | |
203 | """ | |
204 | ||
205 | self.lines.extend(lines) | |
206 | ||
207 | for ligne in lines: | |
208 | self.dlines[ligne] = True | |
209 | ||
210 | ||
211 | class Config(object): | |
4a2587c6 | 212 | |
2fc76430 | 213 | """ |
d8e4c438 | 214 | A frr configuration is stored in a Config object. A Config object |
2fc76430 DS |
215 | contains a dictionary of Context objects where the Context keys |
216 | ('router ospf' for example) are our dictionary key. | |
217 | """ | |
218 | ||
663ece2f | 219 | def __init__(self, vtysh): |
2fc76430 DS |
220 | self.lines = [] |
221 | self.contexts = OrderedDict() | |
663ece2f | 222 | self.vtysh = vtysh |
2fc76430 | 223 | |
663ece2f | 224 | def load_from_file(self, filename): |
2fc76430 DS |
225 | """ |
226 | Read configuration from specified file and slurp it into internal memory | |
227 | The internal representation has been marked appropriately by passing it | |
228 | through vtysh with the -m parameter | |
229 | """ | |
a782e613 | 230 | log.info('Loading Config object from file %s', filename) |
2fc76430 | 231 | |
663ece2f DL |
232 | file_output = self.vtysh.mark_file(filename) |
233 | ||
234 | for line in file_output.split('\n'): | |
2fc76430 | 235 | line = line.strip() |
89cca49b DW |
236 | |
237 | # Compress duplicate whitespaces | |
238 | line = ' '.join(line.split()) | |
239 | ||
e238920d | 240 | if ":" in line and not "ipv6 add": |
2fc76430 DS |
241 | qv6_line = get_normalized_ipv6_line(line) |
242 | self.lines.append(qv6_line) | |
243 | else: | |
244 | self.lines.append(line) | |
245 | ||
246 | self.load_contexts() | |
247 | ||
663ece2f | 248 | def load_from_show_running(self, daemon): |
2fc76430 DS |
249 | """ |
250 | Read running configuration and slurp it into internal memory | |
251 | The internal representation has been marked appropriately by passing it | |
252 | through vtysh with the -m parameter | |
253 | """ | |
a782e613 | 254 | log.info('Loading Config object from vtysh show running') |
2fc76430 | 255 | |
663ece2f DL |
256 | config_text = self.vtysh.mark_show_run(daemon) |
257 | ||
258 | for line in config_text.split('\n'): | |
2fc76430 DS |
259 | line = line.strip() |
260 | ||
261 | if (line == 'Building configuration...' or | |
262 | line == 'Current configuration:' or | |
4a2587c6 | 263 | not line): |
2fc76430 DS |
264 | continue |
265 | ||
266 | self.lines.append(line) | |
267 | ||
268 | self.load_contexts() | |
269 | ||
270 | def get_lines(self): | |
271 | """ | |
272 | Return the lines read in from the configuration | |
273 | """ | |
274 | ||
275 | return '\n'.join(self.lines) | |
276 | ||
277 | def get_contexts(self): | |
278 | """ | |
279 | Return the parsed context as strings for display, log etc. | |
280 | """ | |
281 | ||
1c64265f | 282 | for (_, ctx) in sorted(iteritems(self.contexts)): |
283 | print(str(ctx) + '\n') | |
2fc76430 DS |
284 | |
285 | def save_contexts(self, key, lines): | |
286 | """ | |
287 | Save the provided key and lines as a context | |
288 | """ | |
289 | ||
290 | if not key: | |
291 | return | |
292 | ||
bb972e44 DD |
293 | ''' |
294 | IP addresses specified in "network" statements, "ip prefix-lists" | |
295 | etc. can differ in the host part of the specification the user | |
296 | provides and what the running config displays. For example, user | |
297 | can specify 11.1.1.1/24, and the running config displays this as | |
298 | 11.1.1.0/24. Ensure we don't do a needless operation for such | |
299 | lines. IS-IS & OSPFv3 have no "network" support. | |
300 | ''' | |
4d760f42 | 301 | re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0]) |
bb972e44 DD |
302 | if re_key_rt: |
303 | addr = re_key_rt.group(2) | |
304 | if '/' in addr: | |
305 | try: | |
1c64265f | 306 | if 'ipaddress' not in sys.modules: |
307 | newaddr = IPNetwork(addr) | |
308 | key[0] = '%s route %s/%s%s' % (re_key_rt.group(1), | |
309 | newaddr.network, | |
310 | newaddr.prefixlen, | |
311 | re_key_rt.group(3)) | |
312 | else: | |
313 | newaddr = ip_network(addr, strict=False) | |
314 | key[0] = '%s route %s/%s%s' % (re_key_rt.group(1), | |
315 | str(newaddr.network_address), | |
316 | newaddr.prefixlen, | |
317 | re_key_rt.group(3)) | |
bb972e44 DD |
318 | except ValueError: |
319 | pass | |
320 | ||
321 | re_key_rt = re.match( | |
322 | r'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$', | |
323 | key[0] | |
324 | ) | |
325 | if re_key_rt: | |
326 | addr = re_key_rt.group(4) | |
327 | if '/' in addr: | |
328 | try: | |
1c64265f | 329 | if 'ipaddress' not in sys.modules: |
330 | newaddr = '%s/%s' % (IPNetwork(addr).network, | |
331 | IPNetwork(addr).prefixlen) | |
332 | else: | |
333 | network_addr = ip_network(addr, strict=False) | |
334 | newaddr = '%s/%s' % (str(network_addr.network_address), | |
335 | network_addr.prefixlen) | |
bb972e44 DD |
336 | except ValueError: |
337 | newaddr = addr | |
0845b872 DD |
338 | else: |
339 | newaddr = addr | |
bb972e44 DD |
340 | |
341 | legestr = re_key_rt.group(5) | |
342 | re_lege = re.search(r'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr) | |
343 | if re_lege: | |
344 | legestr = '%sge %s le %s%s' % (re_lege.group(1), | |
345 | re_lege.group(3), | |
346 | re_lege.group(2), | |
347 | re_lege.group(4)) | |
348 | re_lege = re.search(r'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr) | |
349 | ||
350 | if (re_lege and ((re_key_rt.group(1) == "ip" and | |
351 | re_lege.group(3) == "32") or | |
352 | (re_key_rt.group(1) == "ipv6" and | |
353 | re_lege.group(3) == "128"))): | |
354 | legestr = '%sge %s%s' % (re_lege.group(1), | |
355 | re_lege.group(2), | |
356 | re_lege.group(4)) | |
357 | ||
358 | key[0] = '%s prefix-list%s%s %s%s' % (re_key_rt.group(1), | |
359 | re_key_rt.group(2), | |
360 | re_key_rt.group(3), | |
361 | newaddr, | |
362 | legestr) | |
363 | ||
364 | if lines and key[0].startswith('router bgp'): | |
365 | newlines = [] | |
366 | for line in lines: | |
367 | re_net = re.match(r'network\s+([A-Fa-f:.0-9/]+)(.*)$', line) | |
368 | if re_net: | |
369 | addr = re_net.group(1) | |
370 | if '/' not in addr and key[0].startswith('router bgp'): | |
371 | # This is most likely an error because with no | |
372 | # prefixlen, BGP treats the prefixlen as 8 | |
373 | addr = addr + '/8' | |
374 | ||
375 | try: | |
1c64265f | 376 | if 'ipaddress' not in sys.modules: |
377 | newaddr = IPNetwork(addr) | |
378 | line = 'network %s/%s %s' % (newaddr.network, | |
379 | newaddr.prefixlen, | |
380 | re_net.group(2)) | |
381 | else: | |
382 | network_addr = ip_network(addr, strict=False) | |
383 | line = 'network %s/%s %s' % (str(network_addr.network_address), | |
384 | network_addr.prefixlen, | |
385 | re_net.group(2)) | |
bb972e44 | 386 | newlines.append(line) |
0845b872 | 387 | except ValueError: |
bb972e44 DD |
388 | # Really this should be an error. Whats a network |
389 | # without an IP Address following it ? | |
390 | newlines.append(line) | |
391 | else: | |
392 | newlines.append(line) | |
393 | lines = newlines | |
394 | ||
395 | ''' | |
396 | More fixups in user specification and what running config shows. | |
348135a5 | 397 | "null0" in routes must be replaced by Null0. |
bb972e44 DD |
398 | ''' |
399 | if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and | |
348135a5 | 400 | 'null0' in key[0]): |
bb972e44 | 401 | key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0]) |
bb972e44 | 402 | |
2fc76430 DS |
403 | if lines: |
404 | if tuple(key) not in self.contexts: | |
405 | ctx = Context(tuple(key), lines) | |
406 | self.contexts[tuple(key)] = ctx | |
407 | else: | |
408 | ctx = self.contexts[tuple(key)] | |
409 | ctx.add_lines(lines) | |
410 | ||
411 | else: | |
412 | if tuple(key) not in self.contexts: | |
413 | ctx = Context(tuple(key), []) | |
414 | self.contexts[tuple(key)] = ctx | |
415 | ||
416 | def load_contexts(self): | |
417 | """ | |
418 | Parse the configuration and create contexts for each appropriate block | |
419 | """ | |
420 | ||
421 | current_context_lines = [] | |
422 | ctx_keys = [] | |
423 | ||
424 | ''' | |
425 | The end of a context is flagged via the 'end' keyword: | |
426 | ||
427 | ! | |
428 | interface swp52 | |
429 | ipv6 nd suppress-ra | |
430 | link-detect | |
431 | ! | |
432 | end | |
433 | router bgp 10 | |
434 | bgp router-id 10.0.0.1 | |
435 | bgp log-neighbor-changes | |
436 | no bgp default ipv4-unicast | |
437 | neighbor EBGP peer-group | |
438 | neighbor EBGP advertisement-interval 1 | |
439 | neighbor EBGP timers connect 10 | |
440 | neighbor 2001:40:1:4::6 remote-as 40 | |
441 | neighbor 2001:40:1:8::a remote-as 40 | |
442 | ! | |
443 | end | |
444 | address-family ipv6 | |
445 | neighbor IBGPv6 activate | |
446 | neighbor 2001:10::2 peer-group IBGPv6 | |
447 | neighbor 2001:10::3 peer-group IBGPv6 | |
448 | exit-address-family | |
449 | ! | |
7918b335 DW |
450 | end |
451 | address-family evpn | |
452 | neighbor LEAF activate | |
453 | advertise-all-vni | |
454 | vni 10100 | |
455 | rd 65000:10100 | |
456 | route-target import 10.1.1.1:10100 | |
457 | route-target export 10.1.1.1:10100 | |
458 | exit-vni | |
459 | exit-address-family | |
460 | ! | |
2fc76430 DS |
461 | end |
462 | router ospf | |
463 | ospf router-id 10.0.0.1 | |
464 | log-adjacency-changes detail | |
465 | timers throttle spf 0 50 5000 | |
466 | ! | |
467 | end | |
468 | ''' | |
469 | ||
470 | # The code assumes that its working on the output from the "vtysh -m" | |
471 | # command. That provides the appropriate markers to signify end of | |
472 | # a context. This routine uses that to build the contexts for the | |
473 | # config. | |
474 | # | |
475 | # There are single line contexts such as "log file /media/node/zebra.log" | |
476 | # and multi-line contexts such as "router ospf" and subcontexts | |
477 | # within a context such as "address-family" within "router bgp" | |
478 | # In each of these cases, the first line of the context becomes the | |
479 | # key of the context. So "router bgp 10" is the key for the non-address | |
480 | # family part of bgp, "router bgp 10, address-family ipv6 unicast" is | |
481 | # the key for the subcontext and so on. | |
2fc76430 DS |
482 | ctx_keys = [] |
483 | main_ctx_key = [] | |
484 | new_ctx = True | |
2fc76430 DS |
485 | |
486 | # the keywords that we know are single line contexts. bgp in this case | |
487 | # is not the main router bgp block, but enabling multi-instance | |
2fed5dcd | 488 | oneline_ctx_keywords = ("access-list ", |
e80c8c55 | 489 | "agentx", |
55c8666a | 490 | "allow-external-route-update", |
2fed5dcd DW |
491 | "bgp ", |
492 | "debug ", | |
55c8666a | 493 | "domainname ", |
2fed5dcd DW |
494 | "dump ", |
495 | "enable ", | |
825be4c2 | 496 | "frr ", |
2fed5dcd DW |
497 | "hostname ", |
498 | "ip ", | |
499 | "ipv6 ", | |
500 | "log ", | |
ccef6e47 EDP |
501 | "mpls lsp", |
502 | "mpls label", | |
a11209a7 | 503 | "no ", |
2fed5dcd DW |
504 | "password ", |
505 | "ptm-enable", | |
506 | "router-id ", | |
507 | "service ", | |
508 | "table ", | |
509 | "username ", | |
f9d31f6f | 510 | "zebra ", |
a840a40d QY |
511 | "vrrp autoconfigure", |
512 | "evpn mh") | |
2fed5dcd | 513 | |
2fc76430 DS |
514 | for line in self.lines: |
515 | ||
516 | if not line: | |
517 | continue | |
518 | ||
519 | if line.startswith('!') or line.startswith('#'): | |
520 | continue | |
521 | ||
522 | # one line contexts | |
ccef6e47 EDP |
523 | # there is one exception though: ldpd accepts a 'router-id' clause |
524 | # as part of its 'mpls ldp' config context. If we are processing | |
525 | # ldp configuration and encounter a router-id we should NOT switch | |
526 | # to a new context | |
527 | if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords) and not ( | |
528 | ctx_keys and ctx_keys[0].startswith("mpls ldp") and line.startswith("router-id ")): | |
2fc76430 DS |
529 | self.save_contexts(ctx_keys, current_context_lines) |
530 | ||
531 | # Start a new context | |
532 | main_ctx_key = [] | |
4a2587c6 | 533 | ctx_keys = [line, ] |
2fc76430 DS |
534 | current_context_lines = [] |
535 | ||
a782e613 | 536 | log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
2fc76430 DS |
537 | self.save_contexts(ctx_keys, current_context_lines) |
538 | new_ctx = True | |
539 | ||
06ad470d | 540 | elif line == "end": |
2fc76430 | 541 | self.save_contexts(ctx_keys, current_context_lines) |
a782e613 | 542 | log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys) |
2fc76430 DS |
543 | |
544 | # Start a new context | |
06ad470d DS |
545 | new_ctx = True |
546 | main_ctx_key = [] | |
547 | ctx_keys = [] | |
548 | current_context_lines = [] | |
549 | ||
550 | elif line == "exit-vrf": | |
551 | self.save_contexts(ctx_keys, current_context_lines) | |
552 | current_context_lines.append(line) | |
553 | log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys) | |
554 | ||
555 | #Start a new context | |
2fc76430 DS |
556 | new_ctx = True |
557 | main_ctx_key = [] | |
558 | ctx_keys = [] | |
559 | current_context_lines = [] | |
560 | ||
d60f4800 | 561 | elif line in ["exit-address-family", "exit", "exit-vnc"]: |
2fc76430 DS |
562 | # if this exit is for address-family ipv4 unicast, ignore the pop |
563 | if main_ctx_key: | |
564 | self.save_contexts(ctx_keys, current_context_lines) | |
565 | ||
566 | # Start a new context | |
567 | ctx_keys = copy.deepcopy(main_ctx_key) | |
568 | current_context_lines = [] | |
a782e613 | 569 | log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys) |
2fc76430 | 570 | |
609ac8dd | 571 | elif line in ["exit-vni", "exit-ldp-if"]: |
d60f4800 DS |
572 | if sub_main_ctx_key: |
573 | self.save_contexts(ctx_keys, current_context_lines) | |
574 | ||
575 | # Start a new context | |
576 | ctx_keys = copy.deepcopy(sub_main_ctx_key) | |
577 | current_context_lines = [] | |
578 | log.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line, ctx_keys) | |
579 | ||
2fc76430 DS |
580 | elif new_ctx is True: |
581 | if not main_ctx_key: | |
4a2587c6 | 582 | ctx_keys = [line, ] |
2fc76430 DS |
583 | else: |
584 | ctx_keys = copy.deepcopy(main_ctx_key) | |
585 | main_ctx_key = [] | |
586 | ||
587 | current_context_lines = [] | |
588 | new_ctx = False | |
a782e613 | 589 | log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) |
6bd0508a CF |
590 | elif (line.startswith("address-family ") or |
591 | line.startswith("vnc defaults") or | |
592 | line.startswith("vnc l2-group") or | |
ccef6e47 | 593 | line.startswith("vnc nve-group") or |
1c23a0aa | 594 | line.startswith("peer") or |
ccef6e47 | 595 | line.startswith("member pseudowire")): |
2fc76430 DS |
596 | main_ctx_key = [] |
597 | ||
0b960b4d DW |
598 | # Save old context first |
599 | self.save_contexts(ctx_keys, current_context_lines) | |
600 | current_context_lines = [] | |
601 | main_ctx_key = copy.deepcopy(ctx_keys) | |
a782e613 | 602 | log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line) |
2fc76430 | 603 | |
ccef6e47 | 604 | if line == "address-family ipv6" and not ctx_keys[0].startswith("mpls ldp"): |
0b960b4d | 605 | ctx_keys.append("address-family ipv6 unicast") |
ccef6e47 | 606 | elif line == "address-family ipv4" and not ctx_keys[0].startswith("mpls ldp"): |
0b960b4d | 607 | ctx_keys.append("address-family ipv4 unicast") |
5014d96f DW |
608 | elif line == "address-family evpn": |
609 | ctx_keys.append("address-family l2vpn evpn") | |
0b960b4d DW |
610 | else: |
611 | ctx_keys.append(line) | |
2fc76430 | 612 | |
d60f4800 DS |
613 | elif ((line.startswith("vni ") and |
614 | len(ctx_keys) == 2 and | |
615 | ctx_keys[0].startswith('router bgp') and | |
616 | ctx_keys[1] == 'address-family l2vpn evpn')): | |
617 | ||
618 | # Save old context first | |
619 | self.save_contexts(ctx_keys, current_context_lines) | |
620 | current_context_lines = [] | |
621 | sub_main_ctx_key = copy.deepcopy(ctx_keys) | |
622 | log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line) | |
623 | ctx_keys.append(line) | |
609ac8dd EDP |
624 | |
625 | elif ((line.startswith("interface ") and | |
626 | len(ctx_keys) == 2 and | |
627 | ctx_keys[0].startswith('mpls ldp') and | |
628 | ctx_keys[1].startswith('address-family'))): | |
629 | ||
630 | # Save old context first | |
631 | self.save_contexts(ctx_keys, current_context_lines) | |
d60f4800 DS |
632 | current_context_lines = [] |
633 | sub_main_ctx_key = copy.deepcopy(ctx_keys) | |
634 | log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line) | |
635 | ctx_keys.append(line) | |
636 | ||
2fc76430 DS |
637 | else: |
638 | # Continuing in an existing context, add non-commented lines to it | |
639 | current_context_lines.append(line) | |
a782e613 | 640 | log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys) |
2fc76430 DS |
641 | |
642 | # Save the context of the last one | |
643 | self.save_contexts(ctx_keys, current_context_lines) | |
644 | ||
4a2587c6 | 645 | |
663ece2f | 646 | def lines_to_config(ctx_keys, line, delete): |
4a2587c6 | 647 | """ |
e20dc2ba | 648 | Return the command as it would appear in frr.conf |
4a2587c6 DW |
649 | """ |
650 | cmd = [] | |
651 | ||
652 | if line: | |
653 | for (i, ctx_key) in enumerate(ctx_keys): | |
654 | cmd.append(' ' * i + ctx_key) | |
655 | ||
656 | line = line.lstrip() | |
657 | indent = len(ctx_keys) * ' ' | |
658 | ||
663ece2f DL |
659 | # There are some commands that are on by default so their "no" form will be |
660 | # displayed in the config. "no bgp default ipv4-unicast" is one of these. | |
661 | # If we need to remove this line we do so by adding "bgp default ipv4-unicast", | |
662 | # not by doing a "no no bgp default ipv4-unicast" | |
4a2587c6 DW |
663 | if delete: |
664 | if line.startswith('no '): | |
665 | cmd.append('%s%s' % (indent, line[3:])) | |
666 | else: | |
667 | cmd.append('%sno %s' % (indent, line)) | |
668 | ||
669 | else: | |
670 | cmd.append(indent + line) | |
671 | ||
672 | # If line is None then we are typically deleting an entire | |
673 | # context ('no router ospf' for example) | |
674 | else: | |
663ece2f DL |
675 | for i, ctx_key in enumerate(ctx_keys[:-1]): |
676 | cmd.append('%s%s' % (' ' * i, ctx_key)) | |
4a2587c6 | 677 | |
663ece2f DL |
678 | # Only put the 'no' on the last sub-context |
679 | if delete: | |
680 | if ctx_keys[-1].startswith('no '): | |
681 | cmd.append('%s%s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1][3:])) | |
682 | else: | |
683 | cmd.append('%sno %s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1])) | |
4a2587c6 | 684 | else: |
663ece2f | 685 | cmd.append('%s%s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1])) |
8ad1fe6c DW |
686 | |
687 | return cmd | |
4a2587c6 DW |
688 | |
689 | ||
2fc76430 DS |
690 | def get_normalized_ipv6_line(line): |
691 | """ | |
d8e4c438 | 692 | Return a normalized IPv6 line as produced by frr, |
2fc76430 | 693 | with all letters in lower case and trailing and leading |
bb972e44 DD |
694 | zeros removed, and only the network portion present if |
695 | the IPv6 word is a network | |
2fc76430 DS |
696 | """ |
697 | norm_line = "" | |
698 | words = line.split(' ') | |
699 | for word in words: | |
700 | if ":" in word: | |
bb972e44 DD |
701 | norm_word = None |
702 | if "/" in word: | |
703 | try: | |
1c64265f | 704 | if 'ipaddress' not in sys.modules: |
705 | v6word = IPNetwork(word) | |
706 | norm_word = '%s/%s' % (v6word.network, v6word.prefixlen) | |
707 | else: | |
708 | v6word = ip_network(word, strict=False) | |
709 | norm_word = '%s/%s' % (str(v6word.network_address), v6word.prefixlen) | |
bb972e44 DD |
710 | except ValueError: |
711 | pass | |
712 | if not norm_word: | |
713 | try: | |
714 | norm_word = '%s' % IPv6Address(word) | |
0845b872 | 715 | except ValueError: |
bb972e44 | 716 | norm_word = word |
2fc76430 DS |
717 | else: |
718 | norm_word = word | |
719 | norm_line = norm_line + " " + norm_word | |
720 | ||
721 | return norm_line.strip() | |
722 | ||
4a2587c6 | 723 | |
c755f5c4 | 724 | def line_exist(lines, target_ctx_keys, target_line, exact_match=True): |
9fe88bc7 | 725 | for (ctx_keys, line) in lines: |
c755f5c4 DW |
726 | if ctx_keys == target_ctx_keys: |
727 | if exact_match: | |
728 | if line == target_line: | |
729 | return True | |
730 | else: | |
731 | if line.startswith(target_line): | |
732 | return True | |
9fe88bc7 DW |
733 | return False |
734 | ||
eb9113df DS |
735 | def check_for_exit_vrf(lines_to_add, lines_to_del): |
736 | ||
737 | # exit-vrf is a bit tricky. If the new config is missing it but we | |
738 | # have configs under a vrf, we need to add it at the end to do the | |
739 | # right context changes. If exit-vrf exists in both the running and | |
740 | # new config, we cannot delete it or it will break context changes. | |
741 | add_exit_vrf = False | |
742 | index = 0 | |
743 | ||
744 | for (ctx_keys, line) in lines_to_add: | |
745 | if add_exit_vrf == True: | |
746 | if ctx_keys[0] != prior_ctx_key: | |
747 | insert_key=(prior_ctx_key), | |
748 | lines_to_add.insert(index, ((insert_key, "exit-vrf"))) | |
749 | add_exit_vrf = False | |
750 | ||
751 | if ctx_keys[0].startswith('vrf') and line: | |
752 | if line is not "exit-vrf": | |
753 | add_exit_vrf = True | |
754 | prior_ctx_key = (ctx_keys[0]) | |
755 | else: | |
756 | add_exit_vrf = False | |
757 | index+=1 | |
758 | ||
759 | for (ctx_keys, line) in lines_to_del: | |
760 | if line == "exit-vrf": | |
761 | if (line_exist(lines_to_add, ctx_keys, line)): | |
762 | lines_to_del.remove((ctx_keys, line)) | |
763 | ||
764 | return (lines_to_add, lines_to_del) | |
9fe88bc7 | 765 | |
9b166171 | 766 | def ignore_delete_re_add_lines(lines_to_add, lines_to_del): |
9fe88bc7 DW |
767 | |
768 | # Quite possibly the most confusing (while accurate) variable names in history | |
769 | lines_to_add_to_del = [] | |
770 | lines_to_del_to_del = [] | |
771 | ||
772 | for (ctx_keys, line) in lines_to_del: | |
9b166171 DW |
773 | deleted = False |
774 | ||
028bcc88 DW |
775 | if ctx_keys[0].startswith('router bgp') and line: |
776 | ||
777 | if line.startswith('neighbor '): | |
778 | ''' | |
779 | BGP changed how it displays swpX peers that are part of peer-group. Older | |
780 | versions of frr would display these on separate lines: | |
781 | neighbor swp1 interface | |
782 | neighbor swp1 peer-group FOO | |
783 | ||
784 | but today we display via a single line | |
785 | neighbor swp1 interface peer-group FOO | |
786 | ||
787 | This change confuses frr-reload.py so check to see if we are deleting | |
788 | neighbor swp1 interface peer-group FOO | |
789 | ||
790 | and adding | |
791 | neighbor swp1 interface | |
792 | neighbor swp1 peer-group FOO | |
793 | ||
794 | If so then chop the del line and the corresponding add lines | |
795 | ''' | |
796 | ||
797 | re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line) | |
798 | re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line) | |
799 | ||
800 | if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup: | |
801 | swpx_interface = None | |
802 | swpx_peergroup = None | |
803 | ||
804 | if re_swpx_int_peergroup: | |
805 | swpx = re_swpx_int_peergroup.group(1) | |
806 | peergroup = re_swpx_int_peergroup.group(2) | |
807 | swpx_interface = "neighbor %s interface" % swpx | |
808 | elif re_swpx_int_v6only_peergroup: | |
809 | swpx = re_swpx_int_v6only_peergroup.group(1) | |
810 | peergroup = re_swpx_int_v6only_peergroup.group(2) | |
811 | swpx_interface = "neighbor %s interface v6only" % swpx | |
812 | ||
813 | swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup) | |
814 | found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) | |
815 | found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup) | |
816 | tmp_ctx_keys = tuple(list(ctx_keys)) | |
9b166171 DW |
817 | |
818 | if not found_add_swpx_peergroup: | |
819 | tmp_ctx_keys = list(ctx_keys) | |
028bcc88 | 820 | tmp_ctx_keys.append('address-family ipv4 unicast') |
9b166171 DW |
821 | tmp_ctx_keys = tuple(tmp_ctx_keys) |
822 | found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) | |
9fe88bc7 | 823 | |
028bcc88 DW |
824 | if not found_add_swpx_peergroup: |
825 | tmp_ctx_keys = list(ctx_keys) | |
826 | tmp_ctx_keys.append('address-family ipv6 unicast') | |
827 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
828 | found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) | |
829 | ||
830 | if found_add_swpx_interface and found_add_swpx_peergroup: | |
831 | deleted = True | |
832 | lines_to_del_to_del.append((ctx_keys, line)) | |
833 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
834 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup)) | |
835 | ||
ee951352 DS |
836 | ''' |
837 | Changing the bfd timers on neighbors is allowed without doing | |
838 | a delete/add process. Since doing a "no neighbor blah bfd ..." | |
839 | will cause the peer to bounce unnecessarily, just skip the delete | |
840 | and just do the add. | |
841 | ''' | |
842 | re_nbr_bfd_timers = re.search(r'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line) | |
843 | ||
844 | if re_nbr_bfd_timers: | |
845 | nbr = re_nbr_bfd_timers.group(1) | |
846 | bfd_nbr = "neighbor %s" % nbr | |
deb2d401 | 847 | bfd_search_string = bfd_nbr + r' bfd (\S+) (\S+) (\S+)' |
ee951352 DS |
848 | |
849 | for (ctx_keys, add_line) in lines_to_add: | |
ca7f0496 DS |
850 | if ctx_keys[0].startswith('router bgp'): |
851 | re_add_nbr_bfd_timers = re.search(bfd_search_string, add_line) | |
ee951352 | 852 | |
ca7f0496 DS |
853 | if re_add_nbr_bfd_timers: |
854 | found_add_bfd_nbr = line_exist(lines_to_add, ctx_keys, bfd_nbr, False) | |
ee951352 | 855 | |
ca7f0496 DS |
856 | if found_add_bfd_nbr: |
857 | lines_to_del_to_del.append((ctx_keys, line)) | |
ee951352 | 858 | |
028bcc88 | 859 | ''' |
4c76e592 | 860 | We changed how we display the neighbor interface command. Older |
028bcc88 DW |
861 | versions of frr would display the following: |
862 | neighbor swp1 interface | |
863 | neighbor swp1 remote-as external | |
864 | neighbor swp1 capability extended-nexthop | |
865 | ||
866 | but today we display via a single line | |
867 | neighbor swp1 interface remote-as external | |
868 | ||
869 | and capability extended-nexthop is no longer needed because we | |
870 | automatically enable it when the neighbor is of type interface. | |
871 | ||
872 | This change confuses frr-reload.py so check to see if we are deleting | |
873 | neighbor swp1 interface remote-as (external|internal|ASNUM) | |
874 | ||
875 | and adding | |
876 | neighbor swp1 interface | |
877 | neighbor swp1 remote-as (external|internal|ASNUM) | |
878 | neighbor swp1 capability extended-nexthop | |
879 | ||
880 | If so then chop the del line and the corresponding add lines | |
881 | ''' | |
882 | re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line) | |
883 | re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line) | |
884 | ||
885 | if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas: | |
886 | swpx_interface = None | |
887 | swpx_remoteas = None | |
888 | ||
889 | if re_swpx_int_remoteas: | |
890 | swpx = re_swpx_int_remoteas.group(1) | |
891 | remoteas = re_swpx_int_remoteas.group(2) | |
892 | swpx_interface = "neighbor %s interface" % swpx | |
893 | elif re_swpx_int_v6only_remoteas: | |
894 | swpx = re_swpx_int_v6only_remoteas.group(1) | |
895 | remoteas = re_swpx_int_v6only_remoteas.group(2) | |
896 | swpx_interface = "neighbor %s interface v6only" % swpx | |
897 | ||
898 | swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas) | |
899 | found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) | |
900 | found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas) | |
901 | tmp_ctx_keys = tuple(list(ctx_keys)) | |
902 | ||
903 | if found_add_swpx_interface and found_add_swpx_remoteas: | |
904 | deleted = True | |
905 | lines_to_del_to_del.append((ctx_keys, line)) | |
906 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
907 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas)) | |
908 | ||
909 | ''' | |
4c76e592 DW |
910 | We made the 'bgp bestpath as-path multipath-relax' command |
911 | automatically assume 'no-as-set' since the lack of this option caused | |
912 | weird routing problems. When the running config is shown in | |
913 | releases with this change, the no-as-set keyword is not shown as it | |
914 | is the default. This causes frr-reload to unnecessarily unapply | |
915 | this option only to apply it back again, causing unnecessary session | |
916 | resets. | |
028bcc88 DW |
917 | ''' |
918 | if 'multipath-relax' in line: | |
919 | re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line) | |
920 | old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set' | |
921 | found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd) | |
922 | ||
923 | if re_asrelax_new and found_asrelax_old: | |
9b166171 | 924 | deleted = True |
9fe88bc7 | 925 | lines_to_del_to_del.append((ctx_keys, line)) |
028bcc88 DW |
926 | lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd)) |
927 | ||
928 | ''' | |
929 | If we are modifying the BGP table-map we need to avoid a del/add and | |
930 | instead modify the table-map in place via an add. This is needed to | |
931 | avoid installing all routes in the RIB the second the 'no table-map' | |
932 | is issued. | |
933 | ''' | |
934 | if line.startswith('table-map'): | |
935 | found_table_map = line_exist(lines_to_add, ctx_keys, 'table-map', False) | |
936 | ||
937 | if found_table_map: | |
b3a39dc5 | 938 | lines_to_del_to_del.append((ctx_keys, line)) |
c755f5c4 | 939 | |
78e31f46 | 940 | ''' |
028bcc88 DW |
941 | More old-to-new config handling. ip import-table no longer accepts |
942 | distance, but we honor the old syntax. But 'show running' shows only | |
943 | the new syntax. This causes an unnecessary 'no import-table' followed | |
944 | by the same old 'ip import-table' which causes perturbations in | |
945 | announced routes leading to traffic blackholes. Fix this issue. | |
78e31f46 DD |
946 | ''' |
947 | re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0]) | |
948 | if re_importtbl: | |
949 | table_num = re_importtbl.group(1) | |
950 | for ctx in lines_to_add: | |
951 | if ctx[0][0].startswith('ip import-table %s distance' % table_num): | |
78e31f46 DD |
952 | lines_to_del_to_del.append((('ip import-table %s' % table_num,), None)) |
953 | lines_to_add_to_del.append((ctx[0], None)) | |
0bf7cc28 DD |
954 | |
955 | ''' | |
956 | ip/ipv6 prefix-list can be specified without a seq number. However, | |
957 | the running config always adds 'seq x', where x is a number incremented | |
958 | by 5 for every element, to the prefix list. So, ignore such lines as | |
959 | well. Sample prefix-list lines: | |
960 | ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32 | |
961 | ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32 | |
962 | ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64 | |
963 | ''' | |
964 | re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$', | |
965 | ctx_keys[0]) | |
966 | if re_ip_pfxlst: | |
967 | tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) + | |
968 | re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) + | |
969 | re_ip_pfxlst.group(6)) | |
970 | for ctx in lines_to_add: | |
971 | if ctx[0][0] == tmpline: | |
972 | lines_to_del_to_del.append((ctx_keys, None)) | |
973 | lines_to_add_to_del.append(((tmpline,), None)) | |
974 | ||
5014d96f DW |
975 | if (len(ctx_keys) == 3 and |
976 | ctx_keys[0].startswith('router bgp') and | |
977 | ctx_keys[1] == 'address-family l2vpn evpn' and | |
978 | ctx_keys[2].startswith('vni')): | |
979 | ||
980 | re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False | |
981 | ||
982 | if re_route_target: | |
983 | rt = re_route_target.group(1).strip() | |
984 | route_target_import_line = line | |
985 | route_target_export_line = "route-target export %s" % rt | |
986 | route_target_both_line = "route-target both %s" % rt | |
987 | ||
988 | found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line) | |
989 | found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line) | |
990 | ||
991 | ''' | |
992 | If the running configs has | |
993 | route-target import 1:1 | |
994 | route-target export 1:1 | |
995 | ||
996 | and the config we are reloading against has | |
997 | route-target both 1:1 | |
998 | ||
999 | then we can ignore deleting the import/export and ignore adding the 'both' | |
1000 | ''' | |
1001 | if found_route_target_export_line and found_route_target_both_line: | |
1002 | lines_to_del_to_del.append((ctx_keys, route_target_import_line)) | |
1003 | lines_to_del_to_del.append((ctx_keys, route_target_export_line)) | |
1004 | lines_to_add_to_del.append((ctx_keys, route_target_both_line)) | |
1005 | ||
6024e562 DS |
1006 | # Deleting static routes under a vrf can lead to time-outs if each is sent |
1007 | # as separate vtysh -c commands. Change them from being in lines_to_del and | |
1008 | # put the "no" form in lines_to_add | |
1009 | if ctx_keys[0].startswith('vrf ') and line: | |
1010 | if (line.startswith('ip route') or | |
1011 | line.startswith('ipv6 route')): | |
1012 | add_cmd = ('no ' + line) | |
1013 | lines_to_add.append((ctx_keys, add_cmd)) | |
1014 | lines_to_del_to_del.append((ctx_keys, line)) | |
1015 | ||
9b166171 DW |
1016 | if not deleted: |
1017 | found_add_line = line_exist(lines_to_add, ctx_keys, line) | |
1018 | ||
1019 | if found_add_line: | |
1020 | lines_to_del_to_del.append((ctx_keys, line)) | |
1021 | lines_to_add_to_del.append((ctx_keys, line)) | |
1022 | else: | |
1023 | ''' | |
1024 | We have commands that used to be displayed in the global part | |
1025 | of 'router bgp' that are now displayed under 'address-family ipv4 unicast' | |
1026 | ||
1027 | # old way | |
1028 | router bgp 64900 | |
1029 | neighbor ISL advertisement-interval 0 | |
1030 | ||
1031 | vs. | |
1032 | ||
1033 | # new way | |
1034 | router bgp 64900 | |
1035 | address-family ipv4 unicast | |
1036 | neighbor ISL advertisement-interval 0 | |
1037 | ||
1038 | Look to see if we are deleting it in one format just to add it back in the other | |
1039 | ''' | |
1040 | if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast': | |
1041 | tmp_ctx_keys = list(ctx_keys)[:-1] | |
1042 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
1043 | ||
1044 | found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line) | |
1045 | ||
1046 | if found_add_line: | |
1047 | lines_to_del_to_del.append((ctx_keys, line)) | |
1048 | lines_to_add_to_del.append((tmp_ctx_keys, line)) | |
9fe88bc7 DW |
1049 | |
1050 | for (ctx_keys, line) in lines_to_del_to_del: | |
1051 | lines_to_del.remove((ctx_keys, line)) | |
1052 | ||
1053 | for (ctx_keys, line) in lines_to_add_to_del: | |
1054 | lines_to_add.remove((ctx_keys, line)) | |
1055 | ||
1056 | return (lines_to_add, lines_to_del) | |
1057 | ||
1058 | ||
b05a1d3c DW |
1059 | def ignore_unconfigurable_lines(lines_to_add, lines_to_del): |
1060 | """ | |
1061 | There are certain commands that cannot be removed. Remove | |
1062 | those commands from lines_to_del. | |
1063 | """ | |
1064 | lines_to_del_to_del = [] | |
1065 | ||
1066 | for (ctx_keys, line) in lines_to_del: | |
1067 | ||
1068 | if (ctx_keys[0].startswith('frr version') or | |
1069 | ctx_keys[0].startswith('frr defaults') or | |
663ece2f | 1070 | ctx_keys[0].startswith('username') or |
b05a1d3c DW |
1071 | ctx_keys[0].startswith('password') or |
1072 | ctx_keys[0].startswith('line vty') or | |
1073 | ||
1074 | # This is technically "no"able but if we did so frr-reload would | |
1075 | # stop working so do not let the user shoot themselves in the foot | |
1076 | # by removing this. | |
1077 | ctx_keys[0].startswith('service integrated-vtysh-config')): | |
1078 | ||
663ece2f | 1079 | log.info('"%s" cannot be removed' % (ctx_keys[-1],)) |
b05a1d3c DW |
1080 | lines_to_del_to_del.append((ctx_keys, line)) |
1081 | ||
1082 | for (ctx_keys, line) in lines_to_del_to_del: | |
1083 | lines_to_del.remove((ctx_keys, line)) | |
1084 | ||
1085 | return (lines_to_add, lines_to_del) | |
1086 | ||
1087 | ||
2fc76430 DS |
1088 | def compare_context_objects(newconf, running): |
1089 | """ | |
1090 | Create a context diff for the two specified contexts | |
1091 | """ | |
1092 | ||
1093 | # Compare the two Config objects to find the lines that we need to add/del | |
1094 | lines_to_add = [] | |
1095 | lines_to_del = [] | |
926ea62e | 1096 | delete_bgpd = False |
2fc76430 DS |
1097 | |
1098 | # Find contexts that are in newconf but not in running | |
1099 | # Find contexts that are in running but not in newconf | |
1c64265f | 1100 | for (running_ctx_keys, running_ctx) in iteritems(running.contexts): |
2fc76430 DS |
1101 | |
1102 | if running_ctx_keys not in newconf.contexts: | |
1103 | ||
ab5f8310 DW |
1104 | # We check that the len is 1 here so that we only look at ('router bgp 10') |
1105 | # and not ('router bgp 10', 'address-family ipv4 unicast'). The | |
926ea62e | 1106 | # latter could cause a false delete_bgpd positive if ipv4 unicast is in |
ab5f8310 DW |
1107 | # running but not in newconf. |
1108 | if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1: | |
926ea62e DW |
1109 | delete_bgpd = True |
1110 | lines_to_del.append((running_ctx_keys, None)) | |
1111 | ||
2a2b64e4 DS |
1112 | # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it |
1113 | elif running_ctx_keys[0].startswith('interface') or running_ctx_keys[0].startswith('vrf'): | |
768bf950 DD |
1114 | for line in running_ctx.lines: |
1115 | lines_to_del.append((running_ctx_keys, line)) | |
1116 | ||
926ea62e DW |
1117 | # If this is an address-family under 'router bgp' and we are already deleting the |
1118 | # entire 'router bgp' context then ignore this sub-context | |
1119 | elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd: | |
76f69d1c | 1120 | continue |
514665b9 | 1121 | |
5014d96f DW |
1122 | # Delete an entire vni sub-context under "address-family l2vpn evpn" |
1123 | elif ("router bgp" in running_ctx_keys[0] and | |
1124 | len(running_ctx_keys) > 2 and | |
1125 | running_ctx_keys[1].startswith('address-family l2vpn evpn') and | |
1126 | running_ctx_keys[2].startswith('vni ')): | |
1127 | lines_to_del.append((running_ctx_keys, None)) | |
1128 | ||
afa2e8e1 DW |
1129 | elif ("router bgp" in running_ctx_keys[0] and |
1130 | len(running_ctx_keys) > 1 and | |
1131 | running_ctx_keys[1].startswith('address-family')): | |
1132 | # There's no 'no address-family' support and so we have to | |
1133 | # delete each line individually again | |
1134 | for line in running_ctx.lines: | |
1135 | lines_to_del.append((running_ctx_keys, line)) | |
1136 | ||
6024e562 DS |
1137 | # Some commands can happen at higher counts that make |
1138 | # doing vtysh -c inefficient (and can time out.) For | |
1139 | # these commands, instead of adding them to lines_to_del, | |
1140 | # add the "no " version to lines_to_add. | |
1141 | elif (running_ctx_keys[0].startswith('ip route') or | |
1142 | running_ctx_keys[0].startswith('ipv6 route') or | |
1143 | running_ctx_keys[0].startswith('access-list') or | |
1144 | running_ctx_keys[0].startswith('ipv6 access-list') or | |
1145 | running_ctx_keys[0].startswith('ip prefix-list') or | |
1146 | running_ctx_keys[0].startswith('ipv6 prefix-list')): | |
1147 | add_cmd = ('no ' + running_ctx_keys[0],) | |
1148 | lines_to_add.append((add_cmd, None)) | |
1149 | ||
e04ff92e EDP |
1150 | # if this an interface sub-subcontext in an address-family block in ldpd and |
1151 | # we are already deleting the whole context, then ignore this | |
1152 | elif (len(running_ctx_keys) > 2 and running_ctx_keys[0].startswith('mpls ldp') and | |
1153 | running_ctx_keys[1].startswith('address-family') and | |
1154 | (running_ctx_keys[:2], None) in lines_to_del): | |
1155 | continue | |
1156 | ||
2fc76430 | 1157 | # Non-global context |
926ea62e | 1158 | elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys): |
2fc76430 DS |
1159 | lines_to_del.append((running_ctx_keys, None)) |
1160 | ||
7918b335 DW |
1161 | elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys): |
1162 | lines_to_del.append((running_ctx_keys, None)) | |
1163 | ||
2fc76430 DS |
1164 | # Global context |
1165 | else: | |
1166 | for line in running_ctx.lines: | |
1167 | lines_to_del.append((running_ctx_keys, line)) | |
1168 | ||
1169 | # Find the lines within each context to add | |
1170 | # Find the lines within each context to del | |
1c64265f | 1171 | for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts): |
2fc76430 DS |
1172 | |
1173 | if newconf_ctx_keys in running.contexts: | |
1174 | running_ctx = running.contexts[newconf_ctx_keys] | |
1175 | ||
1176 | for line in newconf_ctx.lines: | |
1177 | if line not in running_ctx.dlines: | |
1178 | lines_to_add.append((newconf_ctx_keys, line)) | |
1179 | ||
1180 | for line in running_ctx.lines: | |
1181 | if line not in newconf_ctx.dlines: | |
1182 | lines_to_del.append((newconf_ctx_keys, line)) | |
1183 | ||
1c64265f | 1184 | for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts): |
2fc76430 DS |
1185 | |
1186 | if newconf_ctx_keys not in running.contexts: | |
1187 | lines_to_add.append((newconf_ctx_keys, None)) | |
1188 | ||
1189 | for line in newconf_ctx.lines: | |
1190 | lines_to_add.append((newconf_ctx_keys, line)) | |
1191 | ||
eb9113df | 1192 | (lines_to_add, lines_to_del) = check_for_exit_vrf(lines_to_add, lines_to_del) |
9b166171 | 1193 | (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del) |
b05a1d3c | 1194 | (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del) |
9fe88bc7 | 1195 | |
926ea62e | 1196 | return (lines_to_add, lines_to_del) |
2fc76430 | 1197 | |
8ad1fe6c | 1198 | |
2fc76430 DS |
1199 | if __name__ == '__main__': |
1200 | # Command line options | |
d8e4c438 | 1201 | parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs') |
2fc76430 DS |
1202 | parser.add_argument('--input', help='Read running config from file instead of "show running"') |
1203 | group = parser.add_mutually_exclusive_group(required=True) | |
1204 | group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False) | |
1205 | group.add_argument('--test', action='store_true', help='Show the deltas', default=False) | |
1206 | parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False) | |
cc146ecc | 1207 | parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False) |
a0a7dead | 1208 | parser.add_argument('--pathspace', '-N', metavar='NAME', help='Reload specified path/namespace', default=None) |
d8e4c438 | 1209 | parser.add_argument('filename', help='Location of new frr config file') |
e20dc2ba | 1210 | parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False) |
1a11d9cd EDP |
1211 | parser.add_argument('--bindir', help='path to the vtysh executable', default='/usr/bin') |
1212 | parser.add_argument('--confdir', help='path to the daemon config files', default='/etc/frr') | |
1213 | parser.add_argument('--rundir', help='path for the temp config file', default='/var/run/frr') | |
fa18c6bb | 1214 | parser.add_argument('--vty_socket', help='socket to be used by vtysh to connect to the daemons', default=None) |
d9730542 EDP |
1215 | parser.add_argument('--daemon', help='daemon for which want to replace the config', default='') |
1216 | ||
2fc76430 DS |
1217 | args = parser.parse_args() |
1218 | ||
1219 | # Logging | |
1220 | # For --test log to stdout | |
d8e4c438 | 1221 | # For --reload log to /var/log/frr/frr-reload.log |
cc146ecc | 1222 | if args.test or args.stdout: |
c50aceee | 1223 | logging.basicConfig(level=logging.INFO, |
2fc76430 | 1224 | format='%(asctime)s %(levelname)5s: %(message)s') |
926ea62e DW |
1225 | |
1226 | # Color the errors and warnings in red | |
1227 | logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR)) | |
1228 | logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING)) | |
1229 | ||
2fc76430 | 1230 | elif args.reload: |
d8e4c438 DS |
1231 | if not os.path.isdir('/var/log/frr/'): |
1232 | os.makedirs('/var/log/frr/') | |
2fc76430 | 1233 | |
d8e4c438 | 1234 | logging.basicConfig(filename='/var/log/frr/frr-reload.log', |
c50aceee | 1235 | level=logging.INFO, |
2fc76430 DS |
1236 | format='%(asctime)s %(levelname)5s: %(message)s') |
1237 | ||
1238 | # argparse should prevent this from happening but just to be safe... | |
1239 | else: | |
1240 | raise Exception('Must specify --reload or --test') | |
a782e613 | 1241 | log = logging.getLogger(__name__) |
2fc76430 | 1242 | |
76f69d1c DW |
1243 | # Verify the new config file is valid |
1244 | if not os.path.isfile(args.filename): | |
825be4c2 | 1245 | msg = "Filename %s does not exist" % args.filename |
1c64265f | 1246 | print(msg) |
825be4c2 | 1247 | log.error(msg) |
76f69d1c DW |
1248 | sys.exit(1) |
1249 | ||
1250 | if not os.path.getsize(args.filename): | |
825be4c2 | 1251 | msg = "Filename %s is an empty file" % args.filename |
1c64265f | 1252 | print(msg) |
825be4c2 | 1253 | log.error(msg) |
76f69d1c DW |
1254 | sys.exit(1) |
1255 | ||
1a11d9cd EDP |
1256 | # Verify that confdir is correct |
1257 | if not os.path.isdir(args.confdir): | |
1258 | msg = "Confdir %s is not a valid path" % args.confdir | |
1259 | print(msg) | |
1260 | log.error(msg) | |
1261 | sys.exit(1) | |
1262 | ||
1263 | # Verify that bindir is correct | |
1264 | if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + '/vtysh'): | |
1265 | msg = "Bindir %s is not a valid path to vtysh" % args.bindir | |
1266 | print(msg) | |
1267 | log.error(msg) | |
1268 | sys.exit(1) | |
1269 | ||
fa18c6bb DL |
1270 | # verify that the vty_socket, if specified, is valid |
1271 | if args.vty_socket and not os.path.isdir(args.vty_socket): | |
1272 | msg = 'vty_socket %s is not a valid path' % args.vty_socket | |
1273 | print(msg) | |
1274 | log.error(msg) | |
1275 | sys.exit(1) | |
1276 | ||
d9730542 EDP |
1277 | # verify that the daemon, if specified, is valid |
1278 | if args.daemon and args.daemon not in ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']: | |
1279 | msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon | |
1280 | print(msg) | |
1281 | log.error(msg) | |
1282 | sys.exit(1) | |
1283 | ||
a0a7dead | 1284 | vtysh = Vtysh(args.bindir, args.confdir, args.vty_socket, args.pathspace) |
663ece2f | 1285 | |
76f69d1c | 1286 | # Verify that 'service integrated-vtysh-config' is configured |
a0a7dead DL |
1287 | if args.pathspace: |
1288 | vtysh_filename = args.confdir + '/' + args.pathspace + '/vtysh.conf' | |
1289 | else: | |
1290 | vtysh_filename = args.confdir + '/vtysh.conf' | |
6ac9179c | 1291 | service_integrated_vtysh_config = True |
76f69d1c | 1292 | |
f850d14d DW |
1293 | if os.path.isfile(vtysh_filename): |
1294 | with open(vtysh_filename, 'r') as fh: | |
1295 | for line in fh.readlines(): | |
1296 | line = line.strip() | |
76f69d1c | 1297 | |
6ac9179c DD |
1298 | if line == 'no service integrated-vtysh-config': |
1299 | service_integrated_vtysh_config = False | |
f850d14d | 1300 | break |
76f69d1c | 1301 | |
d9730542 | 1302 | if not service_integrated_vtysh_config and not args.daemon: |
825be4c2 | 1303 | msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'" |
1c64265f | 1304 | print(msg) |
825be4c2 | 1305 | log.error(msg) |
76f69d1c | 1306 | sys.exit(1) |
2fc76430 | 1307 | |
c50aceee | 1308 | if args.debug: |
a782e613 | 1309 | log.setLevel(logging.DEBUG) |
c50aceee | 1310 | |
a782e613 | 1311 | log.info('Called via "%s"', str(args)) |
c50aceee | 1312 | |
2fc76430 | 1313 | # Create a Config object from the config generated by newconf |
663ece2f DL |
1314 | newconf = Config(vtysh) |
1315 | newconf.load_from_file(args.filename) | |
825be4c2 | 1316 | reload_ok = True |
2fc76430 DS |
1317 | |
1318 | if args.test: | |
1319 | ||
1320 | # Create a Config object from the running config | |
663ece2f | 1321 | running = Config(vtysh) |
2fc76430 DS |
1322 | |
1323 | if args.input: | |
663ece2f | 1324 | running.load_from_file(args.input) |
2fc76430 | 1325 | else: |
663ece2f | 1326 | running.load_from_show_running(args.daemon) |
2fc76430 | 1327 | |
926ea62e | 1328 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
4a2587c6 | 1329 | lines_to_configure = [] |
2fc76430 DS |
1330 | |
1331 | if lines_to_del: | |
1c64265f | 1332 | print("\nLines To Delete") |
1333 | print("===============") | |
2fc76430 DS |
1334 | |
1335 | for (ctx_keys, line) in lines_to_del: | |
1336 | ||
1337 | if line == '!': | |
1338 | continue | |
1339 | ||
663ece2f | 1340 | cmd = '\n'.join(lines_to_config(ctx_keys, line, True)) |
4a2587c6 | 1341 | lines_to_configure.append(cmd) |
1c64265f | 1342 | print(cmd) |
2fc76430 DS |
1343 | |
1344 | if lines_to_add: | |
1c64265f | 1345 | print("\nLines To Add") |
1346 | print("============") | |
2fc76430 DS |
1347 | |
1348 | for (ctx_keys, line) in lines_to_add: | |
1349 | ||
1350 | if line == '!': | |
1351 | continue | |
1352 | ||
663ece2f | 1353 | cmd = '\n'.join(lines_to_config(ctx_keys, line, False)) |
4a2587c6 | 1354 | lines_to_configure.append(cmd) |
1c64265f | 1355 | print(cmd) |
2fc76430 | 1356 | |
2fc76430 DS |
1357 | elif args.reload: |
1358 | ||
2f52ad96 | 1359 | # We will not be able to do anything, go ahead and exit(1) |
663ece2f | 1360 | if not vtysh.is_config_available(): |
2f52ad96 DW |
1361 | sys.exit(1) |
1362 | ||
d8e4c438 | 1363 | log.debug('New Frr Config\n%s', newconf.get_lines()) |
2fc76430 DS |
1364 | |
1365 | # This looks a little odd but we have to do this twice...here is why | |
1366 | # If the user had this running bgp config: | |
4a2587c6 | 1367 | # |
2fc76430 DS |
1368 | # router bgp 10 |
1369 | # neighbor 1.1.1.1 remote-as 50 | |
1370 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 | 1371 | # |
2fc76430 | 1372 | # and this config in the newconf config file |
4a2587c6 | 1373 | # |
2fc76430 DS |
1374 | # router bgp 10 |
1375 | # neighbor 1.1.1.1 remote-as 999 | |
1376 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 DW |
1377 | # |
1378 | # | |
2fc76430 DS |
1379 | # Then the script will do |
1380 | # - no neighbor 1.1.1.1 remote-as 50 | |
1381 | # - neighbor 1.1.1.1 remote-as 999 | |
4a2587c6 | 1382 | # |
2fc76430 DS |
1383 | # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove |
1384 | # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the | |
1385 | # configs again to put this line back. | |
1386 | ||
1a8c43f1 | 1387 | # There are many keywords in FRR that can only appear one time under |
2ce26af1 DW |
1388 | # a context, take "bgp router-id" for example. If the config that we are |
1389 | # reloading against has the following: | |
1390 | # | |
1391 | # router bgp 10 | |
1392 | # bgp router-id 1.1.1.1 | |
1393 | # bgp router-id 2.2.2.2 | |
1394 | # | |
1395 | # The final config needs to contain "bgp router-id 2.2.2.2". On the | |
1396 | # first pass we will add "bgp router-id 2.2.2.2" but then on the second | |
1397 | # pass we will see that "bgp router-id 1.1.1.1" is missing and add that | |
1398 | # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the | |
1399 | # second pass to include all of the "adds" from the first pass. | |
1400 | lines_to_add_first_pass = [] | |
1401 | ||
2fc76430 | 1402 | for x in range(2): |
663ece2f DL |
1403 | running = Config(vtysh) |
1404 | running.load_from_show_running(args.daemon) | |
d8e4c438 | 1405 | log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines()) |
2fc76430 | 1406 | |
926ea62e | 1407 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
2fc76430 | 1408 | |
2ce26af1 DW |
1409 | if x == 0: |
1410 | lines_to_add_first_pass = lines_to_add | |
1411 | else: | |
1412 | lines_to_add.extend(lines_to_add_first_pass) | |
1413 | ||
53bddc22 | 1414 | # Only do deletes on the first pass. The reason being if we |
1a8c43f1 | 1415 | # configure a bgp neighbor via "neighbor swp1 interface" FRR |
53bddc22 DW |
1416 | # will automatically add: |
1417 | # | |
1418 | # interface swp1 | |
1419 | # ipv6 nd ra-interval 10 | |
1420 | # no ipv6 nd suppress-ra | |
1421 | # ! | |
1422 | # | |
1423 | # but those lines aren't in the config we are reloading against so | |
1424 | # on the 2nd pass they will show up in lines_to_del. This could | |
1425 | # apply to other scenarios as well where configuring FOO adds BAR | |
1426 | # to the config. | |
1427 | if lines_to_del and x == 0: | |
2fc76430 DS |
1428 | for (ctx_keys, line) in lines_to_del: |
1429 | ||
1430 | if line == '!': | |
1431 | continue | |
1432 | ||
4a2587c6 DW |
1433 | # 'no' commands are tricky, we can't just put them in a file and |
1434 | # vtysh -f that file. See the next comment for an explanation | |
1435 | # of their quirks | |
663ece2f | 1436 | cmd = lines_to_config(ctx_keys, line, True) |
2fc76430 DS |
1437 | original_cmd = cmd |
1438 | ||
d8e4c438 | 1439 | # Some commands in frr are picky about taking a "no" of the entire line. |
76f69d1c DW |
1440 | # OSPF is bad about this, you can't "no" the entire line, you have to "no" |
1441 | # only the beginning. If we hit one of these command an exception will be | |
1442 | # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again. | |
4a2587c6 | 1443 | # |
76f69d1c | 1444 | # Example: |
d8e4c438 DS |
1445 | # frr(config-if)# ip ospf authentication message-digest 1.1.1.1 |
1446 | # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1 | |
76f69d1c | 1447 | # % Unknown command. |
d8e4c438 | 1448 | # frr(config-if)# no ip ospf authentication message-digest |
76f69d1c | 1449 | # % Unknown command. |
d8e4c438 DS |
1450 | # frr(config-if)# no ip ospf authentication |
1451 | # frr(config-if)# | |
2fc76430 DS |
1452 | |
1453 | while True: | |
2fc76430 | 1454 | try: |
663ece2f | 1455 | vtysh(['configure'] + cmd) |
2fc76430 | 1456 | |
663ece2f | 1457 | except VtyshException: |
2fc76430 DS |
1458 | |
1459 | # - Pull the last entry from cmd (this would be | |
1460 | # 'no ip ospf authentication message-digest 1.1.1.1' in | |
1461 | # our example above | |
1462 | # - Split that last entry by whitespace and drop the last word | |
825be4c2 | 1463 | log.info('Failed to execute %s', ' '.join(cmd)) |
2fc76430 DS |
1464 | last_arg = cmd[-1].split(' ') |
1465 | ||
1466 | if len(last_arg) <= 2: | |
663ece2f | 1467 | log.error('"%s" we failed to remove this command', ' -- '.join(original_cmd)) |
2fc76430 DS |
1468 | break |
1469 | ||
1470 | new_last_arg = last_arg[0:-1] | |
1471 | cmd[-1] = ' '.join(new_last_arg) | |
1472 | else: | |
a782e613 | 1473 | log.info('Executed "%s"', ' '.join(cmd)) |
2fc76430 DS |
1474 | break |
1475 | ||
2fc76430 | 1476 | if lines_to_add: |
4a2587c6 DW |
1477 | lines_to_configure = [] |
1478 | ||
2fc76430 DS |
1479 | for (ctx_keys, line) in lines_to_add: |
1480 | ||
1481 | if line == '!': | |
1482 | continue | |
1483 | ||
6024e562 DS |
1484 | # Don't run "no" commands twice since they can error |
1485 | # out the second time due to first deletion | |
1486 | if x == 1 and ctx_keys[0].startswith('no '): | |
1487 | continue | |
1488 | ||
663ece2f | 1489 | cmd = '\n'.join(lines_to_config(ctx_keys, line, False)) + '\n' |
4a2587c6 DW |
1490 | lines_to_configure.append(cmd) |
1491 | ||
1492 | if lines_to_configure: | |
1493 | random_string = ''.join(random.SystemRandom().choice( | |
1494 | string.ascii_uppercase + | |
1495 | string.digits) for _ in range(6)) | |
1496 | ||
1a11d9cd | 1497 | filename = args.rundir + "/reload-%s.txt" % random_string |
a782e613 | 1498 | log.info("%s content\n%s" % (filename, pformat(lines_to_configure))) |
4a2587c6 DW |
1499 | |
1500 | with open(filename, 'w') as fh: | |
1501 | for line in lines_to_configure: | |
1502 | fh.write(line + '\n') | |
825be4c2 | 1503 | |
596074af | 1504 | try: |
663ece2f DL |
1505 | vtysh.exec_file(filename) |
1506 | except VtyshException as e: | |
1507 | log.warning("frr-reload.py failed due to\n%s" % e.args) | |
596074af | 1508 | reload_ok = False |
4a2587c6 | 1509 | os.unlink(filename) |
2fc76430 | 1510 | |
4b78098d | 1511 | # Make these changes persistent |
d9730542 EDP |
1512 | target = str(args.confdir + '/frr.conf') |
1513 | if args.overwrite or (not args.daemon and args.filename != target): | |
663ece2f | 1514 | vtysh('write') |
825be4c2 DW |
1515 | |
1516 | if not reload_ok: | |
1517 | sys.exit(1) |