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