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