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