]>
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 | |
701a0192 | 42 | |
1c64265f | 43 | try: |
44 | from ipaddress import IPv6Address, ip_network | |
45 | except ImportError: | |
46 | from ipaddr import IPv6Address, IPNetwork | |
4a2587c6 DW |
47 | from pprint import pformat |
48 | ||
1c64265f | 49 | try: |
50 | dict.iteritems | |
51 | except AttributeError: | |
52 | # Python 3 | |
53 | def iteritems(d): | |
54 | return iter(d.items()) | |
701a0192 | 55 | |
56 | ||
1c64265f | 57 | else: |
58 | # Python 2 | |
59 | def iteritems(d): | |
60 | return d.iteritems() | |
2fc76430 | 61 | |
701a0192 | 62 | |
a782e613 DW |
63 | log = logging.getLogger(__name__) |
64 | ||
65 | ||
663ece2f | 66 | class VtyshException(Exception): |
276887bb DW |
67 | pass |
68 | ||
701a0192 | 69 | |
663ece2f | 70 | class Vtysh(object): |
a0a7dead | 71 | def __init__(self, bindir=None, confdir=None, sockdir=None, pathspace=None): |
663ece2f DL |
72 | self.bindir = bindir |
73 | self.confdir = confdir | |
a0a7dead | 74 | self.pathspace = pathspace |
701a0192 | 75 | self.common_args = [os.path.join(bindir or "", "vtysh")] |
663ece2f | 76 | if confdir: |
701a0192 | 77 | self.common_args.extend(["--config_dir", confdir]) |
fa18c6bb | 78 | if sockdir: |
701a0192 | 79 | self.common_args.extend(["--vty_socket", sockdir]) |
a0a7dead | 80 | if pathspace: |
701a0192 | 81 | self.common_args.extend(["-N", pathspace]) |
663ece2f DL |
82 | |
83 | def _call(self, args, stdin=None, stdout=None, stderr=None): | |
84 | kwargs = {} | |
85 | if stdin is not None: | |
701a0192 | 86 | kwargs["stdin"] = stdin |
663ece2f | 87 | if stdout is not None: |
701a0192 | 88 | kwargs["stdout"] = stdout |
663ece2f | 89 | if stderr is not None: |
701a0192 | 90 | kwargs["stderr"] = stderr |
663ece2f DL |
91 | return subprocess.Popen(self.common_args + args, **kwargs) |
92 | ||
93 | def _call_cmd(self, command, stdin=None, stdout=None, stderr=None): | |
94 | if isinstance(command, list): | |
701a0192 | 95 | args = [item for sub in command for item in ["-c", sub]] |
663ece2f | 96 | else: |
701a0192 | 97 | args = ["-c", command] |
663ece2f DL |
98 | return self._call(args, stdin, stdout, stderr) |
99 | ||
641b032e | 100 | def __call__(self, command, stdouts=None): |
663ece2f DL |
101 | """ |
102 | Call a CLI command (e.g. "show running-config") | |
103 | ||
104 | Output text is automatically redirected, decoded and returned. | |
105 | Multiple commands may be passed as list. | |
106 | """ | |
107 | proc = self._call_cmd(command, stdout=subprocess.PIPE) | |
108 | stdout, stderr = proc.communicate() | |
109 | if proc.wait() != 0: | |
641b032e | 110 | if stdouts is not None: |
880dcb06 | 111 | stdouts.append(stdout.decode("UTF-8")) |
701a0192 | 112 | raise VtyshException( |
113 | 'vtysh returned status %d for command "%s"' % (proc.returncode, command) | |
114 | ) | |
115 | return stdout.decode("UTF-8") | |
663ece2f DL |
116 | |
117 | def is_config_available(self): | |
118 | """ | |
119 | Return False if no frr daemon is running or some other vtysh session is | |
120 | in 'configuration terminal' mode which will prevent us from making any | |
121 | configuration changes. | |
122 | """ | |
123 | ||
701a0192 | 124 | output = self("configure") |
663ece2f | 125 | |
701a0192 | 126 | if "VTY configuration is locked by other VTY" in output: |
663ece2f DL |
127 | log.error("vtysh 'configure' returned\n%s\n" % (output)) |
128 | return False | |
129 | ||
130 | return True | |
131 | ||
132 | def exec_file(self, filename): | |
701a0192 | 133 | child = self._call(["-f", filename]) |
663ece2f | 134 | if child.wait() != 0: |
701a0192 | 135 | raise VtyshException( |
136 | "vtysh (exec file) exited with status %d" % (child.returncode) | |
137 | ) | |
663ece2f DL |
138 | |
139 | def mark_file(self, filename, stdin=None): | |
701a0192 | 140 | child = self._call( |
141 | ["-m", "-f", filename], | |
142 | stdout=subprocess.PIPE, | |
143 | stdin=subprocess.PIPE, | |
144 | stderr=subprocess.PIPE, | |
145 | ) | |
663ece2f DL |
146 | try: |
147 | stdout, stderr = child.communicate() | |
148 | except subprocess.TimeoutExpired: | |
149 | child.kill() | |
fdaee098 | 150 | stdout, stderr = child.communicate() |
701a0192 | 151 | raise VtyshException("vtysh call timed out!") |
663ece2f DL |
152 | |
153 | if child.wait() != 0: | |
701a0192 | 154 | raise VtyshException( |
155 | "vtysh (mark file) exited with status %d:\n%s" | |
156 | % (child.returncode, stderr) | |
157 | ) | |
663ece2f | 158 | |
701a0192 | 159 | return stdout.decode("UTF-8") |
663ece2f | 160 | |
701a0192 | 161 | def mark_show_run(self, daemon=None): |
162 | cmd = "show running-config" | |
663ece2f | 163 | if daemon: |
701a0192 | 164 | cmd += " %s" % daemon |
165 | cmd += " no-header" | |
663ece2f | 166 | show_run = self._call_cmd(cmd, stdout=subprocess.PIPE) |
701a0192 | 167 | mark = self._call( |
168 | ["-m", "-f", "-"], stdin=show_run.stdout, stdout=subprocess.PIPE | |
169 | ) | |
663ece2f DL |
170 | |
171 | show_run.wait() | |
172 | stdout, stderr = mark.communicate() | |
173 | mark.wait() | |
174 | ||
175 | if show_run.returncode != 0: | |
701a0192 | 176 | raise VtyshException( |
177 | "vtysh (show running-config) exited with status %d:" | |
178 | % (show_run.returncode) | |
179 | ) | |
663ece2f | 180 | if mark.returncode != 0: |
701a0192 | 181 | raise VtyshException( |
182 | "vtysh (mark running-config) exited with status %d" % (mark.returncode) | |
183 | ) | |
184 | ||
185 | return stdout.decode("UTF-8") | |
663ece2f | 186 | |
276887bb | 187 | |
2fc76430 | 188 | class Context(object): |
4a2587c6 | 189 | |
2fc76430 | 190 | """ |
61dc17d8 DS |
191 | A Context object represents a section of frr configuration such as: |
192 | ! | |
193 | interface swp3 | |
194 | description swp3 -> r8's swp1 | |
195 | ipv6 nd suppress-ra | |
196 | link-detect | |
197 | ! | |
2fc76430 | 198 | |
61dc17d8 | 199 | or a single line context object such as this: |
2fc76430 | 200 | |
61dc17d8 | 201 | ip forwarding |
2fc76430 DS |
202 | |
203 | """ | |
204 | ||
205 | def __init__(self, keys, lines): | |
206 | self.keys = keys | |
207 | self.lines = lines | |
208 | ||
209 | # Keep a dictionary of the lines, this is to make it easy to tell if a | |
210 | # line exists in this Context | |
211 | self.dlines = OrderedDict() | |
212 | ||
213 | for ligne in lines: | |
214 | self.dlines[ligne] = True | |
215 | ||
216 | def add_lines(self, lines): | |
217 | """ | |
218 | Add lines to specified context | |
219 | """ | |
220 | ||
221 | self.lines.extend(lines) | |
222 | ||
223 | for ligne in lines: | |
224 | self.dlines[ligne] = True | |
225 | ||
d82c5d61 | 226 | |
8a63e80c AK |
227 | def get_normalized_es_id(line): |
228 | """ | |
229 | The es-id or es-sys-mac need to be converted to lower case | |
230 | """ | |
231 | sub_strs = ["evpn mh es-id", "evpn mh es-sys-mac"] | |
232 | for sub_str in sub_strs: | |
233 | obj = re.match(sub_str + " (?P<esi>\S*)", line) | |
234 | if obj: | |
235 | line = "%s %s" % (sub_str, obj.group("esi").lower()) | |
236 | break | |
237 | return line | |
238 | ||
d82c5d61 | 239 | |
8a63e80c AK |
240 | def get_normalized_mac_ip_line(line): |
241 | if line.startswith("evpn mh es"): | |
242 | return get_normalized_es_id(line) | |
243 | ||
244 | if not "ipv6 add" in line: | |
245 | return get_normalized_ipv6_line(line) | |
246 | ||
247 | return line | |
2fc76430 | 248 | |
d82c5d61 | 249 | |
2fc76430 | 250 | class Config(object): |
4a2587c6 | 251 | |
2fc76430 | 252 | """ |
d8e4c438 | 253 | A frr configuration is stored in a Config object. A Config object |
2fc76430 DS |
254 | contains a dictionary of Context objects where the Context keys |
255 | ('router ospf' for example) are our dictionary key. | |
256 | """ | |
257 | ||
663ece2f | 258 | def __init__(self, vtysh): |
2fc76430 DS |
259 | self.lines = [] |
260 | self.contexts = OrderedDict() | |
663ece2f | 261 | self.vtysh = vtysh |
2fc76430 | 262 | |
663ece2f | 263 | def load_from_file(self, filename): |
2fc76430 DS |
264 | """ |
265 | Read configuration from specified file and slurp it into internal memory | |
266 | The internal representation has been marked appropriately by passing it | |
267 | through vtysh with the -m parameter | |
268 | """ | |
701a0192 | 269 | log.info("Loading Config object from file %s", filename) |
2fc76430 | 270 | |
663ece2f DL |
271 | file_output = self.vtysh.mark_file(filename) |
272 | ||
701a0192 | 273 | for line in file_output.split("\n"): |
2fc76430 | 274 | line = line.strip() |
89cca49b DW |
275 | |
276 | # Compress duplicate whitespaces | |
701a0192 | 277 | line = " ".join(line.split()) |
89cca49b | 278 | |
8a63e80c AK |
279 | if ":" in line: |
280 | line = get_normalized_mac_ip_line(line) | |
281 | ||
00302a58 DS |
282 | """ |
283 | vrf static routes can be added in two ways. The old way is: | |
284 | ||
285 | "ip route x.x.x.x/x y.y.y.y vrf <vrfname>" | |
286 | ||
287 | but it's rendered in the configuration as the new way:: | |
288 | ||
289 | vrf <vrf-name> | |
290 | ip route x.x.x.x/x y.y.y.y | |
291 | exit-vrf | |
292 | ||
293 | this difference causes frr-reload to not consider them a | |
294 | match and delete vrf static routes incorrectly. | |
295 | fix the old way to match new "show running" output so a | |
296 | proper match is found. | |
297 | """ | |
298 | if ( | |
299 | line.startswith("ip route ") or line.startswith("ipv6 route ") | |
300 | ) and " vrf " in line: | |
301 | newline = line.split(" ") | |
302 | vrf_index = newline.index("vrf") | |
303 | vrf_ctx = newline[vrf_index] + " " + newline[vrf_index + 1] | |
304 | del newline[vrf_index : vrf_index + 2] | |
305 | newline = " ".join(newline) | |
306 | self.lines.append(vrf_ctx) | |
307 | self.lines.append(newline) | |
308 | self.lines.append("exit-vrf") | |
309 | line = "end" | |
310 | ||
8a63e80c | 311 | self.lines.append(line) |
2fc76430 DS |
312 | |
313 | self.load_contexts() | |
314 | ||
663ece2f | 315 | def load_from_show_running(self, daemon): |
2fc76430 DS |
316 | """ |
317 | Read running configuration and slurp it into internal memory | |
318 | The internal representation has been marked appropriately by passing it | |
319 | through vtysh with the -m parameter | |
320 | """ | |
701a0192 | 321 | log.info("Loading Config object from vtysh show running") |
2fc76430 | 322 | |
663ece2f DL |
323 | config_text = self.vtysh.mark_show_run(daemon) |
324 | ||
701a0192 | 325 | for line in config_text.split("\n"): |
2fc76430 DS |
326 | line = line.strip() |
327 | ||
701a0192 | 328 | if ( |
329 | line == "Building configuration..." | |
330 | or line == "Current configuration:" | |
331 | or not line | |
332 | ): | |
2fc76430 DS |
333 | continue |
334 | ||
335 | self.lines.append(line) | |
336 | ||
337 | self.load_contexts() | |
338 | ||
339 | def get_lines(self): | |
340 | """ | |
341 | Return the lines read in from the configuration | |
342 | """ | |
343 | ||
701a0192 | 344 | return "\n".join(self.lines) |
2fc76430 DS |
345 | |
346 | def get_contexts(self): | |
347 | """ | |
348 | Return the parsed context as strings for display, log etc. | |
349 | """ | |
350 | ||
1c64265f | 351 | for (_, ctx) in sorted(iteritems(self.contexts)): |
701a0192 | 352 | print(str(ctx) + "\n") |
2fc76430 DS |
353 | |
354 | def save_contexts(self, key, lines): | |
355 | """ | |
356 | Save the provided key and lines as a context | |
357 | """ | |
358 | ||
359 | if not key: | |
360 | return | |
361 | ||
701a0192 | 362 | """ |
bb972e44 DD |
363 | IP addresses specified in "network" statements, "ip prefix-lists" |
364 | etc. can differ in the host part of the specification the user | |
365 | provides and what the running config displays. For example, user | |
366 | can specify 11.1.1.1/24, and the running config displays this as | |
367 | 11.1.1.0/24. Ensure we don't do a needless operation for such | |
368 | lines. IS-IS & OSPFv3 have no "network" support. | |
701a0192 | 369 | """ |
370 | re_key_rt = re.match(r"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key[0]) | |
bb972e44 DD |
371 | if re_key_rt: |
372 | addr = re_key_rt.group(2) | |
701a0192 | 373 | if "/" in addr: |
bb972e44 | 374 | try: |
701a0192 | 375 | if "ipaddress" not in sys.modules: |
1c64265f | 376 | newaddr = IPNetwork(addr) |
701a0192 | 377 | key[0] = "%s route %s/%s%s" % ( |
378 | re_key_rt.group(1), | |
379 | newaddr.network, | |
380 | newaddr.prefixlen, | |
381 | re_key_rt.group(3), | |
382 | ) | |
1c64265f | 383 | else: |
384 | newaddr = ip_network(addr, strict=False) | |
701a0192 | 385 | key[0] = "%s route %s/%s%s" % ( |
386 | re_key_rt.group(1), | |
387 | str(newaddr.network_address), | |
388 | newaddr.prefixlen, | |
389 | re_key_rt.group(3), | |
390 | ) | |
bb972e44 DD |
391 | except ValueError: |
392 | pass | |
393 | ||
394 | re_key_rt = re.match( | |
701a0192 | 395 | r"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key[0] |
bb972e44 DD |
396 | ) |
397 | if re_key_rt: | |
398 | addr = re_key_rt.group(4) | |
701a0192 | 399 | if "/" in addr: |
bb972e44 | 400 | try: |
701a0192 | 401 | if "ipaddress" not in sys.modules: |
402 | newaddr = "%s/%s" % ( | |
403 | IPNetwork(addr).network, | |
404 | IPNetwork(addr).prefixlen, | |
405 | ) | |
1c64265f | 406 | else: |
407 | network_addr = ip_network(addr, strict=False) | |
701a0192 | 408 | newaddr = "%s/%s" % ( |
409 | str(network_addr.network_address), | |
410 | network_addr.prefixlen, | |
411 | ) | |
bb972e44 DD |
412 | except ValueError: |
413 | newaddr = addr | |
0845b872 DD |
414 | else: |
415 | newaddr = addr | |
bb972e44 DD |
416 | |
417 | legestr = re_key_rt.group(5) | |
701a0192 | 418 | re_lege = re.search(r"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr) |
bb972e44 | 419 | if re_lege: |
701a0192 | 420 | legestr = "%sge %s le %s%s" % ( |
421 | re_lege.group(1), | |
422 | re_lege.group(3), | |
423 | re_lege.group(2), | |
424 | re_lege.group(4), | |
425 | ) | |
701a0192 | 426 | |
427 | key[0] = "%s prefix-list%s%s %s%s" % ( | |
428 | re_key_rt.group(1), | |
429 | re_key_rt.group(2), | |
430 | re_key_rt.group(3), | |
431 | newaddr, | |
432 | legestr, | |
433 | ) | |
434 | ||
435 | if lines and key[0].startswith("router bgp"): | |
bb972e44 DD |
436 | newlines = [] |
437 | for line in lines: | |
701a0192 | 438 | re_net = re.match(r"network\s+([A-Fa-f:.0-9/]+)(.*)$", line) |
bb972e44 DD |
439 | if re_net: |
440 | addr = re_net.group(1) | |
701a0192 | 441 | if "/" not in addr and key[0].startswith("router bgp"): |
bb972e44 DD |
442 | # This is most likely an error because with no |
443 | # prefixlen, BGP treats the prefixlen as 8 | |
701a0192 | 444 | addr = addr + "/8" |
bb972e44 DD |
445 | |
446 | try: | |
701a0192 | 447 | if "ipaddress" not in sys.modules: |
1c64265f | 448 | newaddr = IPNetwork(addr) |
701a0192 | 449 | line = "network %s/%s %s" % ( |
450 | newaddr.network, | |
451 | newaddr.prefixlen, | |
452 | re_net.group(2), | |
453 | ) | |
1c64265f | 454 | else: |
455 | network_addr = ip_network(addr, strict=False) | |
701a0192 | 456 | line = "network %s/%s %s" % ( |
457 | str(network_addr.network_address), | |
458 | network_addr.prefixlen, | |
459 | re_net.group(2), | |
460 | ) | |
bb972e44 | 461 | newlines.append(line) |
0845b872 | 462 | except ValueError: |
bb972e44 DD |
463 | # Really this should be an error. Whats a network |
464 | # without an IP Address following it ? | |
465 | newlines.append(line) | |
466 | else: | |
467 | newlines.append(line) | |
468 | lines = newlines | |
469 | ||
701a0192 | 470 | """ |
bb972e44 | 471 | More fixups in user specification and what running config shows. |
348135a5 | 472 | "null0" in routes must be replaced by Null0. |
701a0192 | 473 | """ |
474 | if ( | |
475 | key[0].startswith("ip route") | |
476 | or key[0].startswith("ipv6 route") | |
477 | and "null0" in key[0] | |
478 | ): | |
479 | key[0] = re.sub(r"\s+null0(\s*$)", " Null0", key[0]) | |
bb972e44 | 480 | |
00302a58 DS |
481 | """ |
482 | Similar to above, but when the static is in a vrf, it turns into a | |
483 | blackhole nexthop for both null0 and Null0. Fix it accordingly | |
484 | """ | |
485 | if lines and key[0].startswith("vrf "): | |
486 | newlines = [] | |
487 | for line in lines: | |
488 | if line.startswith("ip route ") or line.startswith("ipv6 route "): | |
489 | if "null0" in line: | |
490 | line = re.sub(r"\s+null0(\s*$)", " blackhole", line) | |
491 | elif "Null0" in line: | |
492 | line = re.sub(r"\s+Null0(\s*$)", " blackhole", line) | |
493 | newlines.append(line) | |
494 | else: | |
495 | newlines.append(line) | |
496 | lines = newlines | |
497 | ||
2fc76430 DS |
498 | if lines: |
499 | if tuple(key) not in self.contexts: | |
500 | ctx = Context(tuple(key), lines) | |
501 | self.contexts[tuple(key)] = ctx | |
502 | else: | |
503 | ctx = self.contexts[tuple(key)] | |
504 | ctx.add_lines(lines) | |
505 | ||
506 | else: | |
507 | if tuple(key) not in self.contexts: | |
508 | ctx = Context(tuple(key), []) | |
509 | self.contexts[tuple(key)] = ctx | |
510 | ||
511 | def load_contexts(self): | |
512 | """ | |
513 | Parse the configuration and create contexts for each appropriate block | |
514 | """ | |
515 | ||
516 | current_context_lines = [] | |
517 | ctx_keys = [] | |
518 | ||
701a0192 | 519 | """ |
2fc76430 DS |
520 | The end of a context is flagged via the 'end' keyword: |
521 | ||
522 | ! | |
523 | interface swp52 | |
524 | ipv6 nd suppress-ra | |
525 | link-detect | |
526 | ! | |
527 | end | |
528 | router bgp 10 | |
529 | bgp router-id 10.0.0.1 | |
530 | bgp log-neighbor-changes | |
531 | no bgp default ipv4-unicast | |
532 | neighbor EBGP peer-group | |
533 | neighbor EBGP advertisement-interval 1 | |
534 | neighbor EBGP timers connect 10 | |
535 | neighbor 2001:40:1:4::6 remote-as 40 | |
536 | neighbor 2001:40:1:8::a remote-as 40 | |
537 | ! | |
538 | end | |
539 | address-family ipv6 | |
540 | neighbor IBGPv6 activate | |
541 | neighbor 2001:10::2 peer-group IBGPv6 | |
542 | neighbor 2001:10::3 peer-group IBGPv6 | |
543 | exit-address-family | |
544 | ! | |
7918b335 DW |
545 | end |
546 | address-family evpn | |
547 | neighbor LEAF activate | |
548 | advertise-all-vni | |
549 | vni 10100 | |
550 | rd 65000:10100 | |
551 | route-target import 10.1.1.1:10100 | |
552 | route-target export 10.1.1.1:10100 | |
553 | exit-vni | |
554 | exit-address-family | |
555 | ! | |
2fc76430 DS |
556 | end |
557 | router ospf | |
558 | ospf router-id 10.0.0.1 | |
559 | log-adjacency-changes detail | |
560 | timers throttle spf 0 50 5000 | |
561 | ! | |
562 | end | |
701a0192 | 563 | """ |
2fc76430 DS |
564 | |
565 | # The code assumes that its working on the output from the "vtysh -m" | |
566 | # command. That provides the appropriate markers to signify end of | |
567 | # a context. This routine uses that to build the contexts for the | |
568 | # config. | |
569 | # | |
570 | # There are single line contexts such as "log file /media/node/zebra.log" | |
571 | # and multi-line contexts such as "router ospf" and subcontexts | |
572 | # within a context such as "address-family" within "router bgp" | |
573 | # In each of these cases, the first line of the context becomes the | |
574 | # key of the context. So "router bgp 10" is the key for the non-address | |
575 | # family part of bgp, "router bgp 10, address-family ipv6 unicast" is | |
576 | # the key for the subcontext and so on. | |
2fc76430 DS |
577 | ctx_keys = [] |
578 | main_ctx_key = [] | |
579 | new_ctx = True | |
2fc76430 DS |
580 | |
581 | # the keywords that we know are single line contexts. bgp in this case | |
582 | # is not the main router bgp block, but enabling multi-instance | |
701a0192 | 583 | oneline_ctx_keywords = ( |
584 | "access-list ", | |
585 | "agentx", | |
586 | "allow-external-route-update", | |
587 | "bgp ", | |
588 | "debug ", | |
589 | "domainname ", | |
590 | "dump ", | |
591 | "enable ", | |
592 | "frr ", | |
2a437850 | 593 | "fpm ", |
701a0192 | 594 | "hostname ", |
595 | "ip ", | |
596 | "ipv6 ", | |
597 | "log ", | |
598 | "mpls lsp", | |
599 | "mpls label", | |
600 | "no ", | |
601 | "password ", | |
602 | "ptm-enable", | |
603 | "router-id ", | |
604 | "service ", | |
605 | "table ", | |
606 | "username ", | |
607 | "zebra ", | |
608 | "vrrp autoconfigure", | |
609 | "evpn mh", | |
610 | ) | |
2fed5dcd | 611 | |
2fc76430 DS |
612 | for line in self.lines: |
613 | ||
614 | if not line: | |
615 | continue | |
616 | ||
701a0192 | 617 | if line.startswith("!") or line.startswith("#"): |
2fc76430 DS |
618 | continue |
619 | ||
880dcb06 DS |
620 | if ( |
621 | len(ctx_keys) == 2 | |
622 | and ctx_keys[0].startswith("bfd") | |
623 | and ctx_keys[1].startswith("profile ") | |
624 | and line == "end" | |
625 | ): | |
626 | log.debug("LINE %-50s: popping from sub context, %-50s", line, ctx_keys) | |
c42dfbb5 RZ |
627 | |
628 | if main_ctx_key: | |
629 | self.save_contexts(ctx_keys, current_context_lines) | |
630 | ctx_keys = copy.deepcopy(main_ctx_key) | |
631 | current_context_lines = [] | |
632 | continue | |
633 | ||
2fc76430 | 634 | # one line contexts |
ccef6e47 EDP |
635 | # there is one exception though: ldpd accepts a 'router-id' clause |
636 | # as part of its 'mpls ldp' config context. If we are processing | |
637 | # ldp configuration and encounter a router-id we should NOT switch | |
638 | # to a new context | |
701a0192 | 639 | if ( |
640 | new_ctx is True | |
641 | and any(line.startswith(keyword) for keyword in oneline_ctx_keywords) | |
642 | and not ( | |
643 | ctx_keys | |
644 | and ctx_keys[0].startswith("mpls ldp") | |
645 | and line.startswith("router-id ") | |
646 | ) | |
647 | ): | |
2fc76430 DS |
648 | self.save_contexts(ctx_keys, current_context_lines) |
649 | ||
650 | # Start a new context | |
651 | main_ctx_key = [] | |
701a0192 | 652 | ctx_keys = [ |
653 | line, | |
654 | ] | |
2fc76430 DS |
655 | current_context_lines = [] |
656 | ||
701a0192 | 657 | log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys) |
2fc76430 DS |
658 | self.save_contexts(ctx_keys, current_context_lines) |
659 | new_ctx = True | |
660 | ||
06ad470d | 661 | elif line == "end": |
2fc76430 | 662 | self.save_contexts(ctx_keys, current_context_lines) |
701a0192 | 663 | log.debug("LINE %-50s: exiting old context, %-50s", line, ctx_keys) |
2fc76430 DS |
664 | |
665 | # Start a new context | |
06ad470d DS |
666 | new_ctx = True |
667 | main_ctx_key = [] | |
668 | ctx_keys = [] | |
669 | current_context_lines = [] | |
670 | ||
7cfb3079 RB |
671 | elif line == "exit" and ctx_keys[0].startswith("rpki"): |
672 | self.save_contexts(ctx_keys, current_context_lines) | |
673 | log.debug("LINE %-50s: exiting old context, %-50s", line, ctx_keys) | |
674 | ||
675 | # Start a new context | |
676 | new_ctx = True | |
677 | main_ctx_key = [] | |
678 | ctx_keys = [] | |
679 | current_context_lines = [] | |
680 | ||
06ad470d DS |
681 | elif line == "exit-vrf": |
682 | self.save_contexts(ctx_keys, current_context_lines) | |
683 | current_context_lines.append(line) | |
701a0192 | 684 | log.debug( |
685 | "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys | |
686 | ) | |
06ad470d | 687 | |
701a0192 | 688 | # Start a new context |
2fc76430 DS |
689 | new_ctx = True |
690 | main_ctx_key = [] | |
691 | ctx_keys = [] | |
692 | current_context_lines = [] | |
693 | ||
4d7b695d SM |
694 | elif ( |
695 | line == "exit" | |
696 | and len(ctx_keys) > 1 | |
697 | and ctx_keys[0].startswith("segment-routing") | |
698 | ): | |
699 | self.save_contexts(ctx_keys, current_context_lines) | |
700 | ||
701 | # Start a new context | |
702 | ctx_keys = ctx_keys[:-1] | |
703 | current_context_lines = [] | |
704 | log.debug( | |
705 | "LINE %-50s: popping segment routing sub-context to ctx%-50s", | |
706 | line, | |
d82c5d61 | 707 | ctx_keys, |
4d7b695d SM |
708 | ) |
709 | ||
d60f4800 | 710 | elif line in ["exit-address-family", "exit", "exit-vnc"]: |
2fc76430 DS |
711 | # if this exit is for address-family ipv4 unicast, ignore the pop |
712 | if main_ctx_key: | |
713 | self.save_contexts(ctx_keys, current_context_lines) | |
714 | ||
715 | # Start a new context | |
716 | ctx_keys = copy.deepcopy(main_ctx_key) | |
717 | current_context_lines = [] | |
701a0192 | 718 | log.debug( |
719 | "LINE %-50s: popping from subcontext to ctx%-50s", | |
720 | line, | |
d82c5d61 | 721 | ctx_keys, |
701a0192 | 722 | ) |
2fc76430 | 723 | |
609ac8dd | 724 | elif line in ["exit-vni", "exit-ldp-if"]: |
d60f4800 DS |
725 | if sub_main_ctx_key: |
726 | self.save_contexts(ctx_keys, current_context_lines) | |
727 | ||
728 | # Start a new context | |
729 | ctx_keys = copy.deepcopy(sub_main_ctx_key) | |
730 | current_context_lines = [] | |
701a0192 | 731 | log.debug( |
732 | "LINE %-50s: popping from sub-subcontext to ctx%-50s", | |
733 | line, | |
734 | ctx_keys, | |
735 | ) | |
d60f4800 | 736 | |
2fc76430 DS |
737 | elif new_ctx is True: |
738 | if not main_ctx_key: | |
701a0192 | 739 | ctx_keys = [ |
740 | line, | |
741 | ] | |
2fc76430 DS |
742 | else: |
743 | ctx_keys = copy.deepcopy(main_ctx_key) | |
744 | main_ctx_key = [] | |
745 | ||
746 | current_context_lines = [] | |
747 | new_ctx = False | |
701a0192 | 748 | log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys) |
efba0985 | 749 | |
701a0192 | 750 | elif ( |
751 | line.startswith("address-family ") | |
752 | or line.startswith("vnc defaults") | |
753 | or line.startswith("vnc l2-group") | |
754 | or line.startswith("vnc nve-group") | |
755 | or line.startswith("peer") | |
756 | or line.startswith("key ") | |
757 | or line.startswith("member pseudowire") | |
758 | ): | |
2fc76430 DS |
759 | main_ctx_key = [] |
760 | ||
0b960b4d DW |
761 | # Save old context first |
762 | self.save_contexts(ctx_keys, current_context_lines) | |
763 | current_context_lines = [] | |
764 | main_ctx_key = copy.deepcopy(ctx_keys) | |
701a0192 | 765 | log.debug("LINE %-50s: entering sub-context, append to ctx_keys", line) |
2fc76430 | 766 | |
701a0192 | 767 | if line == "address-family ipv6" and not ctx_keys[0].startswith( |
768 | "mpls ldp" | |
769 | ): | |
0b960b4d | 770 | ctx_keys.append("address-family ipv6 unicast") |
701a0192 | 771 | elif line == "address-family ipv4" and not ctx_keys[0].startswith( |
772 | "mpls ldp" | |
773 | ): | |
0b960b4d | 774 | ctx_keys.append("address-family ipv4 unicast") |
5014d96f DW |
775 | elif line == "address-family evpn": |
776 | ctx_keys.append("address-family l2vpn evpn") | |
0b960b4d DW |
777 | else: |
778 | ctx_keys.append(line) | |
2fc76430 | 779 | |
701a0192 | 780 | elif ( |
781 | line.startswith("vni ") | |
782 | and len(ctx_keys) == 2 | |
783 | and ctx_keys[0].startswith("router bgp") | |
784 | and ctx_keys[1] == "address-family l2vpn evpn" | |
785 | ): | |
d60f4800 DS |
786 | |
787 | # Save old context first | |
788 | self.save_contexts(ctx_keys, current_context_lines) | |
789 | current_context_lines = [] | |
790 | sub_main_ctx_key = copy.deepcopy(ctx_keys) | |
701a0192 | 791 | log.debug( |
792 | "LINE %-50s: entering sub-sub-context, append to ctx_keys", line | |
793 | ) | |
d60f4800 | 794 | ctx_keys.append(line) |
701a0192 | 795 | |
796 | elif ( | |
797 | line.startswith("interface ") | |
798 | and len(ctx_keys) == 2 | |
799 | and ctx_keys[0].startswith("mpls ldp") | |
800 | and ctx_keys[1].startswith("address-family") | |
801 | ): | |
609ac8dd EDP |
802 | |
803 | # Save old context first | |
804 | self.save_contexts(ctx_keys, current_context_lines) | |
d60f4800 DS |
805 | current_context_lines = [] |
806 | sub_main_ctx_key = copy.deepcopy(ctx_keys) | |
701a0192 | 807 | log.debug( |
808 | "LINE %-50s: entering sub-sub-context, append to ctx_keys", line | |
809 | ) | |
d60f4800 DS |
810 | ctx_keys.append(line) |
811 | ||
4d7b695d SM |
812 | elif ( |
813 | line.startswith("traffic-eng") | |
814 | and len(ctx_keys) == 1 | |
815 | and ctx_keys[0].startswith("segment-routing") | |
816 | ): | |
817 | ||
818 | # Save old context first | |
819 | self.save_contexts(ctx_keys, current_context_lines) | |
820 | current_context_lines = [] | |
821 | log.debug( | |
d82c5d61 DS |
822 | "LINE %-50s: entering segment routing sub-context, append to ctx_keys", |
823 | line, | |
4d7b695d SM |
824 | ) |
825 | ctx_keys.append(line) | |
826 | ||
827 | elif ( | |
828 | line.startswith("segment-list ") | |
829 | and len(ctx_keys) == 2 | |
830 | and ctx_keys[0].startswith("segment-routing") | |
831 | and ctx_keys[1].startswith("traffic-eng") | |
832 | ): | |
833 | ||
834 | # Save old context first | |
835 | self.save_contexts(ctx_keys, current_context_lines) | |
836 | current_context_lines = [] | |
837 | log.debug( | |
d82c5d61 DS |
838 | "LINE %-50s: entering segment routing sub-context, append to ctx_keys", |
839 | line, | |
4d7b695d SM |
840 | ) |
841 | ctx_keys.append(line) | |
842 | ||
843 | elif ( | |
844 | line.startswith("policy ") | |
845 | and len(ctx_keys) == 2 | |
846 | and ctx_keys[0].startswith("segment-routing") | |
847 | and ctx_keys[1].startswith("traffic-eng") | |
848 | ): | |
849 | ||
850 | # Save old context first | |
851 | self.save_contexts(ctx_keys, current_context_lines) | |
852 | current_context_lines = [] | |
853 | log.debug( | |
d82c5d61 DS |
854 | "LINE %-50s: entering segment routing sub-context, append to ctx_keys", |
855 | line, | |
4d7b695d SM |
856 | ) |
857 | ctx_keys.append(line) | |
858 | ||
859 | elif ( | |
860 | line.startswith("candidate-path ") | |
861 | and line.endswith(" dynamic") | |
862 | and len(ctx_keys) == 3 | |
863 | and ctx_keys[0].startswith("segment-routing") | |
864 | and ctx_keys[1].startswith("traffic-eng") | |
865 | and ctx_keys[2].startswith("policy") | |
866 | ): | |
867 | ||
868 | # Save old context first | |
869 | self.save_contexts(ctx_keys, current_context_lines) | |
870 | current_context_lines = [] | |
871 | main_ctx_key = copy.deepcopy(ctx_keys) | |
872 | log.debug( | |
d82c5d61 DS |
873 | "LINE %-50s: entering candidate-path sub-context, append to ctx_keys", |
874 | line, | |
4d7b695d SM |
875 | ) |
876 | ctx_keys.append(line) | |
877 | ||
efba0985 SM |
878 | elif ( |
879 | line.startswith("pcep") | |
880 | and len(ctx_keys) == 2 | |
881 | and ctx_keys[0].startswith("segment-routing") | |
882 | and ctx_keys[1].startswith("traffic-eng") | |
883 | ): | |
884 | ||
885 | # Save old context first | |
886 | self.save_contexts(ctx_keys, current_context_lines) | |
887 | current_context_lines = [] | |
888 | main_ctx_key = copy.deepcopy(ctx_keys) | |
889 | log.debug( | |
890 | "LINE %-50s: entering pcep sub-context, append to ctx_keys", line | |
891 | ) | |
892 | ctx_keys.append(line) | |
893 | ||
894 | elif ( | |
895 | line.startswith("pce-config ") | |
896 | and len(ctx_keys) == 3 | |
897 | and ctx_keys[0].startswith("segment-routing") | |
898 | and ctx_keys[1].startswith("traffic-eng") | |
899 | and ctx_keys[2].startswith("pcep") | |
900 | ): | |
901 | ||
902 | # Save old context first | |
903 | self.save_contexts(ctx_keys, current_context_lines) | |
904 | current_context_lines = [] | |
905 | main_ctx_key = copy.deepcopy(ctx_keys) | |
906 | log.debug( | |
d82c5d61 DS |
907 | "LINE %-50s: entering pce-config sub-context, append to ctx_keys", |
908 | line, | |
efba0985 SM |
909 | ) |
910 | ctx_keys.append(line) | |
911 | ||
912 | elif ( | |
913 | line.startswith("pce ") | |
914 | and len(ctx_keys) == 3 | |
915 | and ctx_keys[0].startswith("segment-routing") | |
916 | and ctx_keys[1].startswith("traffic-eng") | |
917 | and ctx_keys[2].startswith("pcep") | |
918 | ): | |
919 | ||
920 | # Save old context first | |
921 | self.save_contexts(ctx_keys, current_context_lines) | |
922 | current_context_lines = [] | |
923 | main_ctx_key = copy.deepcopy(ctx_keys) | |
924 | log.debug( | |
925 | "LINE %-50s: entering pce sub-context, append to ctx_keys", line | |
926 | ) | |
927 | ctx_keys.append(line) | |
928 | ||
929 | elif ( | |
930 | line.startswith("pcc") | |
931 | and len(ctx_keys) == 3 | |
932 | and ctx_keys[0].startswith("segment-routing") | |
933 | and ctx_keys[1].startswith("traffic-eng") | |
934 | and ctx_keys[2].startswith("pcep") | |
935 | ): | |
936 | ||
937 | # Save old context first | |
938 | self.save_contexts(ctx_keys, current_context_lines) | |
939 | current_context_lines = [] | |
940 | main_ctx_key = copy.deepcopy(ctx_keys) | |
941 | log.debug( | |
942 | "LINE %-50s: entering pcc sub-context, append to ctx_keys", line | |
943 | ) | |
944 | ctx_keys.append(line) | |
c42dfbb5 RZ |
945 | |
946 | elif ( | |
880dcb06 | 947 | line.startswith("profile ") |
c42dfbb5 | 948 | and len(ctx_keys) == 1 |
880dcb06 | 949 | and ctx_keys[0].startswith("bfd") |
c42dfbb5 RZ |
950 | ): |
951 | ||
952 | # Save old context first | |
953 | self.save_contexts(ctx_keys, current_context_lines) | |
954 | current_context_lines = [] | |
955 | main_ctx_key = copy.deepcopy(ctx_keys) | |
956 | log.debug( | |
957 | "LINE %-50s: entering BFD profile sub-context, append to ctx_keys", | |
880dcb06 | 958 | line, |
c42dfbb5 RZ |
959 | ) |
960 | ctx_keys.append(line) | |
efba0985 | 961 | |
2fc76430 DS |
962 | else: |
963 | # Continuing in an existing context, add non-commented lines to it | |
964 | current_context_lines.append(line) | |
701a0192 | 965 | log.debug( |
966 | "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys | |
967 | ) | |
2fc76430 DS |
968 | |
969 | # Save the context of the last one | |
970 | self.save_contexts(ctx_keys, current_context_lines) | |
971 | ||
4a2587c6 | 972 | |
663ece2f | 973 | def lines_to_config(ctx_keys, line, delete): |
4a2587c6 | 974 | """ |
e20dc2ba | 975 | Return the command as it would appear in frr.conf |
4a2587c6 DW |
976 | """ |
977 | cmd = [] | |
978 | ||
979 | if line: | |
980 | for (i, ctx_key) in enumerate(ctx_keys): | |
701a0192 | 981 | cmd.append(" " * i + ctx_key) |
4a2587c6 DW |
982 | |
983 | line = line.lstrip() | |
701a0192 | 984 | indent = len(ctx_keys) * " " |
4a2587c6 | 985 | |
663ece2f DL |
986 | # There are some commands that are on by default so their "no" form will be |
987 | # displayed in the config. "no bgp default ipv4-unicast" is one of these. | |
988 | # If we need to remove this line we do so by adding "bgp default ipv4-unicast", | |
989 | # not by doing a "no no bgp default ipv4-unicast" | |
4a2587c6 | 990 | if delete: |
701a0192 | 991 | if line.startswith("no "): |
992 | cmd.append("%s%s" % (indent, line[3:])) | |
4a2587c6 | 993 | else: |
701a0192 | 994 | cmd.append("%sno %s" % (indent, line)) |
4a2587c6 DW |
995 | |
996 | else: | |
997 | cmd.append(indent + line) | |
998 | ||
999 | # If line is None then we are typically deleting an entire | |
1000 | # context ('no router ospf' for example) | |
1001 | else: | |
663ece2f | 1002 | for i, ctx_key in enumerate(ctx_keys[:-1]): |
701a0192 | 1003 | cmd.append("%s%s" % (" " * i, ctx_key)) |
4a2587c6 | 1004 | |
663ece2f DL |
1005 | # Only put the 'no' on the last sub-context |
1006 | if delete: | |
701a0192 | 1007 | if ctx_keys[-1].startswith("no "): |
1008 | cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1][3:])) | |
663ece2f | 1009 | else: |
701a0192 | 1010 | cmd.append("%sno %s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1])) |
4a2587c6 | 1011 | else: |
701a0192 | 1012 | cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1])) |
8ad1fe6c DW |
1013 | |
1014 | return cmd | |
4a2587c6 DW |
1015 | |
1016 | ||
2fc76430 DS |
1017 | def get_normalized_ipv6_line(line): |
1018 | """ | |
d8e4c438 | 1019 | Return a normalized IPv6 line as produced by frr, |
2fc76430 | 1020 | with all letters in lower case and trailing and leading |
bb972e44 DD |
1021 | zeros removed, and only the network portion present if |
1022 | the IPv6 word is a network | |
2fc76430 DS |
1023 | """ |
1024 | norm_line = "" | |
701a0192 | 1025 | words = line.split(" ") |
2fc76430 DS |
1026 | for word in words: |
1027 | if ":" in word: | |
bb972e44 DD |
1028 | norm_word = None |
1029 | if "/" in word: | |
1030 | try: | |
701a0192 | 1031 | if "ipaddress" not in sys.modules: |
1c64265f | 1032 | v6word = IPNetwork(word) |
701a0192 | 1033 | norm_word = "%s/%s" % (v6word.network, v6word.prefixlen) |
1c64265f | 1034 | else: |
1035 | v6word = ip_network(word, strict=False) | |
701a0192 | 1036 | norm_word = "%s/%s" % ( |
1037 | str(v6word.network_address), | |
1038 | v6word.prefixlen, | |
1039 | ) | |
bb972e44 DD |
1040 | except ValueError: |
1041 | pass | |
1042 | if not norm_word: | |
1043 | try: | |
701a0192 | 1044 | norm_word = "%s" % IPv6Address(word) |
0845b872 | 1045 | except ValueError: |
bb972e44 | 1046 | norm_word = word |
2fc76430 DS |
1047 | else: |
1048 | norm_word = word | |
1049 | norm_line = norm_line + " " + norm_word | |
1050 | ||
1051 | return norm_line.strip() | |
1052 | ||
4a2587c6 | 1053 | |
c755f5c4 | 1054 | def line_exist(lines, target_ctx_keys, target_line, exact_match=True): |
9fe88bc7 | 1055 | for (ctx_keys, line) in lines: |
c755f5c4 DW |
1056 | if ctx_keys == target_ctx_keys: |
1057 | if exact_match: | |
1058 | if line == target_line: | |
1059 | return True | |
1060 | else: | |
1061 | if line.startswith(target_line): | |
1062 | return True | |
9fe88bc7 DW |
1063 | return False |
1064 | ||
701a0192 | 1065 | |
eb9113df DS |
1066 | def check_for_exit_vrf(lines_to_add, lines_to_del): |
1067 | ||
1068 | # exit-vrf is a bit tricky. If the new config is missing it but we | |
1069 | # have configs under a vrf, we need to add it at the end to do the | |
1070 | # right context changes. If exit-vrf exists in both the running and | |
1071 | # new config, we cannot delete it or it will break context changes. | |
1072 | add_exit_vrf = False | |
1073 | index = 0 | |
1074 | ||
1075 | for (ctx_keys, line) in lines_to_add: | |
1076 | if add_exit_vrf == True: | |
1077 | if ctx_keys[0] != prior_ctx_key: | |
701a0192 | 1078 | insert_key = ((prior_ctx_key),) |
eb9113df DS |
1079 | lines_to_add.insert(index, ((insert_key, "exit-vrf"))) |
1080 | add_exit_vrf = False | |
1081 | ||
701a0192 | 1082 | if ctx_keys[0].startswith("vrf") and line: |
0ed6a49d | 1083 | if line != "exit-vrf": |
eb9113df | 1084 | add_exit_vrf = True |
701a0192 | 1085 | prior_ctx_key = ctx_keys[0] |
eb9113df DS |
1086 | else: |
1087 | add_exit_vrf = False | |
701a0192 | 1088 | index += 1 |
eb9113df DS |
1089 | |
1090 | for (ctx_keys, line) in lines_to_del: | |
1091 | if line == "exit-vrf": | |
701a0192 | 1092 | if line_exist(lines_to_add, ctx_keys, line): |
eb9113df DS |
1093 | lines_to_del.remove((ctx_keys, line)) |
1094 | ||
1095 | return (lines_to_add, lines_to_del) | |
9fe88bc7 | 1096 | |
701a0192 | 1097 | |
934c84a0 CS |
1098 | """ |
1099 | This method handles deletion of bgp peer group config. | |
1100 | The objective is to delete config lines related to peers | |
1101 | associated with the peer-group and move the peer-group | |
1102 | config line to the end of the lines_to_del list. | |
1103 | """ | |
1104 | ||
1105 | ||
1106 | def delete_move_lines(lines_to_add, lines_to_del): | |
1107 | ||
1108 | del_dict = dict() | |
1109 | # Stores the lines to move to the end of the pending list. | |
1110 | lines_to_del_to_del = [] | |
1111 | # Stores the lines to move to end of the pending list. | |
1112 | lines_to_del_to_app = [] | |
1113 | found_pg_del_cmd = False | |
1114 | ||
1115 | """ | |
1116 | When "neighbor <pg_name> peer-group" under a bgp instance is removed, | |
1117 | it also deletes the associated peer config. Any config line below no form of | |
1118 | peer-group related to a peer are errored out as the peer no longer exists. | |
1119 | To cleanup peer-group and associated peer(s) configs: | |
1120 | - Remove all the peers config lines from the pending list (lines_to_del list). | |
1121 | - Move peer-group deletion line to the end of the pending list, to allow | |
1122 | removal of any of the peer-group specific configs. | |
1123 | ||
1124 | Create a dictionary of config context (i.e. router bgp vrf x). | |
1125 | Under each context node, create a dictionary of a peer-group name. | |
1126 | Append a peer associated to the peer-group into a list under a peer-group node. | |
1127 | Remove all of the peer associated config lines from the pending list. | |
1128 | Append peer-group deletion line to end of the pending list. | |
1129 | ||
1130 | Example: | |
1131 | neighbor underlay peer-group | |
1132 | neighbor underlay remote-as external | |
1133 | neighbor underlay advertisement-interval 0 | |
1134 | neighbor underlay timers 3 9 | |
1135 | neighbor underlay timers connect 10 | |
1136 | neighbor swp1 interface peer-group underlay | |
1137 | neighbor swp1 advertisement-interval 0 | |
1138 | neighbor swp1 timers 3 9 | |
1139 | neighbor swp1 timers connect 10 | |
1140 | neighbor swp2 interface peer-group underlay | |
1141 | neighbor swp2 advertisement-interval 0 | |
1142 | neighbor swp2 timers 3 9 | |
1143 | neighbor swp2 timers connect 10 | |
1144 | neighbor swp3 interface peer-group underlay | |
1145 | neighbor uplink1 interface remote-as internal | |
1146 | neighbor uplink1 advertisement-interval 0 | |
1147 | neighbor uplink1 timers 3 9 | |
1148 | neighbor uplink1 timers connect 10 | |
1149 | ||
1150 | New order: | |
1151 | "router bgp 200 no bgp bestpath as-path multipath-relax" | |
1152 | "router bgp 200 no neighbor underlay advertisement-interval 0" | |
1153 | "router bgp 200 no neighbor underlay timers 3 9" | |
1154 | "router bgp 200 no neighbor underlay timers connect 10" | |
1155 | "router bgp 200 no neighbor uplink1 advertisement-interval 0" | |
1156 | "router bgp 200 no neighbor uplink1 timers 3 9" | |
1157 | "router bgp 200 no neighbor uplink1 timers connect 10" | |
1158 | "router bgp 200 no neighbor underlay remote-as external" | |
1159 | "router bgp 200 no neighbor uplink1 interface remote-as internal" | |
1160 | "router bgp 200 no neighbor underlay peer-group" | |
1161 | ||
1162 | """ | |
1163 | ||
1164 | for (ctx_keys, line) in lines_to_del: | |
1165 | if ( | |
1166 | ctx_keys[0].startswith("router bgp") | |
1167 | and line | |
1168 | and line.startswith("neighbor ") | |
1169 | ): | |
1170 | """ | |
1171 | When 'neighbor <peer> remote-as <>' is removed it deletes the peer, | |
1172 | there might be a peer associated config which also needs to be removed | |
1173 | prior to peer. | |
1174 | Append the 'neighbor <peer> remote-as <>' to the lines_to_del. | |
1175 | Example: | |
1176 | ||
1177 | neighbor uplink1 interface remote-as internal | |
1178 | neighbor uplink1 advertisement-interval 0 | |
1179 | neighbor uplink1 timers 3 9 | |
1180 | neighbor uplink1 timers connect 10 | |
1181 | ||
1182 | Move to end: | |
1183 | neighbor uplink1 advertisement-interval 0 | |
1184 | neighbor uplink1 timers 3 9 | |
1185 | neighbor uplink1 timers connect 10 | |
1186 | ... | |
1187 | ||
1188 | neighbor uplink1 interface remote-as internal | |
1189 | ||
1190 | """ | |
1191 | # 'no neighbor peer [interface] remote-as <>' | |
1192 | nb_remoteas = "neighbor (\S+) .*remote-as (\S+)" | |
1193 | re_nb_remoteas = re.search(nb_remoteas, line) | |
1194 | if re_nb_remoteas: | |
1195 | lines_to_del_to_app.append((ctx_keys, line)) | |
1196 | ||
1197 | """ | |
1198 | {'router bgp 65001': {'PG': [], 'PG1': []}, | |
1199 | 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}} | |
1200 | """ | |
1201 | if ctx_keys[0] not in del_dict: | |
1202 | del_dict[ctx_keys[0]] = dict() | |
1203 | # find 'no neighbor <pg_name> peer-group' | |
1204 | re_pg = re.match("neighbor (\S+) peer-group$", line) | |
1205 | if re_pg and re_pg.group(1) not in del_dict[ctx_keys[0]]: | |
1206 | del_dict[ctx_keys[0]][re_pg.group(1)] = list() | |
1207 | ||
1208 | for (ctx_keys, line) in lines_to_del_to_app: | |
1209 | lines_to_del.remove((ctx_keys, line)) | |
1210 | lines_to_del.append((ctx_keys, line)) | |
1211 | ||
1212 | if found_pg_del_cmd == False: | |
1213 | return (lines_to_add, lines_to_del) | |
1214 | ||
1215 | """ | |
1216 | {'router bgp 65001': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}, | |
1217 | 'router bgp 65001 vrf vrf1': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}} | |
1218 | """ | |
1219 | for (ctx_keys, line) in lines_to_del: | |
1220 | if ( | |
1221 | ctx_keys[0].startswith("router bgp") | |
1222 | and line | |
1223 | and line.startswith("neighbor ") | |
1224 | ): | |
1225 | if ctx_keys[0] in del_dict: | |
1226 | for pg_key in del_dict[ctx_keys[0]]: | |
1227 | # 'neighbor <peer> [interface] peer-group <pg_name>' | |
1228 | nb_pg = "neighbor (\S+) .*peer-group %s$" % pg_key | |
1229 | re_nbr_pg = re.search(nb_pg, line) | |
1230 | if ( | |
1231 | re_nbr_pg | |
1232 | and re_nbr_pg.group(1) not in del_dict[ctx_keys[0]][pg_key] | |
1233 | ): | |
1234 | del_dict[ctx_keys[0]][pg_key].append(re_nbr_pg.group(1)) | |
1235 | ||
1236 | lines_to_del_to_app = [] | |
1237 | for (ctx_keys, line) in lines_to_del: | |
1238 | if ( | |
1239 | ctx_keys[0].startswith("router bgp") | |
1240 | and line | |
1241 | and line.startswith("neighbor ") | |
1242 | ): | |
1243 | if ctx_keys[0] in del_dict: | |
1244 | for pg in del_dict[ctx_keys[0]]: | |
1245 | for nbr in del_dict[ctx_keys[0]][pg]: | |
1246 | nb_exp = "neighbor %s .*" % nbr | |
1247 | re_nb = re.search(nb_exp, line) | |
1248 | # add peer configs to delete list. | |
1249 | if re_nb and line not in lines_to_del_to_del: | |
1250 | lines_to_del_to_del.append((ctx_keys, line)) | |
1251 | ||
1252 | pg_exp = "neighbor %s peer-group$" % pg | |
1253 | re_pg = re.match(pg_exp, line) | |
1254 | if re_pg: | |
1255 | lines_to_del_to_app.append((ctx_keys, line)) | |
1256 | ||
1257 | for (ctx_keys, line) in lines_to_del_to_del: | |
1258 | lines_to_del.remove((ctx_keys, line)) | |
1259 | ||
1260 | for (ctx_keys, line) in lines_to_del_to_app: | |
1261 | lines_to_del.remove((ctx_keys, line)) | |
1262 | lines_to_del.append((ctx_keys, line)) | |
1263 | ||
1264 | return (lines_to_add, lines_to_del) | |
1265 | ||
1266 | ||
9b166171 | 1267 | def ignore_delete_re_add_lines(lines_to_add, lines_to_del): |
9fe88bc7 DW |
1268 | |
1269 | # Quite possibly the most confusing (while accurate) variable names in history | |
1270 | lines_to_add_to_del = [] | |
1271 | lines_to_del_to_del = [] | |
1272 | ||
1273 | for (ctx_keys, line) in lines_to_del: | |
9b166171 DW |
1274 | deleted = False |
1275 | ||
5ad46333 EDP |
1276 | # If there is a change in the segment routing block ranges, do it |
1277 | # in-place, to avoid requesting spurious label chunks which might fail | |
1278 | if line and "segment-routing global-block" in line: | |
1279 | for (add_key, add_line) in lines_to_add: | |
61dc17d8 DS |
1280 | if ( |
1281 | ctx_keys[0] == add_key[0] | |
1282 | and add_line | |
1283 | and "segment-routing global-block" in add_line | |
1284 | ): | |
5ad46333 EDP |
1285 | lines_to_del_to_del.append((ctx_keys, line)) |
1286 | break | |
1287 | continue | |
1288 | ||
701a0192 | 1289 | if ctx_keys[0].startswith("router bgp") and line: |
028bcc88 | 1290 | |
701a0192 | 1291 | if line.startswith("neighbor "): |
1292 | """ | |
028bcc88 DW |
1293 | BGP changed how it displays swpX peers that are part of peer-group. Older |
1294 | versions of frr would display these on separate lines: | |
1295 | neighbor swp1 interface | |
1296 | neighbor swp1 peer-group FOO | |
1297 | ||
1298 | but today we display via a single line | |
1299 | neighbor swp1 interface peer-group FOO | |
1300 | ||
1301 | This change confuses frr-reload.py so check to see if we are deleting | |
1302 | neighbor swp1 interface peer-group FOO | |
1303 | ||
1304 | and adding | |
1305 | neighbor swp1 interface | |
1306 | neighbor swp1 peer-group FOO | |
1307 | ||
1308 | If so then chop the del line and the corresponding add lines | |
701a0192 | 1309 | """ |
028bcc88 | 1310 | |
701a0192 | 1311 | re_swpx_int_peergroup = re.search( |
1312 | "neighbor (\S+) interface peer-group (\S+)", line | |
1313 | ) | |
1314 | re_swpx_int_v6only_peergroup = re.search( | |
1315 | "neighbor (\S+) interface v6only peer-group (\S+)", line | |
1316 | ) | |
028bcc88 DW |
1317 | |
1318 | if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup: | |
1319 | swpx_interface = None | |
1320 | swpx_peergroup = None | |
1321 | ||
1322 | if re_swpx_int_peergroup: | |
1323 | swpx = re_swpx_int_peergroup.group(1) | |
1324 | peergroup = re_swpx_int_peergroup.group(2) | |
1325 | swpx_interface = "neighbor %s interface" % swpx | |
1326 | elif re_swpx_int_v6only_peergroup: | |
1327 | swpx = re_swpx_int_v6only_peergroup.group(1) | |
1328 | peergroup = re_swpx_int_v6only_peergroup.group(2) | |
1329 | swpx_interface = "neighbor %s interface v6only" % swpx | |
1330 | ||
1331 | swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup) | |
701a0192 | 1332 | found_add_swpx_interface = line_exist( |
1333 | lines_to_add, ctx_keys, swpx_interface | |
1334 | ) | |
1335 | found_add_swpx_peergroup = line_exist( | |
1336 | lines_to_add, ctx_keys, swpx_peergroup | |
1337 | ) | |
028bcc88 | 1338 | tmp_ctx_keys = tuple(list(ctx_keys)) |
9b166171 DW |
1339 | |
1340 | if not found_add_swpx_peergroup: | |
1341 | tmp_ctx_keys = list(ctx_keys) | |
701a0192 | 1342 | tmp_ctx_keys.append("address-family ipv4 unicast") |
9b166171 | 1343 | tmp_ctx_keys = tuple(tmp_ctx_keys) |
701a0192 | 1344 | found_add_swpx_peergroup = line_exist( |
1345 | lines_to_add, tmp_ctx_keys, swpx_peergroup | |
1346 | ) | |
9fe88bc7 | 1347 | |
028bcc88 DW |
1348 | if not found_add_swpx_peergroup: |
1349 | tmp_ctx_keys = list(ctx_keys) | |
701a0192 | 1350 | tmp_ctx_keys.append("address-family ipv6 unicast") |
028bcc88 | 1351 | tmp_ctx_keys = tuple(tmp_ctx_keys) |
701a0192 | 1352 | found_add_swpx_peergroup = line_exist( |
1353 | lines_to_add, tmp_ctx_keys, swpx_peergroup | |
1354 | ) | |
028bcc88 DW |
1355 | |
1356 | if found_add_swpx_interface and found_add_swpx_peergroup: | |
1357 | deleted = True | |
1358 | lines_to_del_to_del.append((ctx_keys, line)) | |
1359 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
1360 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup)) | |
1361 | ||
701a0192 | 1362 | """ |
ee951352 DS |
1363 | Changing the bfd timers on neighbors is allowed without doing |
1364 | a delete/add process. Since doing a "no neighbor blah bfd ..." | |
1365 | will cause the peer to bounce unnecessarily, just skip the delete | |
1366 | and just do the add. | |
701a0192 | 1367 | """ |
1368 | re_nbr_bfd_timers = re.search( | |
1369 | r"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line | |
1370 | ) | |
ee951352 DS |
1371 | |
1372 | if re_nbr_bfd_timers: | |
1373 | nbr = re_nbr_bfd_timers.group(1) | |
1374 | bfd_nbr = "neighbor %s" % nbr | |
701a0192 | 1375 | bfd_search_string = bfd_nbr + r" bfd (\S+) (\S+) (\S+)" |
ee951352 DS |
1376 | |
1377 | for (ctx_keys, add_line) in lines_to_add: | |
701a0192 | 1378 | if ctx_keys[0].startswith("router bgp"): |
1379 | re_add_nbr_bfd_timers = re.search( | |
1380 | bfd_search_string, add_line | |
1381 | ) | |
ee951352 | 1382 | |
ca7f0496 | 1383 | if re_add_nbr_bfd_timers: |
701a0192 | 1384 | found_add_bfd_nbr = line_exist( |
1385 | lines_to_add, ctx_keys, bfd_nbr, False | |
1386 | ) | |
ee951352 | 1387 | |
ca7f0496 DS |
1388 | if found_add_bfd_nbr: |
1389 | lines_to_del_to_del.append((ctx_keys, line)) | |
ee951352 | 1390 | |
701a0192 | 1391 | """ |
4c76e592 | 1392 | We changed how we display the neighbor interface command. Older |
028bcc88 DW |
1393 | versions of frr would display the following: |
1394 | neighbor swp1 interface | |
1395 | neighbor swp1 remote-as external | |
1396 | neighbor swp1 capability extended-nexthop | |
1397 | ||
1398 | but today we display via a single line | |
1399 | neighbor swp1 interface remote-as external | |
1400 | ||
1401 | and capability extended-nexthop is no longer needed because we | |
1402 | automatically enable it when the neighbor is of type interface. | |
1403 | ||
1404 | This change confuses frr-reload.py so check to see if we are deleting | |
1405 | neighbor swp1 interface remote-as (external|internal|ASNUM) | |
1406 | ||
1407 | and adding | |
1408 | neighbor swp1 interface | |
1409 | neighbor swp1 remote-as (external|internal|ASNUM) | |
1410 | neighbor swp1 capability extended-nexthop | |
1411 | ||
1412 | If so then chop the del line and the corresponding add lines | |
701a0192 | 1413 | """ |
1414 | re_swpx_int_remoteas = re.search( | |
1415 | "neighbor (\S+) interface remote-as (\S+)", line | |
1416 | ) | |
1417 | re_swpx_int_v6only_remoteas = re.search( | |
1418 | "neighbor (\S+) interface v6only remote-as (\S+)", line | |
1419 | ) | |
028bcc88 DW |
1420 | |
1421 | if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas: | |
1422 | swpx_interface = None | |
1423 | swpx_remoteas = None | |
1424 | ||
1425 | if re_swpx_int_remoteas: | |
1426 | swpx = re_swpx_int_remoteas.group(1) | |
1427 | remoteas = re_swpx_int_remoteas.group(2) | |
1428 | swpx_interface = "neighbor %s interface" % swpx | |
1429 | elif re_swpx_int_v6only_remoteas: | |
1430 | swpx = re_swpx_int_v6only_remoteas.group(1) | |
1431 | remoteas = re_swpx_int_v6only_remoteas.group(2) | |
1432 | swpx_interface = "neighbor %s interface v6only" % swpx | |
1433 | ||
1434 | swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas) | |
701a0192 | 1435 | found_add_swpx_interface = line_exist( |
1436 | lines_to_add, ctx_keys, swpx_interface | |
1437 | ) | |
1438 | found_add_swpx_remoteas = line_exist( | |
1439 | lines_to_add, ctx_keys, swpx_remoteas | |
1440 | ) | |
028bcc88 DW |
1441 | tmp_ctx_keys = tuple(list(ctx_keys)) |
1442 | ||
1443 | if found_add_swpx_interface and found_add_swpx_remoteas: | |
1444 | deleted = True | |
1445 | lines_to_del_to_del.append((ctx_keys, line)) | |
1446 | lines_to_add_to_del.append((ctx_keys, swpx_interface)) | |
1447 | lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas)) | |
1448 | ||
701a0192 | 1449 | """ |
4c76e592 DW |
1450 | We made the 'bgp bestpath as-path multipath-relax' command |
1451 | automatically assume 'no-as-set' since the lack of this option caused | |
1452 | weird routing problems. When the running config is shown in | |
1453 | releases with this change, the no-as-set keyword is not shown as it | |
1454 | is the default. This causes frr-reload to unnecessarily unapply | |
1455 | this option only to apply it back again, causing unnecessary session | |
1456 | resets. | |
701a0192 | 1457 | """ |
1458 | if "multipath-relax" in line: | |
1459 | re_asrelax_new = re.search( | |
1460 | "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line | |
1461 | ) | |
1462 | old_asrelax_cmd = "bgp bestpath as-path multipath-relax no-as-set" | |
028bcc88 DW |
1463 | found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd) |
1464 | ||
1465 | if re_asrelax_new and found_asrelax_old: | |
9b166171 | 1466 | deleted = True |
9fe88bc7 | 1467 | lines_to_del_to_del.append((ctx_keys, line)) |
028bcc88 DW |
1468 | lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd)) |
1469 | ||
701a0192 | 1470 | """ |
028bcc88 DW |
1471 | If we are modifying the BGP table-map we need to avoid a del/add and |
1472 | instead modify the table-map in place via an add. This is needed to | |
1473 | avoid installing all routes in the RIB the second the 'no table-map' | |
1474 | is issued. | |
701a0192 | 1475 | """ |
1476 | if line.startswith("table-map"): | |
1477 | found_table_map = line_exist(lines_to_add, ctx_keys, "table-map", False) | |
028bcc88 DW |
1478 | |
1479 | if found_table_map: | |
b3a39dc5 | 1480 | lines_to_del_to_del.append((ctx_keys, line)) |
c755f5c4 | 1481 | |
701a0192 | 1482 | """ |
028bcc88 DW |
1483 | More old-to-new config handling. ip import-table no longer accepts |
1484 | distance, but we honor the old syntax. But 'show running' shows only | |
1485 | the new syntax. This causes an unnecessary 'no import-table' followed | |
1486 | by the same old 'ip import-table' which causes perturbations in | |
1487 | announced routes leading to traffic blackholes. Fix this issue. | |
701a0192 | 1488 | """ |
1489 | re_importtbl = re.search("^ip\s+import-table\s+(\d+)$", ctx_keys[0]) | |
78e31f46 DD |
1490 | if re_importtbl: |
1491 | table_num = re_importtbl.group(1) | |
1492 | for ctx in lines_to_add: | |
701a0192 | 1493 | if ctx[0][0].startswith("ip import-table %s distance" % table_num): |
1494 | lines_to_del_to_del.append( | |
1495 | (("ip import-table %s" % table_num,), None) | |
1496 | ) | |
78e31f46 | 1497 | lines_to_add_to_del.append((ctx[0], None)) |
0bf7cc28 | 1498 | |
701a0192 | 1499 | """ |
7ac6afbd DS |
1500 | ip/ipv6 prefix-lists and access-lists can be specified without a seq number. |
1501 | However, the running config always adds 'seq x', where x is a number | |
1502 | incremented by 5 for every element of the prefix/access list. | |
1503 | So, ignore such lines as well. Sample prefix-list and acces-list lines: | |
0bf7cc28 DD |
1504 | ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32 |
1505 | ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32 | |
1506 | ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64 | |
7ac6afbd DS |
1507 | access-list FOO seq 5 permit 2.2.2.2/32 |
1508 | ipv6 access-list BAR seq 5 permit 2:2:2::2/128 | |
701a0192 | 1509 | """ |
7ac6afbd DS |
1510 | re_acl_pfxlst = re.search( |
1511 | "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$", | |
701a0192 | 1512 | ctx_keys[0], |
1513 | ) | |
7ac6afbd DS |
1514 | if re_acl_pfxlst: |
1515 | found = False | |
701a0192 | 1516 | tmpline = ( |
7ac6afbd DS |
1517 | re_acl_pfxlst.group(1) |
1518 | + re_acl_pfxlst.group(2) | |
1519 | + re_acl_pfxlst.group(3) | |
1520 | + re_acl_pfxlst.group(5) | |
1521 | + re_acl_pfxlst.group(6) | |
701a0192 | 1522 | ) |
0bf7cc28 DD |
1523 | for ctx in lines_to_add: |
1524 | if ctx[0][0] == tmpline: | |
1525 | lines_to_del_to_del.append((ctx_keys, None)) | |
1526 | lines_to_add_to_del.append(((tmpline,), None)) | |
7ac6afbd DS |
1527 | found = True |
1528 | """ | |
1529 | If prefix-lists or access-lists are being deleted and | |
1530 | not added (see comment above), add command with 'no' to | |
1531 | lines_to_add and remove from lines_to_del to improve | |
1532 | scaling performance. | |
1533 | """ | |
1534 | if found is False: | |
1535 | add_cmd = ("no " + ctx_keys[0],) | |
1536 | lines_to_add.append((add_cmd, None)) | |
1537 | lines_to_del_to_del.append((ctx_keys, None)) | |
0bf7cc28 | 1538 | |
701a0192 | 1539 | if ( |
1540 | len(ctx_keys) == 3 | |
1541 | and ctx_keys[0].startswith("router bgp") | |
1542 | and ctx_keys[1] == "address-family l2vpn evpn" | |
1543 | and ctx_keys[2].startswith("vni") | |
1544 | ): | |
5014d96f | 1545 | |
701a0192 | 1546 | re_route_target = ( |
1547 | re.search("^route-target import (.*)$", line) | |
1548 | if line is not None | |
1549 | else False | |
1550 | ) | |
5014d96f DW |
1551 | |
1552 | if re_route_target: | |
1553 | rt = re_route_target.group(1).strip() | |
1554 | route_target_import_line = line | |
1555 | route_target_export_line = "route-target export %s" % rt | |
1556 | route_target_both_line = "route-target both %s" % rt | |
1557 | ||
701a0192 | 1558 | found_route_target_export_line = line_exist( |
1559 | lines_to_del, ctx_keys, route_target_export_line | |
1560 | ) | |
1561 | found_route_target_both_line = line_exist( | |
1562 | lines_to_add, ctx_keys, route_target_both_line | |
1563 | ) | |
5014d96f | 1564 | |
701a0192 | 1565 | """ |
5014d96f DW |
1566 | If the running configs has |
1567 | route-target import 1:1 | |
1568 | route-target export 1:1 | |
1569 | ||
1570 | and the config we are reloading against has | |
1571 | route-target both 1:1 | |
1572 | ||
1573 | then we can ignore deleting the import/export and ignore adding the 'both' | |
701a0192 | 1574 | """ |
5014d96f DW |
1575 | if found_route_target_export_line and found_route_target_both_line: |
1576 | lines_to_del_to_del.append((ctx_keys, route_target_import_line)) | |
1577 | lines_to_del_to_del.append((ctx_keys, route_target_export_line)) | |
1578 | lines_to_add_to_del.append((ctx_keys, route_target_both_line)) | |
1579 | ||
6024e562 DS |
1580 | # Deleting static routes under a vrf can lead to time-outs if each is sent |
1581 | # as separate vtysh -c commands. Change them from being in lines_to_del and | |
1582 | # put the "no" form in lines_to_add | |
701a0192 | 1583 | if ctx_keys[0].startswith("vrf ") and line: |
1584 | if line.startswith("ip route") or line.startswith("ipv6 route"): | |
1585 | add_cmd = "no " + line | |
6024e562 DS |
1586 | lines_to_add.append((ctx_keys, add_cmd)) |
1587 | lines_to_del_to_del.append((ctx_keys, line)) | |
1588 | ||
9b166171 DW |
1589 | if not deleted: |
1590 | found_add_line = line_exist(lines_to_add, ctx_keys, line) | |
1591 | ||
1592 | if found_add_line: | |
1593 | lines_to_del_to_del.append((ctx_keys, line)) | |
1594 | lines_to_add_to_del.append((ctx_keys, line)) | |
1595 | else: | |
701a0192 | 1596 | """ |
9b166171 DW |
1597 | We have commands that used to be displayed in the global part |
1598 | of 'router bgp' that are now displayed under 'address-family ipv4 unicast' | |
1599 | ||
1600 | # old way | |
1601 | router bgp 64900 | |
1602 | neighbor ISL advertisement-interval 0 | |
1603 | ||
1604 | vs. | |
1605 | ||
1606 | # new way | |
1607 | router bgp 64900 | |
1608 | address-family ipv4 unicast | |
1609 | neighbor ISL advertisement-interval 0 | |
1610 | ||
1611 | Look to see if we are deleting it in one format just to add it back in the other | |
701a0192 | 1612 | """ |
1613 | if ( | |
1614 | ctx_keys[0].startswith("router bgp") | |
1615 | and len(ctx_keys) > 1 | |
1616 | and ctx_keys[1] == "address-family ipv4 unicast" | |
1617 | ): | |
9b166171 DW |
1618 | tmp_ctx_keys = list(ctx_keys)[:-1] |
1619 | tmp_ctx_keys = tuple(tmp_ctx_keys) | |
1620 | ||
1621 | found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line) | |
1622 | ||
1623 | if found_add_line: | |
1624 | lines_to_del_to_del.append((ctx_keys, line)) | |
1625 | lines_to_add_to_del.append((tmp_ctx_keys, line)) | |
9fe88bc7 DW |
1626 | |
1627 | for (ctx_keys, line) in lines_to_del_to_del: | |
1628 | lines_to_del.remove((ctx_keys, line)) | |
1629 | ||
1630 | for (ctx_keys, line) in lines_to_add_to_del: | |
1631 | lines_to_add.remove((ctx_keys, line)) | |
1632 | ||
1633 | return (lines_to_add, lines_to_del) | |
1634 | ||
1635 | ||
b05a1d3c DW |
1636 | def ignore_unconfigurable_lines(lines_to_add, lines_to_del): |
1637 | """ | |
1638 | There are certain commands that cannot be removed. Remove | |
1639 | those commands from lines_to_del. | |
1640 | """ | |
1641 | lines_to_del_to_del = [] | |
1642 | ||
1643 | for (ctx_keys, line) in lines_to_del: | |
1644 | ||
701a0192 | 1645 | if ( |
1646 | ctx_keys[0].startswith("frr version") | |
1647 | or ctx_keys[0].startswith("frr defaults") | |
1648 | or ctx_keys[0].startswith("username") | |
1649 | or ctx_keys[0].startswith("password") | |
1650 | or ctx_keys[0].startswith("line vty") | |
1651 | or | |
b05a1d3c DW |
1652 | # This is technically "no"able but if we did so frr-reload would |
1653 | # stop working so do not let the user shoot themselves in the foot | |
1654 | # by removing this. | |
701a0192 | 1655 | ctx_keys[0].startswith("service integrated-vtysh-config") |
1656 | ): | |
b05a1d3c | 1657 | |
663ece2f | 1658 | log.info('"%s" cannot be removed' % (ctx_keys[-1],)) |
b05a1d3c DW |
1659 | lines_to_del_to_del.append((ctx_keys, line)) |
1660 | ||
1661 | for (ctx_keys, line) in lines_to_del_to_del: | |
1662 | lines_to_del.remove((ctx_keys, line)) | |
1663 | ||
1664 | return (lines_to_add, lines_to_del) | |
1665 | ||
1666 | ||
2fc76430 DS |
1667 | def compare_context_objects(newconf, running): |
1668 | """ | |
1669 | Create a context diff for the two specified contexts | |
1670 | """ | |
1671 | ||
1672 | # Compare the two Config objects to find the lines that we need to add/del | |
1673 | lines_to_add = [] | |
1674 | lines_to_del = [] | |
4d7b695d SM |
1675 | pollist_to_del = [] |
1676 | seglist_to_del = [] | |
0e11b1e2 EDP |
1677 | pceconf_to_del = [] |
1678 | pcclist_to_del = [] | |
4d7b695d | 1679 | candidates_to_add = [] |
926ea62e | 1680 | delete_bgpd = False |
2fc76430 DS |
1681 | |
1682 | # Find contexts that are in newconf but not in running | |
1683 | # Find contexts that are in running but not in newconf | |
1c64265f | 1684 | for (running_ctx_keys, running_ctx) in iteritems(running.contexts): |
2fc76430 DS |
1685 | |
1686 | if running_ctx_keys not in newconf.contexts: | |
1687 | ||
ab5f8310 DW |
1688 | # We check that the len is 1 here so that we only look at ('router bgp 10') |
1689 | # and not ('router bgp 10', 'address-family ipv4 unicast'). The | |
926ea62e | 1690 | # latter could cause a false delete_bgpd positive if ipv4 unicast is in |
ab5f8310 DW |
1691 | # running but not in newconf. |
1692 | if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1: | |
926ea62e DW |
1693 | delete_bgpd = True |
1694 | lines_to_del.append((running_ctx_keys, None)) | |
1695 | ||
2a2b64e4 | 1696 | # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it |
701a0192 | 1697 | elif running_ctx_keys[0].startswith("interface") or running_ctx_keys[ |
1698 | 0 | |
1699 | ].startswith("vrf"): | |
768bf950 DD |
1700 | for line in running_ctx.lines: |
1701 | lines_to_del.append((running_ctx_keys, line)) | |
1702 | ||
926ea62e DW |
1703 | # If this is an address-family under 'router bgp' and we are already deleting the |
1704 | # entire 'router bgp' context then ignore this sub-context | |
701a0192 | 1705 | elif ( |
1706 | "router bgp" in running_ctx_keys[0] | |
1707 | and len(running_ctx_keys) > 1 | |
1708 | and delete_bgpd | |
1709 | ): | |
76f69d1c | 1710 | continue |
514665b9 | 1711 | |
5014d96f | 1712 | # Delete an entire vni sub-context under "address-family l2vpn evpn" |
701a0192 | 1713 | elif ( |
1714 | "router bgp" in running_ctx_keys[0] | |
1715 | and len(running_ctx_keys) > 2 | |
1716 | and running_ctx_keys[1].startswith("address-family l2vpn evpn") | |
1717 | and running_ctx_keys[2].startswith("vni ") | |
1718 | ): | |
5014d96f DW |
1719 | lines_to_del.append((running_ctx_keys, None)) |
1720 | ||
701a0192 | 1721 | elif ( |
1722 | "router bgp" in running_ctx_keys[0] | |
1723 | and len(running_ctx_keys) > 1 | |
1724 | and running_ctx_keys[1].startswith("address-family") | |
1725 | ): | |
afa2e8e1 DW |
1726 | # There's no 'no address-family' support and so we have to |
1727 | # delete each line individually again | |
1728 | for line in running_ctx.lines: | |
1729 | lines_to_del.append((running_ctx_keys, line)) | |
1730 | ||
6024e562 DS |
1731 | # Some commands can happen at higher counts that make |
1732 | # doing vtysh -c inefficient (and can time out.) For | |
1733 | # these commands, instead of adding them to lines_to_del, | |
1734 | # add the "no " version to lines_to_add. | |
7ac6afbd DS |
1735 | elif running_ctx_keys[0].startswith("ip route") or running_ctx_keys[ |
1736 | 0 | |
1737 | ].startswith("ipv6 route"): | |
701a0192 | 1738 | add_cmd = ("no " + running_ctx_keys[0],) |
6024e562 DS |
1739 | lines_to_add.append((add_cmd, None)) |
1740 | ||
e04ff92e EDP |
1741 | # if this an interface sub-subcontext in an address-family block in ldpd and |
1742 | # we are already deleting the whole context, then ignore this | |
701a0192 | 1743 | elif ( |
1744 | len(running_ctx_keys) > 2 | |
1745 | and running_ctx_keys[0].startswith("mpls ldp") | |
1746 | and running_ctx_keys[1].startswith("address-family") | |
1747 | and (running_ctx_keys[:2], None) in lines_to_del | |
1748 | ): | |
e04ff92e EDP |
1749 | continue |
1750 | ||
8239db83 | 1751 | # same thing for a pseudowire sub-context inside an l2vpn context |
d82c5d61 DS |
1752 | elif ( |
1753 | len(running_ctx_keys) > 1 | |
1754 | and running_ctx_keys[0].startswith("l2vpn") | |
1755 | and running_ctx_keys[1].startswith("member pseudowire") | |
1756 | and (running_ctx_keys[:1], None) in lines_to_del | |
1757 | ): | |
8239db83 EDP |
1758 | continue |
1759 | ||
4d7b695d SM |
1760 | # Segment routing and traffic engineering never need to be deleted |
1761 | elif ( | |
d82c5d61 | 1762 | running_ctx_keys[0].startswith("segment-routing") |
4d7b695d | 1763 | and len(running_ctx_keys) < 3 |
4d7b695d SM |
1764 | ): |
1765 | continue | |
1766 | ||
efba0985 SM |
1767 | # Neither the pcep command |
1768 | elif ( | |
1769 | len(running_ctx_keys) == 3 | |
d82c5d61 DS |
1770 | and running_ctx_keys[0].startswith("segment-routing") |
1771 | and running_ctx_keys[2].startswith("pcep") | |
efba0985 SM |
1772 | ): |
1773 | continue | |
1774 | ||
4d7b695d SM |
1775 | # Segment lists can only be deleted after we removed all the candidate paths that |
1776 | # use them, so add them to a separate array that is going to be appended at the end | |
1777 | elif ( | |
1778 | len(running_ctx_keys) == 3 | |
d82c5d61 DS |
1779 | and running_ctx_keys[0].startswith("segment-routing") |
1780 | and running_ctx_keys[2].startswith("segment-list") | |
4d7b695d SM |
1781 | ): |
1782 | seglist_to_del.append((running_ctx_keys, None)) | |
1783 | ||
1784 | # Policies must be deleted after there candidate path, to be sure | |
1785 | # we add them to a separate array that is going to be appended at the end | |
1786 | elif ( | |
1787 | len(running_ctx_keys) == 3 | |
d82c5d61 DS |
1788 | and running_ctx_keys[0].startswith("segment-routing") |
1789 | and running_ctx_keys[2].startswith("policy") | |
4d7b695d SM |
1790 | ): |
1791 | pollist_to_del.append((running_ctx_keys, None)) | |
1792 | ||
0e11b1e2 EDP |
1793 | # pce-config must be deleted after the pce, to be sure we add them |
1794 | # to a separate array that is going to be appended at the end | |
1795 | elif ( | |
1796 | len(running_ctx_keys) >= 4 | |
d82c5d61 DS |
1797 | and running_ctx_keys[0].startswith("segment-routing") |
1798 | and running_ctx_keys[3].startswith("pce-config") | |
0e11b1e2 EDP |
1799 | ): |
1800 | pceconf_to_del.append((running_ctx_keys, None)) | |
1801 | ||
1802 | # pcc must be deleted after the pce and pce-config too | |
1803 | elif ( | |
1804 | len(running_ctx_keys) >= 4 | |
d82c5d61 DS |
1805 | and running_ctx_keys[0].startswith("segment-routing") |
1806 | and running_ctx_keys[3].startswith("pcc") | |
0e11b1e2 EDP |
1807 | ): |
1808 | pcclist_to_del.append((running_ctx_keys, None)) | |
1809 | ||
2fc76430 | 1810 | # Non-global context |
701a0192 | 1811 | elif running_ctx_keys and not any( |
1812 | "address-family" in key for key in running_ctx_keys | |
1813 | ): | |
2fc76430 DS |
1814 | lines_to_del.append((running_ctx_keys, None)) |
1815 | ||
7918b335 DW |
1816 | elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys): |
1817 | lines_to_del.append((running_ctx_keys, None)) | |
1818 | ||
2fc76430 DS |
1819 | # Global context |
1820 | else: | |
1821 | for line in running_ctx.lines: | |
1822 | lines_to_del.append((running_ctx_keys, line)) | |
1823 | ||
4d7b695d SM |
1824 | # if we have some policies commands to delete, append them to lines_to_del |
1825 | if len(pollist_to_del) > 0: | |
1826 | lines_to_del.extend(pollist_to_del) | |
1827 | ||
1828 | # if we have some segment list commands to delete, append them to lines_to_del | |
1829 | if len(seglist_to_del) > 0: | |
1830 | lines_to_del.extend(seglist_to_del) | |
1831 | ||
0e11b1e2 EDP |
1832 | # if we have some pce list commands to delete, append them to lines_to_del |
1833 | if len(pceconf_to_del) > 0: | |
1834 | lines_to_del.extend(pceconf_to_del) | |
1835 | ||
1836 | # if we have some pcc list commands to delete, append them to lines_to_del | |
1837 | if len(pcclist_to_del) > 0: | |
1838 | lines_to_del.extend(pcclist_to_del) | |
1839 | ||
2fc76430 DS |
1840 | # Find the lines within each context to add |
1841 | # Find the lines within each context to del | |
1c64265f | 1842 | for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts): |
2fc76430 DS |
1843 | |
1844 | if newconf_ctx_keys in running.contexts: | |
1845 | running_ctx = running.contexts[newconf_ctx_keys] | |
1846 | ||
1847 | for line in newconf_ctx.lines: | |
1848 | if line not in running_ctx.dlines: | |
4d7b695d SM |
1849 | |
1850 | # candidate paths can only be added after the policy and segment list, | |
1851 | # so add them to a separate array that is going to be appended at the end | |
1852 | if ( | |
1853 | len(newconf_ctx_keys) == 3 | |
d82c5d61 DS |
1854 | and newconf_ctx_keys[0].startswith("segment-routing") |
1855 | and newconf_ctx_keys[2].startswith("policy ") | |
1856 | and line.startswith("candidate-path ") | |
4d7b695d SM |
1857 | ): |
1858 | candidates_to_add.append((newconf_ctx_keys, line)) | |
1859 | ||
1860 | else: | |
1861 | lines_to_add.append((newconf_ctx_keys, line)) | |
2fc76430 DS |
1862 | |
1863 | for line in running_ctx.lines: | |
1864 | if line not in newconf_ctx.dlines: | |
1865 | lines_to_del.append((newconf_ctx_keys, line)) | |
1866 | ||
1c64265f | 1867 | for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts): |
2fc76430 DS |
1868 | |
1869 | if newconf_ctx_keys not in running.contexts: | |
2fc76430 | 1870 | |
4d7b695d SM |
1871 | # candidate paths can only be added after the policy and segment list, |
1872 | # so add them to a separate array that is going to be appended at the end | |
1873 | if ( | |
1874 | len(newconf_ctx_keys) == 4 | |
d82c5d61 DS |
1875 | and newconf_ctx_keys[0].startswith("segment-routing") |
1876 | and newconf_ctx_keys[3].startswith("candidate-path") | |
4d7b695d SM |
1877 | ): |
1878 | candidates_to_add.append((newconf_ctx_keys, None)) | |
1879 | for line in newconf_ctx.lines: | |
1880 | candidates_to_add.append((newconf_ctx_keys, line)) | |
1881 | ||
1882 | else: | |
1883 | lines_to_add.append((newconf_ctx_keys, None)) | |
1884 | ||
1885 | for line in newconf_ctx.lines: | |
1886 | lines_to_add.append((newconf_ctx_keys, line)) | |
1887 | ||
1888 | # if we have some candidate paths commands to add, append them to lines_to_add | |
1889 | if len(candidates_to_add) > 0: | |
1890 | lines_to_add.extend(candidates_to_add) | |
2fc76430 | 1891 | |
eb9113df | 1892 | (lines_to_add, lines_to_del) = check_for_exit_vrf(lines_to_add, lines_to_del) |
701a0192 | 1893 | (lines_to_add, lines_to_del) = ignore_delete_re_add_lines( |
1894 | lines_to_add, lines_to_del | |
1895 | ) | |
934c84a0 | 1896 | (lines_to_add, lines_to_del) = delete_move_lines(lines_to_add, lines_to_del) |
701a0192 | 1897 | (lines_to_add, lines_to_del) = ignore_unconfigurable_lines( |
1898 | lines_to_add, lines_to_del | |
1899 | ) | |
9fe88bc7 | 1900 | |
926ea62e | 1901 | return (lines_to_add, lines_to_del) |
2fc76430 | 1902 | |
8ad1fe6c | 1903 | |
701a0192 | 1904 | if __name__ == "__main__": |
2fc76430 | 1905 | # Command line options |
701a0192 | 1906 | parser = argparse.ArgumentParser( |
1907 | description="Dynamically apply diff in frr configs" | |
1908 | ) | |
1909 | parser.add_argument( | |
1910 | "--input", help='Read running config from file instead of "show running"' | |
1911 | ) | |
2fc76430 | 1912 | group = parser.add_mutually_exclusive_group(required=True) |
701a0192 | 1913 | group.add_argument( |
1914 | "--reload", action="store_true", help="Apply the deltas", default=False | |
1915 | ) | |
1916 | group.add_argument( | |
1917 | "--test", action="store_true", help="Show the deltas", default=False | |
1918 | ) | |
9c782ad2 | 1919 | level_group = parser.add_mutually_exclusive_group() |
701a0192 | 1920 | level_group.add_argument( |
1921 | "--debug", | |
1922 | action="store_true", | |
1923 | help="Enable debugs (synonym for --log-level=debug)", | |
1924 | default=False, | |
1925 | ) | |
1926 | level_group.add_argument( | |
1927 | "--log-level", | |
1928 | help="Log level", | |
1929 | default="info", | |
1930 | choices=("critical", "error", "warning", "info", "debug"), | |
1931 | ) | |
1932 | parser.add_argument( | |
1933 | "--stdout", action="store_true", help="Log to STDOUT", default=False | |
1934 | ) | |
1935 | parser.add_argument( | |
1936 | "--pathspace", | |
1937 | "-N", | |
1938 | metavar="NAME", | |
1939 | help="Reload specified path/namespace", | |
1940 | default=None, | |
1941 | ) | |
1942 | parser.add_argument("filename", help="Location of new frr config file") | |
1943 | parser.add_argument( | |
1944 | "--overwrite", | |
1945 | action="store_true", | |
1946 | help="Overwrite frr.conf with running config output", | |
1947 | default=False, | |
1948 | ) | |
1949 | parser.add_argument( | |
1950 | "--bindir", help="path to the vtysh executable", default="/usr/bin" | |
1951 | ) | |
1952 | parser.add_argument( | |
1953 | "--confdir", help="path to the daemon config files", default="/etc/frr" | |
1954 | ) | |
1955 | parser.add_argument( | |
1956 | "--rundir", help="path for the temp config file", default="/var/run/frr" | |
1957 | ) | |
1958 | parser.add_argument( | |
1959 | "--vty_socket", | |
1960 | help="socket to be used by vtysh to connect to the daemons", | |
1961 | default=None, | |
1962 | ) | |
1963 | parser.add_argument( | |
1964 | "--daemon", help="daemon for which want to replace the config", default="" | |
1965 | ) | |
d9730542 | 1966 | |
2fc76430 DS |
1967 | args = parser.parse_args() |
1968 | ||
1969 | # Logging | |
1970 | # For --test log to stdout | |
d8e4c438 | 1971 | # For --reload log to /var/log/frr/frr-reload.log |
cc146ecc | 1972 | if args.test or args.stdout: |
701a0192 | 1973 | logging.basicConfig(format="%(asctime)s %(levelname)5s: %(message)s") |
926ea62e DW |
1974 | |
1975 | # Color the errors and warnings in red | |
701a0192 | 1976 | logging.addLevelName( |
1977 | logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR) | |
1978 | ) | |
1979 | logging.addLevelName( | |
1980 | logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING) | |
1981 | ) | |
926ea62e | 1982 | |
2fc76430 | 1983 | elif args.reload: |
701a0192 | 1984 | if not os.path.isdir("/var/log/frr/"): |
1985 | os.makedirs("/var/log/frr/") | |
2fc76430 | 1986 | |
701a0192 | 1987 | logging.basicConfig( |
1988 | filename="/var/log/frr/frr-reload.log", | |
1989 | format="%(asctime)s %(levelname)5s: %(message)s", | |
1990 | ) | |
2fc76430 DS |
1991 | |
1992 | # argparse should prevent this from happening but just to be safe... | |
1993 | else: | |
701a0192 | 1994 | raise Exception("Must specify --reload or --test") |
a782e613 | 1995 | log = logging.getLogger(__name__) |
2fc76430 | 1996 | |
9c782ad2 DE |
1997 | if args.debug: |
1998 | log.setLevel(logging.DEBUG) | |
1999 | else: | |
2000 | log.setLevel(args.log_level.upper()) | |
2001 | ||
6eee4767 DE |
2002 | if args.reload and not args.stdout: |
2003 | # Additionally send errors and above to STDOUT, with no metadata, | |
2004 | # when we are logging to a file. This specifically does not follow | |
2005 | # args.log_level, and is analagous to behaviour in earlier versions | |
2006 | # which additionally logged most errors using print(). | |
2007 | ||
2008 | stdout_hdlr = logging.StreamHandler(sys.stdout) | |
2009 | stdout_hdlr.setLevel(logging.ERROR) | |
2010 | stdout_hdlr.setFormatter(logging.Formatter()) | |
2011 | log.addHandler(stdout_hdlr) | |
2012 | ||
76f69d1c DW |
2013 | # Verify the new config file is valid |
2014 | if not os.path.isfile(args.filename): | |
6eee4767 | 2015 | log.error("Filename %s does not exist" % args.filename) |
76f69d1c DW |
2016 | sys.exit(1) |
2017 | ||
2018 | if not os.path.getsize(args.filename): | |
6eee4767 | 2019 | log.error("Filename %s is an empty file" % args.filename) |
76f69d1c DW |
2020 | sys.exit(1) |
2021 | ||
1a11d9cd EDP |
2022 | # Verify that confdir is correct |
2023 | if not os.path.isdir(args.confdir): | |
6eee4767 | 2024 | log.error("Confdir %s is not a valid path" % args.confdir) |
1a11d9cd EDP |
2025 | sys.exit(1) |
2026 | ||
2027 | # Verify that bindir is correct | |
701a0192 | 2028 | if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + "/vtysh"): |
6eee4767 | 2029 | log.error("Bindir %s is not a valid path to vtysh" % args.bindir) |
1a11d9cd EDP |
2030 | sys.exit(1) |
2031 | ||
fa18c6bb DL |
2032 | # verify that the vty_socket, if specified, is valid |
2033 | if args.vty_socket and not os.path.isdir(args.vty_socket): | |
701a0192 | 2034 | log.error("vty_socket %s is not a valid path" % args.vty_socket) |
fa18c6bb DL |
2035 | sys.exit(1) |
2036 | ||
d9730542 | 2037 | # verify that the daemon, if specified, is valid |
701a0192 | 2038 | if args.daemon and args.daemon not in [ |
2039 | "zebra", | |
2040 | "bgpd", | |
2041 | "fabricd", | |
2042 | "isisd", | |
2043 | "ospf6d", | |
2044 | "ospfd", | |
2045 | "pbrd", | |
2046 | "pimd", | |
2047 | "ripd", | |
2048 | "ripngd", | |
2049 | "sharpd", | |
2050 | "staticd", | |
2051 | "vrrpd", | |
2052 | "ldpd", | |
4d7b695d | 2053 | "pathd", |
ee96c52a | 2054 | "bfdd", |
701a0192 | 2055 | ]: |
4d7b695d SM |
2056 | msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon |
2057 | print(msg) | |
2058 | log.error(msg) | |
d9730542 EDP |
2059 | sys.exit(1) |
2060 | ||
a0a7dead | 2061 | vtysh = Vtysh(args.bindir, args.confdir, args.vty_socket, args.pathspace) |
663ece2f | 2062 | |
76f69d1c | 2063 | # Verify that 'service integrated-vtysh-config' is configured |
a0a7dead | 2064 | if args.pathspace: |
701a0192 | 2065 | vtysh_filename = args.confdir + "/" + args.pathspace + "/vtysh.conf" |
a0a7dead | 2066 | else: |
701a0192 | 2067 | vtysh_filename = args.confdir + "/vtysh.conf" |
6ac9179c | 2068 | service_integrated_vtysh_config = True |
76f69d1c | 2069 | |
f850d14d | 2070 | if os.path.isfile(vtysh_filename): |
701a0192 | 2071 | with open(vtysh_filename, "r") as fh: |
f850d14d DW |
2072 | for line in fh.readlines(): |
2073 | line = line.strip() | |
76f69d1c | 2074 | |
701a0192 | 2075 | if line == "no service integrated-vtysh-config": |
6ac9179c | 2076 | service_integrated_vtysh_config = False |
f850d14d | 2077 | break |
76f69d1c | 2078 | |
d9730542 | 2079 | if not service_integrated_vtysh_config and not args.daemon: |
701a0192 | 2080 | log.error( |
2081 | "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'" | |
2082 | ) | |
76f69d1c | 2083 | sys.exit(1) |
2fc76430 | 2084 | |
a782e613 | 2085 | log.info('Called via "%s"', str(args)) |
c50aceee | 2086 | |
2fc76430 | 2087 | # Create a Config object from the config generated by newconf |
663ece2f | 2088 | newconf = Config(vtysh) |
fdaee098 QY |
2089 | try: |
2090 | newconf.load_from_file(args.filename) | |
2091 | reload_ok = True | |
2092 | except VtyshException as ve: | |
2093 | log.error("vtysh failed to process new configuration: {}".format(ve)) | |
2094 | reload_ok = False | |
2fc76430 DS |
2095 | |
2096 | if args.test: | |
2097 | ||
2098 | # Create a Config object from the running config | |
663ece2f | 2099 | running = Config(vtysh) |
2fc76430 DS |
2100 | |
2101 | if args.input: | |
663ece2f | 2102 | running.load_from_file(args.input) |
2fc76430 | 2103 | else: |
663ece2f | 2104 | running.load_from_show_running(args.daemon) |
2fc76430 | 2105 | |
926ea62e | 2106 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
4a2587c6 | 2107 | lines_to_configure = [] |
2fc76430 DS |
2108 | |
2109 | if lines_to_del: | |
1c64265f | 2110 | print("\nLines To Delete") |
2111 | print("===============") | |
2fc76430 DS |
2112 | |
2113 | for (ctx_keys, line) in lines_to_del: | |
2114 | ||
701a0192 | 2115 | if line == "!": |
2fc76430 DS |
2116 | continue |
2117 | ||
701a0192 | 2118 | cmd = "\n".join(lines_to_config(ctx_keys, line, True)) |
4a2587c6 | 2119 | lines_to_configure.append(cmd) |
1c64265f | 2120 | print(cmd) |
2fc76430 DS |
2121 | |
2122 | if lines_to_add: | |
1c64265f | 2123 | print("\nLines To Add") |
2124 | print("============") | |
2fc76430 DS |
2125 | |
2126 | for (ctx_keys, line) in lines_to_add: | |
2127 | ||
701a0192 | 2128 | if line == "!": |
2fc76430 DS |
2129 | continue |
2130 | ||
701a0192 | 2131 | cmd = "\n".join(lines_to_config(ctx_keys, line, False)) |
4a2587c6 | 2132 | lines_to_configure.append(cmd) |
1c64265f | 2133 | print(cmd) |
2fc76430 | 2134 | |
2fc76430 DS |
2135 | elif args.reload: |
2136 | ||
2f52ad96 | 2137 | # We will not be able to do anything, go ahead and exit(1) |
663ece2f | 2138 | if not vtysh.is_config_available(): |
2f52ad96 DW |
2139 | sys.exit(1) |
2140 | ||
701a0192 | 2141 | log.debug("New Frr Config\n%s", newconf.get_lines()) |
2fc76430 DS |
2142 | |
2143 | # This looks a little odd but we have to do this twice...here is why | |
2144 | # If the user had this running bgp config: | |
4a2587c6 | 2145 | # |
2fc76430 DS |
2146 | # router bgp 10 |
2147 | # neighbor 1.1.1.1 remote-as 50 | |
2148 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 | 2149 | # |
2fc76430 | 2150 | # and this config in the newconf config file |
4a2587c6 | 2151 | # |
2fc76430 DS |
2152 | # router bgp 10 |
2153 | # neighbor 1.1.1.1 remote-as 999 | |
2154 | # neighbor 1.1.1.1 route-map FOO out | |
4a2587c6 DW |
2155 | # |
2156 | # | |
2fc76430 DS |
2157 | # Then the script will do |
2158 | # - no neighbor 1.1.1.1 remote-as 50 | |
2159 | # - neighbor 1.1.1.1 remote-as 999 | |
4a2587c6 | 2160 | # |
2fc76430 DS |
2161 | # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove |
2162 | # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the | |
2163 | # configs again to put this line back. | |
2164 | ||
1a8c43f1 | 2165 | # There are many keywords in FRR that can only appear one time under |
2ce26af1 DW |
2166 | # a context, take "bgp router-id" for example. If the config that we are |
2167 | # reloading against has the following: | |
2168 | # | |
2169 | # router bgp 10 | |
2170 | # bgp router-id 1.1.1.1 | |
2171 | # bgp router-id 2.2.2.2 | |
2172 | # | |
2173 | # The final config needs to contain "bgp router-id 2.2.2.2". On the | |
2174 | # first pass we will add "bgp router-id 2.2.2.2" but then on the second | |
2175 | # pass we will see that "bgp router-id 1.1.1.1" is missing and add that | |
2176 | # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the | |
2177 | # second pass to include all of the "adds" from the first pass. | |
2178 | lines_to_add_first_pass = [] | |
2179 | ||
2fc76430 | 2180 | for x in range(2): |
663ece2f DL |
2181 | running = Config(vtysh) |
2182 | running.load_from_show_running(args.daemon) | |
701a0192 | 2183 | log.debug("Running Frr Config (Pass #%d)\n%s", x, running.get_lines()) |
2fc76430 | 2184 | |
926ea62e | 2185 | (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) |
2fc76430 | 2186 | |
2ce26af1 DW |
2187 | if x == 0: |
2188 | lines_to_add_first_pass = lines_to_add | |
2189 | else: | |
2190 | lines_to_add.extend(lines_to_add_first_pass) | |
2191 | ||
53bddc22 | 2192 | # Only do deletes on the first pass. The reason being if we |
1a8c43f1 | 2193 | # configure a bgp neighbor via "neighbor swp1 interface" FRR |
53bddc22 DW |
2194 | # will automatically add: |
2195 | # | |
2196 | # interface swp1 | |
2197 | # ipv6 nd ra-interval 10 | |
2198 | # no ipv6 nd suppress-ra | |
2199 | # ! | |
2200 | # | |
2201 | # but those lines aren't in the config we are reloading against so | |
2202 | # on the 2nd pass they will show up in lines_to_del. This could | |
2203 | # apply to other scenarios as well where configuring FOO adds BAR | |
2204 | # to the config. | |
2205 | if lines_to_del and x == 0: | |
2fc76430 DS |
2206 | for (ctx_keys, line) in lines_to_del: |
2207 | ||
701a0192 | 2208 | if line == "!": |
2fc76430 DS |
2209 | continue |
2210 | ||
4a2587c6 DW |
2211 | # 'no' commands are tricky, we can't just put them in a file and |
2212 | # vtysh -f that file. See the next comment for an explanation | |
2213 | # of their quirks | |
663ece2f | 2214 | cmd = lines_to_config(ctx_keys, line, True) |
2fc76430 DS |
2215 | original_cmd = cmd |
2216 | ||
d8e4c438 | 2217 | # Some commands in frr are picky about taking a "no" of the entire line. |
76f69d1c DW |
2218 | # OSPF is bad about this, you can't "no" the entire line, you have to "no" |
2219 | # only the beginning. If we hit one of these command an exception will be | |
2220 | # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again. | |
4a2587c6 | 2221 | # |
76f69d1c | 2222 | # Example: |
d8e4c438 DS |
2223 | # frr(config-if)# ip ospf authentication message-digest 1.1.1.1 |
2224 | # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1 | |
76f69d1c | 2225 | # % Unknown command. |
d8e4c438 | 2226 | # frr(config-if)# no ip ospf authentication message-digest |
76f69d1c | 2227 | # % Unknown command. |
d8e4c438 DS |
2228 | # frr(config-if)# no ip ospf authentication |
2229 | # frr(config-if)# | |
2fc76430 | 2230 | |
641b032e | 2231 | stdouts = [] |
2fc76430 | 2232 | while True: |
2fc76430 | 2233 | try: |
641b032e | 2234 | vtysh(["configure"] + cmd, stdouts) |
2fc76430 | 2235 | |
663ece2f | 2236 | except VtyshException: |
2fc76430 DS |
2237 | |
2238 | # - Pull the last entry from cmd (this would be | |
2239 | # 'no ip ospf authentication message-digest 1.1.1.1' in | |
2240 | # our example above | |
2241 | # - Split that last entry by whitespace and drop the last word | |
701a0192 | 2242 | log.info("Failed to execute %s", " ".join(cmd)) |
2243 | last_arg = cmd[-1].split(" ") | |
2fc76430 DS |
2244 | |
2245 | if len(last_arg) <= 2: | |
701a0192 | 2246 | log.error( |
2247 | '"%s" we failed to remove this command', | |
2248 | " -- ".join(original_cmd), | |
2249 | ) | |
641b032e CS |
2250 | # Log first error msg for original_cmd |
2251 | if stdouts: | |
2252 | log.error(stdouts[0]) | |
f26070fc | 2253 | reload_ok = False |
2fc76430 DS |
2254 | break |
2255 | ||
2256 | new_last_arg = last_arg[0:-1] | |
701a0192 | 2257 | cmd[-1] = " ".join(new_last_arg) |
2fc76430 | 2258 | else: |
701a0192 | 2259 | log.info('Executed "%s"', " ".join(cmd)) |
2fc76430 DS |
2260 | break |
2261 | ||
2fc76430 | 2262 | if lines_to_add: |
4a2587c6 DW |
2263 | lines_to_configure = [] |
2264 | ||
2fc76430 DS |
2265 | for (ctx_keys, line) in lines_to_add: |
2266 | ||
701a0192 | 2267 | if line == "!": |
2fc76430 DS |
2268 | continue |
2269 | ||
6024e562 DS |
2270 | # Don't run "no" commands twice since they can error |
2271 | # out the second time due to first deletion | |
701a0192 | 2272 | if x == 1 and ctx_keys[0].startswith("no "): |
6024e562 DS |
2273 | continue |
2274 | ||
701a0192 | 2275 | cmd = "\n".join(lines_to_config(ctx_keys, line, False)) + "\n" |
4a2587c6 DW |
2276 | lines_to_configure.append(cmd) |
2277 | ||
2278 | if lines_to_configure: | |
701a0192 | 2279 | random_string = "".join( |
2280 | random.SystemRandom().choice( | |
2281 | string.ascii_uppercase + string.digits | |
2282 | ) | |
2283 | for _ in range(6) | |
2284 | ) | |
4a2587c6 | 2285 | |
1a11d9cd | 2286 | filename = args.rundir + "/reload-%s.txt" % random_string |
a782e613 | 2287 | log.info("%s content\n%s" % (filename, pformat(lines_to_configure))) |
4a2587c6 | 2288 | |
701a0192 | 2289 | with open(filename, "w") as fh: |
4a2587c6 | 2290 | for line in lines_to_configure: |
701a0192 | 2291 | fh.write(line + "\n") |
825be4c2 | 2292 | |
596074af | 2293 | try: |
663ece2f DL |
2294 | vtysh.exec_file(filename) |
2295 | except VtyshException as e: | |
2296 | log.warning("frr-reload.py failed due to\n%s" % e.args) | |
596074af | 2297 | reload_ok = False |
4a2587c6 | 2298 | os.unlink(filename) |
2fc76430 | 2299 | |
4b78098d | 2300 | # Make these changes persistent |
701a0192 | 2301 | target = str(args.confdir + "/frr.conf") |
d9730542 | 2302 | if args.overwrite or (not args.daemon and args.filename != target): |
701a0192 | 2303 | vtysh("write") |
825be4c2 DW |
2304 | |
2305 | if not reload_ok: | |
2306 | sys.exit(1) |