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