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