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