]> git.proxmox.com Git - mirror_frr.git/blob - tools/frr-reload.py
Merge pull request #5285 from ton31337/fix/send_BGP_NOTIFY_CEASE_PEER_UNCONFIG_after_...
[mirror_frr.git] / tools / frr-reload.py
1 #!/usr/bin/python
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 copy
34 import logging
35 import os
36 import random
37 import re
38 import string
39 import subprocess
40 import sys
41 from collections import OrderedDict
42 try:
43 from ipaddress import IPv6Address, ip_network
44 except ImportError:
45 from ipaddr import IPv6Address, IPNetwork
46 from pprint import pformat
47
48 try:
49 dict.iteritems
50 except AttributeError:
51 # Python 3
52 def iteritems(d):
53 return iter(d.items())
54 else:
55 # Python 2
56 def iteritems(d):
57 return d.iteritems()
58
59 log = logging.getLogger(__name__)
60
61
62 class VtyshMarkException(Exception):
63 pass
64
65
66 class Context(object):
67
68 """
69 A Context object represents a section of frr configuration such as:
70 !
71 interface swp3
72 description swp3 -> r8's swp1
73 ipv6 nd suppress-ra
74 link-detect
75 !
76
77 or a single line context object such as this:
78
79 ip forwarding
80
81 """
82
83 def __init__(self, keys, lines):
84 self.keys = keys
85 self.lines = lines
86
87 # Keep a dictionary of the lines, this is to make it easy to tell if a
88 # line exists in this Context
89 self.dlines = OrderedDict()
90
91 for ligne in lines:
92 self.dlines[ligne] = True
93
94 def add_lines(self, lines):
95 """
96 Add lines to specified context
97 """
98
99 self.lines.extend(lines)
100
101 for ligne in lines:
102 self.dlines[ligne] = True
103
104
105 class Config(object):
106
107 """
108 A frr configuration is stored in a Config object. A Config object
109 contains a dictionary of Context objects where the Context keys
110 ('router ospf' for example) are our dictionary key.
111 """
112
113 def __init__(self):
114 self.lines = []
115 self.contexts = OrderedDict()
116
117 def load_from_file(self, filename):
118 """
119 Read configuration from specified file and slurp it into internal memory
120 The internal representation has been marked appropriately by passing it
121 through vtysh with the -m parameter
122 """
123 log.info('Loading Config object from file %s', filename)
124
125 try:
126 file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename],
127 stderr=subprocess.STDOUT)
128 except subprocess.CalledProcessError as e:
129 ve = VtyshMarkException(e)
130 ve.output = e.output
131 raise ve
132
133 for line in file_output.decode('utf-8').split('\n'):
134 line = line.strip()
135
136 # Compress duplicate whitespaces
137 line = ' '.join(line.split())
138
139 if ":" in line:
140 qv6_line = get_normalized_ipv6_line(line)
141 self.lines.append(qv6_line)
142 else:
143 self.lines.append(line)
144
145 self.load_contexts()
146
147 def load_from_show_running(self):
148 """
149 Read running configuration and slurp it into internal memory
150 The internal representation has been marked appropriately by passing it
151 through vtysh with the -m parameter
152 """
153 log.info('Loading Config object from vtysh show running')
154
155 try:
156 config_text = subprocess.check_output(
157 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
158 shell=True, stderr=subprocess.STDOUT)
159 except subprocess.CalledProcessError as e:
160 ve = VtyshMarkException(e)
161 ve.output = e.output
162 raise ve
163
164 for line in config_text.decode('utf-8').split('\n'):
165 line = line.strip()
166
167 if (line == 'Building configuration...' or
168 line == 'Current configuration:' or
169 not line):
170 continue
171
172 self.lines.append(line)
173
174 self.load_contexts()
175
176 def get_lines(self):
177 """
178 Return the lines read in from the configuration
179 """
180
181 return '\n'.join(self.lines)
182
183 def get_contexts(self):
184 """
185 Return the parsed context as strings for display, log etc.
186 """
187
188 for (_, ctx) in sorted(iteritems(self.contexts)):
189 print(str(ctx) + '\n')
190
191 def save_contexts(self, key, lines):
192 """
193 Save the provided key and lines as a context
194 """
195
196 if not key:
197 return
198
199 '''
200 IP addresses specified in "network" statements, "ip prefix-lists"
201 etc. can differ in the host part of the specification the user
202 provides and what the running config displays. For example, user
203 can specify 11.1.1.1/24, and the running config displays this as
204 11.1.1.0/24. Ensure we don't do a needless operation for such
205 lines. IS-IS & OSPFv3 have no "network" support.
206 '''
207 re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0])
208 if re_key_rt:
209 addr = re_key_rt.group(2)
210 if '/' in addr:
211 try:
212 if 'ipaddress' not in sys.modules:
213 newaddr = IPNetwork(addr)
214 key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
215 newaddr.network,
216 newaddr.prefixlen,
217 re_key_rt.group(3))
218 else:
219 newaddr = ip_network(addr, strict=False)
220 key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
221 str(newaddr.network_address),
222 newaddr.prefixlen,
223 re_key_rt.group(3))
224 except ValueError:
225 pass
226
227 re_key_rt = re.match(
228 r'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
229 key[0]
230 )
231 if re_key_rt:
232 addr = re_key_rt.group(4)
233 if '/' in addr:
234 try:
235 if 'ipaddress' not in sys.modules:
236 newaddr = '%s/%s' % (IPNetwork(addr).network,
237 IPNetwork(addr).prefixlen)
238 else:
239 network_addr = ip_network(addr, strict=False)
240 newaddr = '%s/%s' % (str(network_addr.network_address),
241 network_addr.prefixlen)
242 except ValueError:
243 newaddr = addr
244 else:
245 newaddr = addr
246
247 legestr = re_key_rt.group(5)
248 re_lege = re.search(r'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr)
249 if re_lege:
250 legestr = '%sge %s le %s%s' % (re_lege.group(1),
251 re_lege.group(3),
252 re_lege.group(2),
253 re_lege.group(4))
254 re_lege = re.search(r'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr)
255
256 if (re_lege and ((re_key_rt.group(1) == "ip" and
257 re_lege.group(3) == "32") or
258 (re_key_rt.group(1) == "ipv6" and
259 re_lege.group(3) == "128"))):
260 legestr = '%sge %s%s' % (re_lege.group(1),
261 re_lege.group(2),
262 re_lege.group(4))
263
264 key[0] = '%s prefix-list%s%s %s%s' % (re_key_rt.group(1),
265 re_key_rt.group(2),
266 re_key_rt.group(3),
267 newaddr,
268 legestr)
269
270 if lines and key[0].startswith('router bgp'):
271 newlines = []
272 for line in lines:
273 re_net = re.match(r'network\s+([A-Fa-f:.0-9/]+)(.*)$', line)
274 if re_net:
275 addr = re_net.group(1)
276 if '/' not in addr and key[0].startswith('router bgp'):
277 # This is most likely an error because with no
278 # prefixlen, BGP treats the prefixlen as 8
279 addr = addr + '/8'
280
281 try:
282 if 'ipaddress' not in sys.modules:
283 newaddr = IPNetwork(addr)
284 line = 'network %s/%s %s' % (newaddr.network,
285 newaddr.prefixlen,
286 re_net.group(2))
287 else:
288 network_addr = ip_network(addr, strict=False)
289 line = 'network %s/%s %s' % (str(network_addr.network_address),
290 network_addr.prefixlen,
291 re_net.group(2))
292 newlines.append(line)
293 except ValueError:
294 # Really this should be an error. Whats a network
295 # without an IP Address following it ?
296 newlines.append(line)
297 else:
298 newlines.append(line)
299 lines = newlines
300
301 '''
302 More fixups in user specification and what running config shows.
303 "null0" in routes must be replaced by Null0.
304 '''
305 if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and
306 'null0' in key[0]):
307 key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0])
308
309 if lines:
310 if tuple(key) not in self.contexts:
311 ctx = Context(tuple(key), lines)
312 self.contexts[tuple(key)] = ctx
313 else:
314 ctx = self.contexts[tuple(key)]
315 ctx.add_lines(lines)
316
317 else:
318 if tuple(key) not in self.contexts:
319 ctx = Context(tuple(key), [])
320 self.contexts[tuple(key)] = ctx
321
322 def load_contexts(self):
323 """
324 Parse the configuration and create contexts for each appropriate block
325 """
326
327 current_context_lines = []
328 ctx_keys = []
329
330 '''
331 The end of a context is flagged via the 'end' keyword:
332
333 !
334 interface swp52
335 ipv6 nd suppress-ra
336 link-detect
337 !
338 end
339 router bgp 10
340 bgp router-id 10.0.0.1
341 bgp log-neighbor-changes
342 no bgp default ipv4-unicast
343 neighbor EBGP peer-group
344 neighbor EBGP advertisement-interval 1
345 neighbor EBGP timers connect 10
346 neighbor 2001:40:1:4::6 remote-as 40
347 neighbor 2001:40:1:8::a remote-as 40
348 !
349 end
350 address-family ipv6
351 neighbor IBGPv6 activate
352 neighbor 2001:10::2 peer-group IBGPv6
353 neighbor 2001:10::3 peer-group IBGPv6
354 exit-address-family
355 !
356 end
357 address-family evpn
358 neighbor LEAF activate
359 advertise-all-vni
360 vni 10100
361 rd 65000:10100
362 route-target import 10.1.1.1:10100
363 route-target export 10.1.1.1:10100
364 exit-vni
365 exit-address-family
366 !
367 end
368 router ospf
369 ospf router-id 10.0.0.1
370 log-adjacency-changes detail
371 timers throttle spf 0 50 5000
372 !
373 end
374 '''
375
376 # The code assumes that its working on the output from the "vtysh -m"
377 # command. That provides the appropriate markers to signify end of
378 # a context. This routine uses that to build the contexts for the
379 # config.
380 #
381 # There are single line contexts such as "log file /media/node/zebra.log"
382 # and multi-line contexts such as "router ospf" and subcontexts
383 # within a context such as "address-family" within "router bgp"
384 # In each of these cases, the first line of the context becomes the
385 # key of the context. So "router bgp 10" is the key for the non-address
386 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
387 # the key for the subcontext and so on.
388 ctx_keys = []
389 main_ctx_key = []
390 new_ctx = True
391
392 # the keywords that we know are single line contexts. bgp in this case
393 # is not the main router bgp block, but enabling multi-instance
394 oneline_ctx_keywords = ("access-list ",
395 "agentx",
396 "allow-external-route-update",
397 "bgp ",
398 "debug ",
399 "domainname ",
400 "dump ",
401 "enable ",
402 "frr ",
403 "hostname ",
404 "ip ",
405 "ipv6 ",
406 "log ",
407 "mpls",
408 "no ",
409 "password ",
410 "ptm-enable",
411 "router-id ",
412 "service ",
413 "table ",
414 "username ",
415 "zebra ",
416 "vrrp autoconfigure")
417
418 for line in self.lines:
419
420 if not line:
421 continue
422
423 if line.startswith('!') or line.startswith('#'):
424 continue
425
426 # one line contexts
427 if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords):
428 self.save_contexts(ctx_keys, current_context_lines)
429
430 # Start a new context
431 main_ctx_key = []
432 ctx_keys = [line, ]
433 current_context_lines = []
434
435 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
436 self.save_contexts(ctx_keys, current_context_lines)
437 new_ctx = True
438
439 elif line == "end":
440 self.save_contexts(ctx_keys, current_context_lines)
441 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
442
443 # Start a new context
444 new_ctx = True
445 main_ctx_key = []
446 ctx_keys = []
447 current_context_lines = []
448
449 elif line == "exit-vrf":
450 self.save_contexts(ctx_keys, current_context_lines)
451 current_context_lines.append(line)
452 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
453
454 #Start a new context
455 new_ctx = True
456 main_ctx_key = []
457 ctx_keys = []
458 current_context_lines = []
459
460 elif line in ["exit-address-family", "exit", "exit-vnc"]:
461 # if this exit is for address-family ipv4 unicast, ignore the pop
462 if main_ctx_key:
463 self.save_contexts(ctx_keys, current_context_lines)
464
465 # Start a new context
466 ctx_keys = copy.deepcopy(main_ctx_key)
467 current_context_lines = []
468 log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
469
470 elif line == "exit-vni":
471 if sub_main_ctx_key:
472 self.save_contexts(ctx_keys, current_context_lines)
473
474 # Start a new context
475 ctx_keys = copy.deepcopy(sub_main_ctx_key)
476 current_context_lines = []
477 log.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line, ctx_keys)
478
479 elif new_ctx is True:
480 if not main_ctx_key:
481 ctx_keys = [line, ]
482 else:
483 ctx_keys = copy.deepcopy(main_ctx_key)
484 main_ctx_key = []
485
486 current_context_lines = []
487 new_ctx = False
488 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
489 elif (line.startswith("address-family ") or
490 line.startswith("vnc defaults") or
491 line.startswith("vnc l2-group") or
492 line.startswith("vnc nve-group")):
493 main_ctx_key = []
494
495 # Save old context first
496 self.save_contexts(ctx_keys, current_context_lines)
497 current_context_lines = []
498 main_ctx_key = copy.deepcopy(ctx_keys)
499 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
500
501 if line == "address-family ipv6":
502 ctx_keys.append("address-family ipv6 unicast")
503 elif line == "address-family ipv4":
504 ctx_keys.append("address-family ipv4 unicast")
505 elif line == "address-family evpn":
506 ctx_keys.append("address-family l2vpn evpn")
507 else:
508 ctx_keys.append(line)
509
510 elif ((line.startswith("vni ") and
511 len(ctx_keys) == 2 and
512 ctx_keys[0].startswith('router bgp') and
513 ctx_keys[1] == 'address-family l2vpn evpn')):
514
515 # Save old context first
516 self.save_contexts(ctx_keys, current_context_lines)
517 current_context_lines = []
518 sub_main_ctx_key = copy.deepcopy(ctx_keys)
519 log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line)
520 ctx_keys.append(line)
521
522 else:
523 # Continuing in an existing context, add non-commented lines to it
524 current_context_lines.append(line)
525 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
526
527 # Save the context of the last one
528 self.save_contexts(ctx_keys, current_context_lines)
529
530
531 def line_to_vtysh_conft(ctx_keys, line, delete):
532 """
533 Return the vtysh command for the specified context line
534 """
535
536 cmd = []
537 cmd.append('vtysh')
538 cmd.append('-c')
539 cmd.append('conf t')
540
541 if line:
542 for ctx_key in ctx_keys:
543 cmd.append('-c')
544 cmd.append(ctx_key)
545
546 line = line.lstrip()
547
548 if delete:
549 cmd.append('-c')
550
551 if line.startswith('no '):
552 cmd.append('%s' % line[3:])
553 else:
554 cmd.append('no %s' % line)
555
556 else:
557 cmd.append('-c')
558 cmd.append(line)
559
560 # If line is None then we are typically deleting an entire
561 # context ('no router ospf' for example)
562 else:
563
564 if delete:
565
566 # Only put the 'no' on the last sub-context
567 for ctx_key in ctx_keys:
568 cmd.append('-c')
569
570 if ctx_key == ctx_keys[-1]:
571 cmd.append('no %s' % ctx_key)
572 else:
573 cmd.append('%s' % ctx_key)
574 else:
575 for ctx_key in ctx_keys:
576 cmd.append('-c')
577 cmd.append(ctx_key)
578
579 return cmd
580
581
582 def line_for_vtysh_file(ctx_keys, line, delete):
583 """
584 Return the command as it would appear in frr.conf
585 """
586 cmd = []
587
588 if line:
589 for (i, ctx_key) in enumerate(ctx_keys):
590 cmd.append(' ' * i + ctx_key)
591
592 line = line.lstrip()
593 indent = len(ctx_keys) * ' '
594
595 if delete:
596 if line.startswith('no '):
597 cmd.append('%s%s' % (indent, line[3:]))
598 else:
599 cmd.append('%sno %s' % (indent, line))
600
601 else:
602 cmd.append(indent + line)
603
604 # If line is None then we are typically deleting an entire
605 # context ('no router ospf' for example)
606 else:
607 if delete:
608
609 # Only put the 'no' on the last sub-context
610 for ctx_key in ctx_keys:
611
612 if ctx_key == ctx_keys[-1]:
613 cmd.append('no %s' % ctx_key)
614 else:
615 cmd.append('%s' % ctx_key)
616 else:
617 for ctx_key in ctx_keys:
618 cmd.append(ctx_key)
619
620 cmd = '\n' + '\n'.join(cmd)
621
622 # There are some commands that are on by default so their "no" form will be
623 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
624 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
625 # not by doing a "no no bgp default ipv4-unicast"
626 cmd = cmd.replace('no no ', '')
627
628 return cmd
629
630
631 def get_normalized_ipv6_line(line):
632 """
633 Return a normalized IPv6 line as produced by frr,
634 with all letters in lower case and trailing and leading
635 zeros removed, and only the network portion present if
636 the IPv6 word is a network
637 """
638 norm_line = ""
639 words = line.split(' ')
640 for word in words:
641 if ":" in word:
642 norm_word = None
643 if "/" in word:
644 try:
645 if 'ipaddress' not in sys.modules:
646 v6word = IPNetwork(word)
647 norm_word = '%s/%s' % (v6word.network, v6word.prefixlen)
648 else:
649 v6word = ip_network(word, strict=False)
650 norm_word = '%s/%s' % (str(v6word.network_address), v6word.prefixlen)
651 except ValueError:
652 pass
653 if not norm_word:
654 try:
655 norm_word = '%s' % IPv6Address(word)
656 except ValueError:
657 norm_word = word
658 else:
659 norm_word = word
660 norm_line = norm_line + " " + norm_word
661
662 return norm_line.strip()
663
664
665 def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
666 for (ctx_keys, line) in lines:
667 if ctx_keys == target_ctx_keys:
668 if exact_match:
669 if line == target_line:
670 return True
671 else:
672 if line.startswith(target_line):
673 return True
674 return False
675
676
677 def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
678
679 # Quite possibly the most confusing (while accurate) variable names in history
680 lines_to_add_to_del = []
681 lines_to_del_to_del = []
682
683 for (ctx_keys, line) in lines_to_del:
684 deleted = False
685
686 if ctx_keys[0].startswith('router bgp') and line:
687
688 if line.startswith('neighbor '):
689 '''
690 BGP changed how it displays swpX peers that are part of peer-group. Older
691 versions of frr would display these on separate lines:
692 neighbor swp1 interface
693 neighbor swp1 peer-group FOO
694
695 but today we display via a single line
696 neighbor swp1 interface peer-group FOO
697
698 This change confuses frr-reload.py so check to see if we are deleting
699 neighbor swp1 interface peer-group FOO
700
701 and adding
702 neighbor swp1 interface
703 neighbor swp1 peer-group FOO
704
705 If so then chop the del line and the corresponding add lines
706 '''
707
708 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
709 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
710
711 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
712 swpx_interface = None
713 swpx_peergroup = None
714
715 if re_swpx_int_peergroup:
716 swpx = re_swpx_int_peergroup.group(1)
717 peergroup = re_swpx_int_peergroup.group(2)
718 swpx_interface = "neighbor %s interface" % swpx
719 elif re_swpx_int_v6only_peergroup:
720 swpx = re_swpx_int_v6only_peergroup.group(1)
721 peergroup = re_swpx_int_v6only_peergroup.group(2)
722 swpx_interface = "neighbor %s interface v6only" % swpx
723
724 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
725 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
726 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
727 tmp_ctx_keys = tuple(list(ctx_keys))
728
729 if not found_add_swpx_peergroup:
730 tmp_ctx_keys = list(ctx_keys)
731 tmp_ctx_keys.append('address-family ipv4 unicast')
732 tmp_ctx_keys = tuple(tmp_ctx_keys)
733 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
734
735 if not found_add_swpx_peergroup:
736 tmp_ctx_keys = list(ctx_keys)
737 tmp_ctx_keys.append('address-family ipv6 unicast')
738 tmp_ctx_keys = tuple(tmp_ctx_keys)
739 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
740
741 if found_add_swpx_interface and found_add_swpx_peergroup:
742 deleted = True
743 lines_to_del_to_del.append((ctx_keys, line))
744 lines_to_add_to_del.append((ctx_keys, swpx_interface))
745 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
746
747 '''
748 Changing the bfd timers on neighbors is allowed without doing
749 a delete/add process. Since doing a "no neighbor blah bfd ..."
750 will cause the peer to bounce unnecessarily, just skip the delete
751 and just do the add.
752 '''
753 re_nbr_bfd_timers = re.search(r'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line)
754
755 if re_nbr_bfd_timers:
756 nbr = re_nbr_bfd_timers.group(1)
757 bfd_nbr = "neighbor %s" % nbr
758
759 for (ctx_keys, add_line) in lines_to_add:
760 re_add_nbr_bfd_timers = re.search(r'neighbor bfd_nbr bfd (\S+) (\S+) (\S+)', add_line)
761
762 if re_add_nbr_bfd_timers:
763 found_add_bfd_nbr = line_exist(lines_to_add, ctx_keys, bfd_nbr, False)
764
765 if found_add_bfd_nbr:
766 lines_to_del_to_del.append((ctx_keys, line))
767
768 '''
769 We changed how we display the neighbor interface command. Older
770 versions of frr would display the following:
771 neighbor swp1 interface
772 neighbor swp1 remote-as external
773 neighbor swp1 capability extended-nexthop
774
775 but today we display via a single line
776 neighbor swp1 interface remote-as external
777
778 and capability extended-nexthop is no longer needed because we
779 automatically enable it when the neighbor is of type interface.
780
781 This change confuses frr-reload.py so check to see if we are deleting
782 neighbor swp1 interface remote-as (external|internal|ASNUM)
783
784 and adding
785 neighbor swp1 interface
786 neighbor swp1 remote-as (external|internal|ASNUM)
787 neighbor swp1 capability extended-nexthop
788
789 If so then chop the del line and the corresponding add lines
790 '''
791 re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
792 re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
793
794 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
795 swpx_interface = None
796 swpx_remoteas = None
797
798 if re_swpx_int_remoteas:
799 swpx = re_swpx_int_remoteas.group(1)
800 remoteas = re_swpx_int_remoteas.group(2)
801 swpx_interface = "neighbor %s interface" % swpx
802 elif re_swpx_int_v6only_remoteas:
803 swpx = re_swpx_int_v6only_remoteas.group(1)
804 remoteas = re_swpx_int_v6only_remoteas.group(2)
805 swpx_interface = "neighbor %s interface v6only" % swpx
806
807 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
808 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
809 found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
810 tmp_ctx_keys = tuple(list(ctx_keys))
811
812 if found_add_swpx_interface and found_add_swpx_remoteas:
813 deleted = True
814 lines_to_del_to_del.append((ctx_keys, line))
815 lines_to_add_to_del.append((ctx_keys, swpx_interface))
816 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
817
818 '''
819 We made the 'bgp bestpath as-path multipath-relax' command
820 automatically assume 'no-as-set' since the lack of this option caused
821 weird routing problems. When the running config is shown in
822 releases with this change, the no-as-set keyword is not shown as it
823 is the default. This causes frr-reload to unnecessarily unapply
824 this option only to apply it back again, causing unnecessary session
825 resets.
826 '''
827 if 'multipath-relax' in line:
828 re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line)
829 old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set'
830 found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
831
832 if re_asrelax_new and found_asrelax_old:
833 deleted = True
834 lines_to_del_to_del.append((ctx_keys, line))
835 lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
836
837 '''
838 If we are modifying the BGP table-map we need to avoid a del/add and
839 instead modify the table-map in place via an add. This is needed to
840 avoid installing all routes in the RIB the second the 'no table-map'
841 is issued.
842 '''
843 if line.startswith('table-map'):
844 found_table_map = line_exist(lines_to_add, ctx_keys, 'table-map', False)
845
846 if found_table_map:
847 lines_to_del_to_del.append((ctx_keys, line))
848
849 '''
850 More old-to-new config handling. ip import-table no longer accepts
851 distance, but we honor the old syntax. But 'show running' shows only
852 the new syntax. This causes an unnecessary 'no import-table' followed
853 by the same old 'ip import-table' which causes perturbations in
854 announced routes leading to traffic blackholes. Fix this issue.
855 '''
856 re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0])
857 if re_importtbl:
858 table_num = re_importtbl.group(1)
859 for ctx in lines_to_add:
860 if ctx[0][0].startswith('ip import-table %s distance' % table_num):
861 lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
862 lines_to_add_to_del.append((ctx[0], None))
863
864 '''
865 ip/ipv6 prefix-list can be specified without a seq number. However,
866 the running config always adds 'seq x', where x is a number incremented
867 by 5 for every element, to the prefix list. So, ignore such lines as
868 well. Sample prefix-list lines:
869 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
870 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
871 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
872 '''
873 re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
874 ctx_keys[0])
875 if re_ip_pfxlst:
876 tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) +
877 re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) +
878 re_ip_pfxlst.group(6))
879 for ctx in lines_to_add:
880 if ctx[0][0] == tmpline:
881 lines_to_del_to_del.append((ctx_keys, None))
882 lines_to_add_to_del.append(((tmpline,), None))
883
884 if (len(ctx_keys) == 3 and
885 ctx_keys[0].startswith('router bgp') and
886 ctx_keys[1] == 'address-family l2vpn evpn' and
887 ctx_keys[2].startswith('vni')):
888
889 re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False
890
891 if re_route_target:
892 rt = re_route_target.group(1).strip()
893 route_target_import_line = line
894 route_target_export_line = "route-target export %s" % rt
895 route_target_both_line = "route-target both %s" % rt
896
897 found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line)
898 found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line)
899
900 '''
901 If the running configs has
902 route-target import 1:1
903 route-target export 1:1
904
905 and the config we are reloading against has
906 route-target both 1:1
907
908 then we can ignore deleting the import/export and ignore adding the 'both'
909 '''
910 if found_route_target_export_line and found_route_target_both_line:
911 lines_to_del_to_del.append((ctx_keys, route_target_import_line))
912 lines_to_del_to_del.append((ctx_keys, route_target_export_line))
913 lines_to_add_to_del.append((ctx_keys, route_target_both_line))
914
915 if not deleted:
916 found_add_line = line_exist(lines_to_add, ctx_keys, line)
917
918 if found_add_line:
919 lines_to_del_to_del.append((ctx_keys, line))
920 lines_to_add_to_del.append((ctx_keys, line))
921 else:
922 '''
923 We have commands that used to be displayed in the global part
924 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
925
926 # old way
927 router bgp 64900
928 neighbor ISL advertisement-interval 0
929
930 vs.
931
932 # new way
933 router bgp 64900
934 address-family ipv4 unicast
935 neighbor ISL advertisement-interval 0
936
937 Look to see if we are deleting it in one format just to add it back in the other
938 '''
939 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
940 tmp_ctx_keys = list(ctx_keys)[:-1]
941 tmp_ctx_keys = tuple(tmp_ctx_keys)
942
943 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
944
945 if found_add_line:
946 lines_to_del_to_del.append((ctx_keys, line))
947 lines_to_add_to_del.append((tmp_ctx_keys, line))
948
949 for (ctx_keys, line) in lines_to_del_to_del:
950 lines_to_del.remove((ctx_keys, line))
951
952 for (ctx_keys, line) in lines_to_add_to_del:
953 lines_to_add.remove((ctx_keys, line))
954
955 return (lines_to_add, lines_to_del)
956
957
958 def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
959 """
960 There are certain commands that cannot be removed. Remove
961 those commands from lines_to_del.
962 """
963 lines_to_del_to_del = []
964
965 for (ctx_keys, line) in lines_to_del:
966
967 if (ctx_keys[0].startswith('frr version') or
968 ctx_keys[0].startswith('frr defaults') or
969 ctx_keys[0].startswith('password') or
970 ctx_keys[0].startswith('line vty') or
971
972 # This is technically "no"able but if we did so frr-reload would
973 # stop working so do not let the user shoot themselves in the foot
974 # by removing this.
975 ctx_keys[0].startswith('service integrated-vtysh-config')):
976
977 log.info("(%s, %s) cannot be removed" % (pformat(ctx_keys), line))
978 lines_to_del_to_del.append((ctx_keys, line))
979
980 for (ctx_keys, line) in lines_to_del_to_del:
981 lines_to_del.remove((ctx_keys, line))
982
983 return (lines_to_add, lines_to_del)
984
985
986 def compare_context_objects(newconf, running):
987 """
988 Create a context diff for the two specified contexts
989 """
990
991 # Compare the two Config objects to find the lines that we need to add/del
992 lines_to_add = []
993 lines_to_del = []
994 delete_bgpd = False
995
996 # Find contexts that are in newconf but not in running
997 # Find contexts that are in running but not in newconf
998 for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
999
1000 if running_ctx_keys not in newconf.contexts:
1001
1002 # We check that the len is 1 here so that we only look at ('router bgp 10')
1003 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1004 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1005 # running but not in newconf.
1006 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
1007 delete_bgpd = True
1008 lines_to_del.append((running_ctx_keys, None))
1009
1010 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1011 elif running_ctx_keys[0].startswith('interface') or running_ctx_keys[0].startswith('vrf'):
1012 for line in running_ctx.lines:
1013 lines_to_del.append((running_ctx_keys, line))
1014
1015 # If this is an address-family under 'router bgp' and we are already deleting the
1016 # entire 'router bgp' context then ignore this sub-context
1017 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
1018 continue
1019
1020 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1021 elif ("router bgp" in running_ctx_keys[0] and
1022 len(running_ctx_keys) > 2 and
1023 running_ctx_keys[1].startswith('address-family l2vpn evpn') and
1024 running_ctx_keys[2].startswith('vni ')):
1025 lines_to_del.append((running_ctx_keys, None))
1026
1027 elif ("router bgp" in running_ctx_keys[0] and
1028 len(running_ctx_keys) > 1 and
1029 running_ctx_keys[1].startswith('address-family')):
1030 # There's no 'no address-family' support and so we have to
1031 # delete each line individually again
1032 for line in running_ctx.lines:
1033 lines_to_del.append((running_ctx_keys, line))
1034
1035 # Non-global context
1036 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
1037 lines_to_del.append((running_ctx_keys, None))
1038
1039 elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys):
1040 lines_to_del.append((running_ctx_keys, None))
1041
1042 # Global context
1043 else:
1044 for line in running_ctx.lines:
1045 lines_to_del.append((running_ctx_keys, line))
1046
1047 # Find the lines within each context to add
1048 # Find the lines within each context to del
1049 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
1050
1051 if newconf_ctx_keys in running.contexts:
1052 running_ctx = running.contexts[newconf_ctx_keys]
1053
1054 for line in newconf_ctx.lines:
1055 if line not in running_ctx.dlines:
1056 lines_to_add.append((newconf_ctx_keys, line))
1057
1058 for line in running_ctx.lines:
1059 if line not in newconf_ctx.dlines:
1060 lines_to_del.append((newconf_ctx_keys, line))
1061
1062 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
1063
1064 if newconf_ctx_keys not in running.contexts:
1065 lines_to_add.append((newconf_ctx_keys, None))
1066
1067 for line in newconf_ctx.lines:
1068 lines_to_add.append((newconf_ctx_keys, line))
1069
1070 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
1071 (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del)
1072
1073 return (lines_to_add, lines_to_del)
1074
1075
1076
1077 def vtysh_config_available():
1078 """
1079 Return False if no frr daemon is running or some other vtysh session is
1080 in 'configuration terminal' mode which will prevent us from making any
1081 configuration changes.
1082 """
1083
1084 try:
1085 cmd = ['/usr/bin/vtysh', '-c', 'conf t']
1086 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).strip()
1087
1088 if 'VTY configuration is locked by other VTY' in output.decode('utf-8'):
1089 print(output)
1090 log.error("'%s' returned\n%s\n" % (' '.join(cmd), output))
1091 return False
1092
1093 except subprocess.CalledProcessError as e:
1094 msg = "vtysh could not connect with any frr daemons"
1095 print(msg)
1096 log.error(msg)
1097 return False
1098
1099 return True
1100
1101
1102 if __name__ == '__main__':
1103 # Command line options
1104 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
1105 parser.add_argument('--input', help='Read running config from file instead of "show running"')
1106 group = parser.add_mutually_exclusive_group(required=True)
1107 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
1108 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
1109 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
1110 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
1111 parser.add_argument('filename', help='Location of new frr config file')
1112 parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
1113 args = parser.parse_args()
1114
1115 # Logging
1116 # For --test log to stdout
1117 # For --reload log to /var/log/frr/frr-reload.log
1118 if args.test or args.stdout:
1119 logging.basicConfig(level=logging.INFO,
1120 format='%(asctime)s %(levelname)5s: %(message)s')
1121
1122 # Color the errors and warnings in red
1123 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
1124 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
1125
1126 elif args.reload:
1127 if not os.path.isdir('/var/log/frr/'):
1128 os.makedirs('/var/log/frr/')
1129
1130 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
1131 level=logging.INFO,
1132 format='%(asctime)s %(levelname)5s: %(message)s')
1133
1134 # argparse should prevent this from happening but just to be safe...
1135 else:
1136 raise Exception('Must specify --reload or --test')
1137 log = logging.getLogger(__name__)
1138
1139 # Verify the new config file is valid
1140 if not os.path.isfile(args.filename):
1141 msg = "Filename %s does not exist" % args.filename
1142 print(msg)
1143 log.error(msg)
1144 sys.exit(1)
1145
1146 if not os.path.getsize(args.filename):
1147 msg = "Filename %s is an empty file" % args.filename
1148 print(msg)
1149 log.error(msg)
1150 sys.exit(1)
1151
1152 # Verify that 'service integrated-vtysh-config' is configured
1153 vtysh_filename = '/etc/frr/vtysh.conf'
1154 service_integrated_vtysh_config = True
1155
1156 if os.path.isfile(vtysh_filename):
1157 with open(vtysh_filename, 'r') as fh:
1158 for line in fh.readlines():
1159 line = line.strip()
1160
1161 if line == 'no service integrated-vtysh-config':
1162 service_integrated_vtysh_config = False
1163 break
1164
1165 if not service_integrated_vtysh_config:
1166 msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1167 print(msg)
1168 log.error(msg)
1169 sys.exit(1)
1170
1171 if args.debug:
1172 log.setLevel(logging.DEBUG)
1173
1174 log.info('Called via "%s"', str(args))
1175
1176 # Create a Config object from the config generated by newconf
1177 newconf = Config()
1178 newconf.load_from_file(args.filename)
1179 reload_ok = True
1180
1181 if args.test:
1182
1183 # Create a Config object from the running config
1184 running = Config()
1185
1186 if args.input:
1187 running.load_from_file(args.input)
1188 else:
1189 running.load_from_show_running()
1190
1191 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
1192 lines_to_configure = []
1193
1194 if lines_to_del:
1195 print("\nLines To Delete")
1196 print("===============")
1197
1198 for (ctx_keys, line) in lines_to_del:
1199
1200 if line == '!':
1201 continue
1202
1203 cmd = line_for_vtysh_file(ctx_keys, line, True)
1204 lines_to_configure.append(cmd)
1205 print(cmd)
1206
1207 if lines_to_add:
1208 print("\nLines To Add")
1209 print("============")
1210
1211 for (ctx_keys, line) in lines_to_add:
1212
1213 if line == '!':
1214 continue
1215
1216 cmd = line_for_vtysh_file(ctx_keys, line, False)
1217 lines_to_configure.append(cmd)
1218 print(cmd)
1219
1220 elif args.reload:
1221
1222 # We will not be able to do anything, go ahead and exit(1)
1223 if not vtysh_config_available():
1224 sys.exit(1)
1225
1226 log.debug('New Frr Config\n%s', newconf.get_lines())
1227
1228 # This looks a little odd but we have to do this twice...here is why
1229 # If the user had this running bgp config:
1230 #
1231 # router bgp 10
1232 # neighbor 1.1.1.1 remote-as 50
1233 # neighbor 1.1.1.1 route-map FOO out
1234 #
1235 # and this config in the newconf config file
1236 #
1237 # router bgp 10
1238 # neighbor 1.1.1.1 remote-as 999
1239 # neighbor 1.1.1.1 route-map FOO out
1240 #
1241 #
1242 # Then the script will do
1243 # - no neighbor 1.1.1.1 remote-as 50
1244 # - neighbor 1.1.1.1 remote-as 999
1245 #
1246 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1247 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1248 # configs again to put this line back.
1249
1250 # There are many keywords in FRR that can only appear one time under
1251 # a context, take "bgp router-id" for example. If the config that we are
1252 # reloading against has the following:
1253 #
1254 # router bgp 10
1255 # bgp router-id 1.1.1.1
1256 # bgp router-id 2.2.2.2
1257 #
1258 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1259 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1260 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1261 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1262 # second pass to include all of the "adds" from the first pass.
1263 lines_to_add_first_pass = []
1264
1265 for x in range(2):
1266 running = Config()
1267 running.load_from_show_running()
1268 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
1269
1270 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
1271
1272 if x == 0:
1273 lines_to_add_first_pass = lines_to_add
1274 else:
1275 lines_to_add.extend(lines_to_add_first_pass)
1276
1277 # Only do deletes on the first pass. The reason being if we
1278 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1279 # will automatically add:
1280 #
1281 # interface swp1
1282 # ipv6 nd ra-interval 10
1283 # no ipv6 nd suppress-ra
1284 # !
1285 #
1286 # but those lines aren't in the config we are reloading against so
1287 # on the 2nd pass they will show up in lines_to_del. This could
1288 # apply to other scenarios as well where configuring FOO adds BAR
1289 # to the config.
1290 if lines_to_del and x == 0:
1291 for (ctx_keys, line) in lines_to_del:
1292
1293 if line == '!':
1294 continue
1295
1296 # 'no' commands are tricky, we can't just put them in a file and
1297 # vtysh -f that file. See the next comment for an explanation
1298 # of their quirks
1299 cmd = line_to_vtysh_conft(ctx_keys, line, True)
1300 original_cmd = cmd
1301
1302 # Some commands in frr are picky about taking a "no" of the entire line.
1303 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1304 # only the beginning. If we hit one of these command an exception will be
1305 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1306 #
1307 # Example:
1308 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1309 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1310 # % Unknown command.
1311 # frr(config-if)# no ip ospf authentication message-digest
1312 # % Unknown command.
1313 # frr(config-if)# no ip ospf authentication
1314 # frr(config-if)#
1315
1316 while True:
1317 try:
1318 _ = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1319
1320 except subprocess.CalledProcessError:
1321
1322 # - Pull the last entry from cmd (this would be
1323 # 'no ip ospf authentication message-digest 1.1.1.1' in
1324 # our example above
1325 # - Split that last entry by whitespace and drop the last word
1326 log.info('Failed to execute %s', ' '.join(cmd))
1327 last_arg = cmd[-1].split(' ')
1328
1329 if len(last_arg) <= 2:
1330 log.error('"%s" we failed to remove this command', original_cmd)
1331 break
1332
1333 new_last_arg = last_arg[0:-1]
1334 cmd[-1] = ' '.join(new_last_arg)
1335 else:
1336 log.info('Executed "%s"', ' '.join(cmd))
1337 break
1338
1339 if lines_to_add:
1340 lines_to_configure = []
1341
1342 for (ctx_keys, line) in lines_to_add:
1343
1344 if line == '!':
1345 continue
1346
1347 cmd = line_for_vtysh_file(ctx_keys, line, False)
1348 lines_to_configure.append(cmd)
1349
1350 if lines_to_configure:
1351 random_string = ''.join(random.SystemRandom().choice(
1352 string.ascii_uppercase +
1353 string.digits) for _ in range(6))
1354
1355 filename = "/var/run/frr/reload-%s.txt" % random_string
1356 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
1357
1358 with open(filename, 'w') as fh:
1359 for line in lines_to_configure:
1360 fh.write(line + '\n')
1361
1362 try:
1363 subprocess.check_output(['/usr/bin/vtysh', '-f', filename], stderr=subprocess.STDOUT)
1364 except subprocess.CalledProcessError as e:
1365 log.warning("frr-reload.py failed due to\n%s" % e.output)
1366 reload_ok = False
1367 os.unlink(filename)
1368
1369 # Make these changes persistent
1370 if args.overwrite or args.filename != '/etc/frr/frr.conf':
1371 subprocess.call(['/usr/bin/vtysh', '-c', 'write'])
1372
1373 if not reload_ok:
1374 sys.exit(1)