]> git.proxmox.com Git - mirror_frr.git/blame - tools/frr-reload.py
Merge pull request #1858 from qlyoung/gitignore-clippy-sources
[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
9d877afc
DW
437 # The 'vni' keyword under 'router bgp X/address-family l2vpn evpn' creates
438 # a sub-context but the 'vni' keyword in other places (such as 'vrf BLUE')
439 # does not.
440 elif ("vni " in line and
441 len(ctx_keys) == 2 and
442 ctx_keys[0].startswith('router bgp') and
443 ctx_keys[1] == 'address-family l2vpn evpn'):
444
7918b335
DW
445 main_ctx_key = []
446
447 # Save old context first
448 self.save_contexts(ctx_keys, current_context_lines)
449 current_context_lines = []
450 main_ctx_key = copy.deepcopy(ctx_keys)
451 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
452
453 ctx_keys.append(line)
454
2fc76430
DS
455 elif "address-family " in line:
456 main_ctx_key = []
457
0b960b4d
DW
458 # Save old context first
459 self.save_contexts(ctx_keys, current_context_lines)
460 current_context_lines = []
461 main_ctx_key = copy.deepcopy(ctx_keys)
a782e613 462 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
2fc76430 463
0b960b4d
DW
464 if line == "address-family ipv6":
465 ctx_keys.append("address-family ipv6 unicast")
466 elif line == "address-family ipv4":
467 ctx_keys.append("address-family ipv4 unicast")
5014d96f
DW
468 elif line == "address-family evpn":
469 ctx_keys.append("address-family l2vpn evpn")
0b960b4d
DW
470 else:
471 ctx_keys.append(line)
2fc76430
DS
472
473 else:
474 # Continuing in an existing context, add non-commented lines to it
475 current_context_lines.append(line)
a782e613 476 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
2fc76430
DS
477
478 # Save the context of the last one
479 self.save_contexts(ctx_keys, current_context_lines)
480
4a2587c6 481
2fc76430
DS
482def line_to_vtysh_conft(ctx_keys, line, delete):
483 """
4a2587c6 484 Return the vtysh command for the specified context line
2fc76430
DS
485 """
486
487 cmd = []
488 cmd.append('vtysh')
489 cmd.append('-c')
490 cmd.append('conf t')
491
492 if line:
493 for ctx_key in ctx_keys:
494 cmd.append('-c')
495 cmd.append(ctx_key)
496
497 line = line.lstrip()
498
499 if delete:
500 cmd.append('-c')
501
502 if line.startswith('no '):
503 cmd.append('%s' % line[3:])
504 else:
505 cmd.append('no %s' % line)
506
507 else:
508 cmd.append('-c')
509 cmd.append(line)
510
511 # If line is None then we are typically deleting an entire
512 # context ('no router ospf' for example)
513 else:
514
515 if delete:
516
517 # Only put the 'no' on the last sub-context
518 for ctx_key in ctx_keys:
519 cmd.append('-c')
520
521 if ctx_key == ctx_keys[-1]:
522 cmd.append('no %s' % ctx_key)
523 else:
524 cmd.append('%s' % ctx_key)
525 else:
526 for ctx_key in ctx_keys:
527 cmd.append('-c')
528 cmd.append(ctx_key)
529
530 return cmd
531
4a2587c6
DW
532
533def line_for_vtysh_file(ctx_keys, line, delete):
534 """
e20dc2ba 535 Return the command as it would appear in frr.conf
4a2587c6
DW
536 """
537 cmd = []
538
539 if line:
540 for (i, ctx_key) in enumerate(ctx_keys):
541 cmd.append(' ' * i + ctx_key)
542
543 line = line.lstrip()
544 indent = len(ctx_keys) * ' '
545
546 if delete:
547 if line.startswith('no '):
548 cmd.append('%s%s' % (indent, line[3:]))
549 else:
550 cmd.append('%sno %s' % (indent, line))
551
552 else:
553 cmd.append(indent + line)
554
555 # If line is None then we are typically deleting an entire
556 # context ('no router ospf' for example)
557 else:
558 if delete:
559
560 # Only put the 'no' on the last sub-context
561 for ctx_key in ctx_keys:
562
563 if ctx_key == ctx_keys[-1]:
564 cmd.append('no %s' % ctx_key)
565 else:
566 cmd.append('%s' % ctx_key)
567 else:
568 for ctx_key in ctx_keys:
569 cmd.append(ctx_key)
570
8ad1fe6c
DW
571 cmd = '\n' + '\n'.join(cmd)
572
573 # There are some commands that are on by default so their "no" form will be
574 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
575 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
576 # not by doing a "no no bgp default ipv4-unicast"
577 cmd = cmd.replace('no no ', '')
578
579 return cmd
4a2587c6
DW
580
581
2fc76430
DS
582def get_normalized_ipv6_line(line):
583 """
d8e4c438 584 Return a normalized IPv6 line as produced by frr,
2fc76430 585 with all letters in lower case and trailing and leading
bb972e44
DD
586 zeros removed, and only the network portion present if
587 the IPv6 word is a network
2fc76430
DS
588 """
589 norm_line = ""
590 words = line.split(' ')
591 for word in words:
592 if ":" in word:
bb972e44
DD
593 norm_word = None
594 if "/" in word:
595 try:
596 v6word = IPNetwork(word)
597 norm_word = '%s/%s' % (v6word.network, v6word.prefixlen)
598 except ValueError:
599 pass
600 if not norm_word:
601 try:
602 norm_word = '%s' % IPv6Address(word)
0845b872 603 except ValueError:
bb972e44 604 norm_word = word
2fc76430
DS
605 else:
606 norm_word = word
607 norm_line = norm_line + " " + norm_word
608
609 return norm_line.strip()
610
4a2587c6 611
c755f5c4 612def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
9fe88bc7 613 for (ctx_keys, line) in lines:
c755f5c4
DW
614 if ctx_keys == target_ctx_keys:
615 if exact_match:
616 if line == target_line:
617 return True
618 else:
619 if line.startswith(target_line):
620 return True
9fe88bc7
DW
621 return False
622
623
9b166171 624def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
9fe88bc7
DW
625
626 # Quite possibly the most confusing (while accurate) variable names in history
627 lines_to_add_to_del = []
628 lines_to_del_to_del = []
629
630 for (ctx_keys, line) in lines_to_del:
9b166171
DW
631 deleted = False
632
028bcc88
DW
633 if ctx_keys[0].startswith('router bgp') and line:
634
635 if line.startswith('neighbor '):
636 '''
637 BGP changed how it displays swpX peers that are part of peer-group. Older
638 versions of frr would display these on separate lines:
639 neighbor swp1 interface
640 neighbor swp1 peer-group FOO
641
642 but today we display via a single line
643 neighbor swp1 interface peer-group FOO
644
645 This change confuses frr-reload.py so check to see if we are deleting
646 neighbor swp1 interface peer-group FOO
647
648 and adding
649 neighbor swp1 interface
650 neighbor swp1 peer-group FOO
651
652 If so then chop the del line and the corresponding add lines
653 '''
654
655 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
656 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
657
658 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
659 swpx_interface = None
660 swpx_peergroup = None
661
662 if re_swpx_int_peergroup:
663 swpx = re_swpx_int_peergroup.group(1)
664 peergroup = re_swpx_int_peergroup.group(2)
665 swpx_interface = "neighbor %s interface" % swpx
666 elif re_swpx_int_v6only_peergroup:
667 swpx = re_swpx_int_v6only_peergroup.group(1)
668 peergroup = re_swpx_int_v6only_peergroup.group(2)
669 swpx_interface = "neighbor %s interface v6only" % swpx
670
671 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
672 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
673 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
674 tmp_ctx_keys = tuple(list(ctx_keys))
9b166171
DW
675
676 if not found_add_swpx_peergroup:
677 tmp_ctx_keys = list(ctx_keys)
028bcc88 678 tmp_ctx_keys.append('address-family ipv4 unicast')
9b166171
DW
679 tmp_ctx_keys = tuple(tmp_ctx_keys)
680 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
9fe88bc7 681
028bcc88
DW
682 if not found_add_swpx_peergroup:
683 tmp_ctx_keys = list(ctx_keys)
684 tmp_ctx_keys.append('address-family ipv6 unicast')
685 tmp_ctx_keys = tuple(tmp_ctx_keys)
686 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
687
688 if found_add_swpx_interface and found_add_swpx_peergroup:
689 deleted = True
690 lines_to_del_to_del.append((ctx_keys, line))
691 lines_to_add_to_del.append((ctx_keys, swpx_interface))
692 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
693
694 '''
4c76e592 695 We changed how we display the neighbor interface command. Older
028bcc88
DW
696 versions of frr would display the following:
697 neighbor swp1 interface
698 neighbor swp1 remote-as external
699 neighbor swp1 capability extended-nexthop
700
701 but today we display via a single line
702 neighbor swp1 interface remote-as external
703
704 and capability extended-nexthop is no longer needed because we
705 automatically enable it when the neighbor is of type interface.
706
707 This change confuses frr-reload.py so check to see if we are deleting
708 neighbor swp1 interface remote-as (external|internal|ASNUM)
709
710 and adding
711 neighbor swp1 interface
712 neighbor swp1 remote-as (external|internal|ASNUM)
713 neighbor swp1 capability extended-nexthop
714
715 If so then chop the del line and the corresponding add lines
716 '''
717 re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
718 re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
719
720 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
721 swpx_interface = None
722 swpx_remoteas = None
723
724 if re_swpx_int_remoteas:
725 swpx = re_swpx_int_remoteas.group(1)
726 remoteas = re_swpx_int_remoteas.group(2)
727 swpx_interface = "neighbor %s interface" % swpx
728 elif re_swpx_int_v6only_remoteas:
729 swpx = re_swpx_int_v6only_remoteas.group(1)
730 remoteas = re_swpx_int_v6only_remoteas.group(2)
731 swpx_interface = "neighbor %s interface v6only" % swpx
732
733 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
734 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
735 found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
736 tmp_ctx_keys = tuple(list(ctx_keys))
737
738 if found_add_swpx_interface and found_add_swpx_remoteas:
739 deleted = True
740 lines_to_del_to_del.append((ctx_keys, line))
741 lines_to_add_to_del.append((ctx_keys, swpx_interface))
742 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
743
744 '''
4c76e592
DW
745 We made the 'bgp bestpath as-path multipath-relax' command
746 automatically assume 'no-as-set' since the lack of this option caused
747 weird routing problems. When the running config is shown in
748 releases with this change, the no-as-set keyword is not shown as it
749 is the default. This causes frr-reload to unnecessarily unapply
750 this option only to apply it back again, causing unnecessary session
751 resets.
028bcc88
DW
752 '''
753 if 'multipath-relax' in line:
754 re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line)
755 old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set'
756 found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
757
758 if re_asrelax_new and found_asrelax_old:
9b166171 759 deleted = True
9fe88bc7 760 lines_to_del_to_del.append((ctx_keys, line))
028bcc88
DW
761 lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
762
763 '''
764 If we are modifying the BGP table-map we need to avoid a del/add and
765 instead modify the table-map in place via an add. This is needed to
766 avoid installing all routes in the RIB the second the 'no table-map'
767 is issued.
768 '''
769 if line.startswith('table-map'):
770 found_table_map = line_exist(lines_to_add, ctx_keys, 'table-map', False)
771
772 if found_table_map:
b3a39dc5 773 lines_to_del_to_del.append((ctx_keys, line))
c755f5c4 774
78e31f46 775 '''
028bcc88
DW
776 More old-to-new config handling. ip import-table no longer accepts
777 distance, but we honor the old syntax. But 'show running' shows only
778 the new syntax. This causes an unnecessary 'no import-table' followed
779 by the same old 'ip import-table' which causes perturbations in
780 announced routes leading to traffic blackholes. Fix this issue.
78e31f46
DD
781 '''
782 re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0])
783 if re_importtbl:
784 table_num = re_importtbl.group(1)
785 for ctx in lines_to_add:
786 if ctx[0][0].startswith('ip import-table %s distance' % table_num):
78e31f46
DD
787 lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
788 lines_to_add_to_del.append((ctx[0], None))
0bf7cc28
DD
789
790 '''
791 ip/ipv6 prefix-list can be specified without a seq number. However,
792 the running config always adds 'seq x', where x is a number incremented
793 by 5 for every element, to the prefix list. So, ignore such lines as
794 well. Sample prefix-list lines:
795 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
796 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
797 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
798 '''
799 re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
800 ctx_keys[0])
801 if re_ip_pfxlst:
802 tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) +
803 re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) +
804 re_ip_pfxlst.group(6))
805 for ctx in lines_to_add:
806 if ctx[0][0] == tmpline:
807 lines_to_del_to_del.append((ctx_keys, None))
808 lines_to_add_to_del.append(((tmpline,), None))
809
5014d96f
DW
810 if (len(ctx_keys) == 3 and
811 ctx_keys[0].startswith('router bgp') and
812 ctx_keys[1] == 'address-family l2vpn evpn' and
813 ctx_keys[2].startswith('vni')):
814
815 re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False
816
817 if re_route_target:
818 rt = re_route_target.group(1).strip()
819 route_target_import_line = line
820 route_target_export_line = "route-target export %s" % rt
821 route_target_both_line = "route-target both %s" % rt
822
823 found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line)
824 found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line)
825
826 '''
827 If the running configs has
828 route-target import 1:1
829 route-target export 1:1
830
831 and the config we are reloading against has
832 route-target both 1:1
833
834 then we can ignore deleting the import/export and ignore adding the 'both'
835 '''
836 if found_route_target_export_line and found_route_target_both_line:
837 lines_to_del_to_del.append((ctx_keys, route_target_import_line))
838 lines_to_del_to_del.append((ctx_keys, route_target_export_line))
839 lines_to_add_to_del.append((ctx_keys, route_target_both_line))
840
9b166171
DW
841 if not deleted:
842 found_add_line = line_exist(lines_to_add, ctx_keys, line)
843
844 if found_add_line:
845 lines_to_del_to_del.append((ctx_keys, line))
846 lines_to_add_to_del.append((ctx_keys, line))
847 else:
848 '''
849 We have commands that used to be displayed in the global part
850 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
851
852 # old way
853 router bgp 64900
854 neighbor ISL advertisement-interval 0
855
856 vs.
857
858 # new way
859 router bgp 64900
860 address-family ipv4 unicast
861 neighbor ISL advertisement-interval 0
862
863 Look to see if we are deleting it in one format just to add it back in the other
864 '''
865 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
866 tmp_ctx_keys = list(ctx_keys)[:-1]
867 tmp_ctx_keys = tuple(tmp_ctx_keys)
868
869 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
870
871 if found_add_line:
872 lines_to_del_to_del.append((ctx_keys, line))
873 lines_to_add_to_del.append((tmp_ctx_keys, line))
9fe88bc7
DW
874
875 for (ctx_keys, line) in lines_to_del_to_del:
876 lines_to_del.remove((ctx_keys, line))
877
878 for (ctx_keys, line) in lines_to_add_to_del:
879 lines_to_add.remove((ctx_keys, line))
880
881 return (lines_to_add, lines_to_del)
882
883
b05a1d3c
DW
884def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
885 """
886 There are certain commands that cannot be removed. Remove
887 those commands from lines_to_del.
888 """
889 lines_to_del_to_del = []
890
891 for (ctx_keys, line) in lines_to_del:
892
893 if (ctx_keys[0].startswith('frr version') or
894 ctx_keys[0].startswith('frr defaults') or
895 ctx_keys[0].startswith('password') or
896 ctx_keys[0].startswith('line vty') or
897
898 # This is technically "no"able but if we did so frr-reload would
899 # stop working so do not let the user shoot themselves in the foot
900 # by removing this.
901 ctx_keys[0].startswith('service integrated-vtysh-config')):
902
903 log.info("(%s, %s) cannot be removed" % (pformat(ctx_keys), line))
904 lines_to_del_to_del.append((ctx_keys, line))
905
906 for (ctx_keys, line) in lines_to_del_to_del:
907 lines_to_del.remove((ctx_keys, line))
908
909 return (lines_to_add, lines_to_del)
910
911
2fc76430
DS
912def compare_context_objects(newconf, running):
913 """
914 Create a context diff for the two specified contexts
915 """
916
917 # Compare the two Config objects to find the lines that we need to add/del
918 lines_to_add = []
919 lines_to_del = []
926ea62e 920 delete_bgpd = False
2fc76430
DS
921
922 # Find contexts that are in newconf but not in running
923 # Find contexts that are in running but not in newconf
924 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
925
926 if running_ctx_keys not in newconf.contexts:
927
ab5f8310
DW
928 # We check that the len is 1 here so that we only look at ('router bgp 10')
929 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
926ea62e 930 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
ab5f8310
DW
931 # running but not in newconf.
932 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
926ea62e
DW
933 delete_bgpd = True
934 lines_to_del.append((running_ctx_keys, None))
935
1a8c43f1 936 # We cannot do 'no interface' in FRR, and so deal with it
768bf950
DD
937 elif running_ctx_keys[0].startswith('interface'):
938 for line in running_ctx.lines:
939 lines_to_del.append((running_ctx_keys, line))
940
926ea62e
DW
941 # If this is an address-family under 'router bgp' and we are already deleting the
942 # entire 'router bgp' context then ignore this sub-context
943 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
76f69d1c 944 continue
514665b9 945
5014d96f
DW
946 # Delete an entire vni sub-context under "address-family l2vpn evpn"
947 elif ("router bgp" in running_ctx_keys[0] and
948 len(running_ctx_keys) > 2 and
949 running_ctx_keys[1].startswith('address-family l2vpn evpn') and
950 running_ctx_keys[2].startswith('vni ')):
951 lines_to_del.append((running_ctx_keys, None))
952
afa2e8e1
DW
953 elif ("router bgp" in running_ctx_keys[0] and
954 len(running_ctx_keys) > 1 and
955 running_ctx_keys[1].startswith('address-family')):
956 # There's no 'no address-family' support and so we have to
957 # delete each line individually again
958 for line in running_ctx.lines:
959 lines_to_del.append((running_ctx_keys, line))
960
2fc76430 961 # Non-global context
926ea62e 962 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
963 lines_to_del.append((running_ctx_keys, None))
964
7918b335
DW
965 elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys):
966 lines_to_del.append((running_ctx_keys, None))
967
2fc76430
DS
968 # Global context
969 else:
970 for line in running_ctx.lines:
971 lines_to_del.append((running_ctx_keys, line))
972
973 # Find the lines within each context to add
974 # Find the lines within each context to del
975 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
976
977 if newconf_ctx_keys in running.contexts:
978 running_ctx = running.contexts[newconf_ctx_keys]
979
980 for line in newconf_ctx.lines:
981 if line not in running_ctx.dlines:
982 lines_to_add.append((newconf_ctx_keys, line))
983
984 for line in running_ctx.lines:
985 if line not in newconf_ctx.dlines:
986 lines_to_del.append((newconf_ctx_keys, line))
987
988 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
989
990 if newconf_ctx_keys not in running.contexts:
991 lines_to_add.append((newconf_ctx_keys, None))
992
993 for line in newconf_ctx.lines:
994 lines_to_add.append((newconf_ctx_keys, line))
995
9b166171 996 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
b05a1d3c 997 (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del)
9fe88bc7 998
926ea62e 999 return (lines_to_add, lines_to_del)
2fc76430 1000
8ad1fe6c 1001
2f52ad96
DW
1002
1003def vtysh_config_available():
1004 """
1005 Return False if no frr daemon is running or some other vtysh session is
1006 in 'configuration terminal' mode which will prevent us from making any
1007 configuration changes.
1008 """
1009
1010 try:
1011 cmd = ['/usr/bin/vtysh', '-c', 'conf t']
1012 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).strip()
1013
1014 if 'VTY configuration is locked by other VTY' in output:
1015 print output
1016 log.error("'%s' returned\n%s\n" % (' '.join(cmd), output))
1017 return False
1018
1019 except subprocess.CalledProcessError as e:
1020 msg = "vtysh could not connect with any frr daemons"
1021 print msg
1022 log.error(msg)
1023 return False
1024
1025 return True
1026
1027
2fc76430
DS
1028if __name__ == '__main__':
1029 # Command line options
d8e4c438 1030 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
2fc76430
DS
1031 parser.add_argument('--input', help='Read running config from file instead of "show running"')
1032 group = parser.add_mutually_exclusive_group(required=True)
1033 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
1034 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
1035 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
cc146ecc 1036 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
d8e4c438 1037 parser.add_argument('filename', help='Location of new frr config file')
e20dc2ba 1038 parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
2fc76430
DS
1039 args = parser.parse_args()
1040
1041 # Logging
1042 # For --test log to stdout
d8e4c438 1043 # For --reload log to /var/log/frr/frr-reload.log
cc146ecc 1044 if args.test or args.stdout:
c50aceee 1045 logging.basicConfig(level=logging.INFO,
2fc76430 1046 format='%(asctime)s %(levelname)5s: %(message)s')
926ea62e
DW
1047
1048 # Color the errors and warnings in red
1049 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
1050 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
1051
2fc76430 1052 elif args.reload:
d8e4c438
DS
1053 if not os.path.isdir('/var/log/frr/'):
1054 os.makedirs('/var/log/frr/')
2fc76430 1055
d8e4c438 1056 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
c50aceee 1057 level=logging.INFO,
2fc76430
DS
1058 format='%(asctime)s %(levelname)5s: %(message)s')
1059
1060 # argparse should prevent this from happening but just to be safe...
1061 else:
1062 raise Exception('Must specify --reload or --test')
a782e613 1063 log = logging.getLogger(__name__)
2fc76430 1064
76f69d1c
DW
1065 # Verify the new config file is valid
1066 if not os.path.isfile(args.filename):
825be4c2
DW
1067 msg = "Filename %s does not exist" % args.filename
1068 print msg
1069 log.error(msg)
76f69d1c
DW
1070 sys.exit(1)
1071
1072 if not os.path.getsize(args.filename):
825be4c2
DW
1073 msg = "Filename %s is an empty file" % args.filename
1074 print msg
1075 log.error(msg)
76f69d1c
DW
1076 sys.exit(1)
1077
76f69d1c 1078 # Verify that 'service integrated-vtysh-config' is configured
d8e4c438 1079 vtysh_filename = '/etc/frr/vtysh.conf'
6ac9179c 1080 service_integrated_vtysh_config = True
76f69d1c 1081
f850d14d
DW
1082 if os.path.isfile(vtysh_filename):
1083 with open(vtysh_filename, 'r') as fh:
1084 for line in fh.readlines():
1085 line = line.strip()
76f69d1c 1086
6ac9179c
DD
1087 if line == 'no service integrated-vtysh-config':
1088 service_integrated_vtysh_config = False
f850d14d 1089 break
76f69d1c
DW
1090
1091 if not service_integrated_vtysh_config:
825be4c2
DW
1092 msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1093 print msg
1094 log.error(msg)
76f69d1c 1095 sys.exit(1)
2fc76430 1096
c50aceee 1097 if args.debug:
a782e613 1098 log.setLevel(logging.DEBUG)
c50aceee 1099
a782e613 1100 log.info('Called via "%s"', str(args))
c50aceee 1101
2fc76430
DS
1102 # Create a Config object from the config generated by newconf
1103 newconf = Config()
1104 newconf.load_from_file(args.filename)
825be4c2 1105 reload_ok = True
2fc76430
DS
1106
1107 if args.test:
1108
1109 # Create a Config object from the running config
1110 running = Config()
1111
1112 if args.input:
1113 running.load_from_file(args.input)
1114 else:
1115 running.load_from_show_running()
1116
926ea62e 1117 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
4a2587c6 1118 lines_to_configure = []
2fc76430
DS
1119
1120 if lines_to_del:
1121 print "\nLines To Delete"
1122 print "==============="
1123
1124 for (ctx_keys, line) in lines_to_del:
1125
1126 if line == '!':
1127 continue
1128
4a2587c6
DW
1129 cmd = line_for_vtysh_file(ctx_keys, line, True)
1130 lines_to_configure.append(cmd)
9fe88bc7 1131 print cmd
2fc76430
DS
1132
1133 if lines_to_add:
1134 print "\nLines To Add"
1135 print "============"
1136
1137 for (ctx_keys, line) in lines_to_add:
1138
1139 if line == '!':
1140 continue
1141
4a2587c6
DW
1142 cmd = line_for_vtysh_file(ctx_keys, line, False)
1143 lines_to_configure.append(cmd)
9fe88bc7 1144 print cmd
2fc76430 1145
2fc76430
DS
1146 elif args.reload:
1147
2f52ad96
DW
1148 # We will not be able to do anything, go ahead and exit(1)
1149 if not vtysh_config_available():
1150 sys.exit(1)
1151
d8e4c438 1152 log.debug('New Frr Config\n%s', newconf.get_lines())
2fc76430
DS
1153
1154 # This looks a little odd but we have to do this twice...here is why
1155 # If the user had this running bgp config:
4a2587c6 1156 #
2fc76430
DS
1157 # router bgp 10
1158 # neighbor 1.1.1.1 remote-as 50
1159 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 1160 #
2fc76430 1161 # and this config in the newconf config file
4a2587c6 1162 #
2fc76430
DS
1163 # router bgp 10
1164 # neighbor 1.1.1.1 remote-as 999
1165 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
1166 #
1167 #
2fc76430
DS
1168 # Then the script will do
1169 # - no neighbor 1.1.1.1 remote-as 50
1170 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 1171 #
2fc76430
DS
1172 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1173 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1174 # configs again to put this line back.
1175
1a8c43f1 1176 # There are many keywords in FRR that can only appear one time under
2ce26af1
DW
1177 # a context, take "bgp router-id" for example. If the config that we are
1178 # reloading against has the following:
1179 #
1180 # router bgp 10
1181 # bgp router-id 1.1.1.1
1182 # bgp router-id 2.2.2.2
1183 #
1184 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1185 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1186 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1187 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1188 # second pass to include all of the "adds" from the first pass.
1189 lines_to_add_first_pass = []
1190
2fc76430
DS
1191 for x in range(2):
1192 running = Config()
1193 running.load_from_show_running()
d8e4c438 1194 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 1195
926ea62e 1196 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2fc76430 1197
2ce26af1
DW
1198 if x == 0:
1199 lines_to_add_first_pass = lines_to_add
1200 else:
1201 lines_to_add.extend(lines_to_add_first_pass)
1202
53bddc22 1203 # Only do deletes on the first pass. The reason being if we
1a8c43f1 1204 # configure a bgp neighbor via "neighbor swp1 interface" FRR
53bddc22
DW
1205 # will automatically add:
1206 #
1207 # interface swp1
1208 # ipv6 nd ra-interval 10
1209 # no ipv6 nd suppress-ra
1210 # !
1211 #
1212 # but those lines aren't in the config we are reloading against so
1213 # on the 2nd pass they will show up in lines_to_del. This could
1214 # apply to other scenarios as well where configuring FOO adds BAR
1215 # to the config.
1216 if lines_to_del and x == 0:
2fc76430
DS
1217 for (ctx_keys, line) in lines_to_del:
1218
1219 if line == '!':
1220 continue
1221
4a2587c6
DW
1222 # 'no' commands are tricky, we can't just put them in a file and
1223 # vtysh -f that file. See the next comment for an explanation
1224 # of their quirks
2fc76430
DS
1225 cmd = line_to_vtysh_conft(ctx_keys, line, True)
1226 original_cmd = cmd
1227
d8e4c438 1228 # Some commands in frr are picky about taking a "no" of the entire line.
76f69d1c
DW
1229 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1230 # only the beginning. If we hit one of these command an exception will be
1231 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 1232 #
76f69d1c 1233 # Example:
d8e4c438
DS
1234 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1235 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 1236 # % Unknown command.
d8e4c438 1237 # frr(config-if)# no ip ospf authentication message-digest
76f69d1c 1238 # % Unknown command.
d8e4c438
DS
1239 # frr(config-if)# no ip ospf authentication
1240 # frr(config-if)#
2fc76430
DS
1241
1242 while True:
2fc76430 1243 try:
478f9ce2 1244 _ = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
2fc76430
DS
1245
1246 except subprocess.CalledProcessError:
1247
1248 # - Pull the last entry from cmd (this would be
1249 # 'no ip ospf authentication message-digest 1.1.1.1' in
1250 # our example above
1251 # - Split that last entry by whitespace and drop the last word
825be4c2 1252 log.info('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
1253 last_arg = cmd[-1].split(' ')
1254
1255 if len(last_arg) <= 2:
a782e613 1256 log.error('"%s" we failed to remove this command', original_cmd)
2fc76430
DS
1257 break
1258
1259 new_last_arg = last_arg[0:-1]
1260 cmd[-1] = ' '.join(new_last_arg)
1261 else:
a782e613 1262 log.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
1263 break
1264
2fc76430 1265 if lines_to_add:
4a2587c6
DW
1266 lines_to_configure = []
1267
2fc76430
DS
1268 for (ctx_keys, line) in lines_to_add:
1269
1270 if line == '!':
1271 continue
1272
4a2587c6
DW
1273 cmd = line_for_vtysh_file(ctx_keys, line, False)
1274 lines_to_configure.append(cmd)
1275
1276 if lines_to_configure:
1277 random_string = ''.join(random.SystemRandom().choice(
1278 string.ascii_uppercase +
1279 string.digits) for _ in range(6))
1280
d8e4c438 1281 filename = "/var/run/frr/reload-%s.txt" % random_string
a782e613 1282 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
4a2587c6
DW
1283
1284 with open(filename, 'w') as fh:
1285 for line in lines_to_configure:
1286 fh.write(line + '\n')
825be4c2 1287
596074af 1288 try:
478f9ce2 1289 subprocess.check_output(['/usr/bin/vtysh', '-f', filename], stderr=subprocess.STDOUT)
596074af
DW
1290 except subprocess.CalledProcessError as e:
1291 log.warning("frr-reload.py failed due to\n%s" % e.output)
1292 reload_ok = False
4a2587c6 1293 os.unlink(filename)
2fc76430 1294
4b78098d 1295 # Make these changes persistent
e20dc2ba 1296 if args.overwrite or args.filename != '/etc/frr/frr.conf':
926ea62e 1297 subprocess.call(['/usr/bin/vtysh', '-c', 'write'])
825be4c2
DW
1298
1299 if not reload_ok:
1300 sys.exit(1)