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