]> git.proxmox.com Git - mirror_frr.git/blame - tools/frr-reload.py
Merge pull request #5954 from ton31337/feature/rfc7607
[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 -",
3fa139a6 158 shell=True)
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
6024e562
DS
937 # Deleting static routes under a vrf can lead to time-outs if each is sent
938 # as separate vtysh -c commands. Change them from being in lines_to_del and
939 # put the "no" form in lines_to_add
940 if ctx_keys[0].startswith('vrf ') and line:
941 if (line.startswith('ip route') or
942 line.startswith('ipv6 route')):
943 add_cmd = ('no ' + line)
944 lines_to_add.append((ctx_keys, add_cmd))
945 lines_to_del_to_del.append((ctx_keys, line))
946
9b166171
DW
947 if not deleted:
948 found_add_line = line_exist(lines_to_add, ctx_keys, line)
949
950 if found_add_line:
951 lines_to_del_to_del.append((ctx_keys, line))
952 lines_to_add_to_del.append((ctx_keys, line))
953 else:
954 '''
955 We have commands that used to be displayed in the global part
956 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
957
958 # old way
959 router bgp 64900
960 neighbor ISL advertisement-interval 0
961
962 vs.
963
964 # new way
965 router bgp 64900
966 address-family ipv4 unicast
967 neighbor ISL advertisement-interval 0
968
969 Look to see if we are deleting it in one format just to add it back in the other
970 '''
971 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
972 tmp_ctx_keys = list(ctx_keys)[:-1]
973 tmp_ctx_keys = tuple(tmp_ctx_keys)
974
975 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
976
977 if found_add_line:
978 lines_to_del_to_del.append((ctx_keys, line))
979 lines_to_add_to_del.append((tmp_ctx_keys, line))
9fe88bc7
DW
980
981 for (ctx_keys, line) in lines_to_del_to_del:
982 lines_to_del.remove((ctx_keys, line))
983
984 for (ctx_keys, line) in lines_to_add_to_del:
985 lines_to_add.remove((ctx_keys, line))
986
987 return (lines_to_add, lines_to_del)
988
989
b05a1d3c
DW
990def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
991 """
992 There are certain commands that cannot be removed. Remove
993 those commands from lines_to_del.
994 """
995 lines_to_del_to_del = []
996
997 for (ctx_keys, line) in lines_to_del:
998
999 if (ctx_keys[0].startswith('frr version') or
1000 ctx_keys[0].startswith('frr defaults') or
1001 ctx_keys[0].startswith('password') or
1002 ctx_keys[0].startswith('line vty') or
1003
1004 # This is technically "no"able but if we did so frr-reload would
1005 # stop working so do not let the user shoot themselves in the foot
1006 # by removing this.
1007 ctx_keys[0].startswith('service integrated-vtysh-config')):
1008
1009 log.info("(%s, %s) cannot be removed" % (pformat(ctx_keys), line))
1010 lines_to_del_to_del.append((ctx_keys, line))
1011
1012 for (ctx_keys, line) in lines_to_del_to_del:
1013 lines_to_del.remove((ctx_keys, line))
1014
1015 return (lines_to_add, lines_to_del)
1016
1017
2fc76430
DS
1018def compare_context_objects(newconf, running):
1019 """
1020 Create a context diff for the two specified contexts
1021 """
1022
1023 # Compare the two Config objects to find the lines that we need to add/del
1024 lines_to_add = []
1025 lines_to_del = []
926ea62e 1026 delete_bgpd = False
2fc76430
DS
1027
1028 # Find contexts that are in newconf but not in running
1029 # Find contexts that are in running but not in newconf
1c64265f 1030 for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
2fc76430
DS
1031
1032 if running_ctx_keys not in newconf.contexts:
1033
ab5f8310
DW
1034 # We check that the len is 1 here so that we only look at ('router bgp 10')
1035 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
926ea62e 1036 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
ab5f8310
DW
1037 # running but not in newconf.
1038 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
926ea62e
DW
1039 delete_bgpd = True
1040 lines_to_del.append((running_ctx_keys, None))
1041
2a2b64e4
DS
1042 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1043 elif running_ctx_keys[0].startswith('interface') or running_ctx_keys[0].startswith('vrf'):
768bf950
DD
1044 for line in running_ctx.lines:
1045 lines_to_del.append((running_ctx_keys, line))
1046
926ea62e
DW
1047 # If this is an address-family under 'router bgp' and we are already deleting the
1048 # entire 'router bgp' context then ignore this sub-context
1049 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
76f69d1c 1050 continue
514665b9 1051
5014d96f
DW
1052 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1053 elif ("router bgp" in running_ctx_keys[0] and
1054 len(running_ctx_keys) > 2 and
1055 running_ctx_keys[1].startswith('address-family l2vpn evpn') and
1056 running_ctx_keys[2].startswith('vni ')):
1057 lines_to_del.append((running_ctx_keys, None))
1058
afa2e8e1
DW
1059 elif ("router bgp" in running_ctx_keys[0] and
1060 len(running_ctx_keys) > 1 and
1061 running_ctx_keys[1].startswith('address-family')):
1062 # There's no 'no address-family' support and so we have to
1063 # delete each line individually again
1064 for line in running_ctx.lines:
1065 lines_to_del.append((running_ctx_keys, line))
1066
6024e562
DS
1067 # Some commands can happen at higher counts that make
1068 # doing vtysh -c inefficient (and can time out.) For
1069 # these commands, instead of adding them to lines_to_del,
1070 # add the "no " version to lines_to_add.
1071 elif (running_ctx_keys[0].startswith('ip route') or
1072 running_ctx_keys[0].startswith('ipv6 route') or
1073 running_ctx_keys[0].startswith('access-list') or
1074 running_ctx_keys[0].startswith('ipv6 access-list') or
1075 running_ctx_keys[0].startswith('ip prefix-list') or
1076 running_ctx_keys[0].startswith('ipv6 prefix-list')):
1077 add_cmd = ('no ' + running_ctx_keys[0],)
1078 lines_to_add.append((add_cmd, None))
1079
2fc76430 1080 # Non-global context
926ea62e 1081 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
1082 lines_to_del.append((running_ctx_keys, None))
1083
7918b335
DW
1084 elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys):
1085 lines_to_del.append((running_ctx_keys, None))
1086
2fc76430
DS
1087 # Global context
1088 else:
1089 for line in running_ctx.lines:
1090 lines_to_del.append((running_ctx_keys, line))
1091
1092 # Find the lines within each context to add
1093 # Find the lines within each context to del
1c64265f 1094 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
2fc76430
DS
1095
1096 if newconf_ctx_keys in running.contexts:
1097 running_ctx = running.contexts[newconf_ctx_keys]
1098
1099 for line in newconf_ctx.lines:
1100 if line not in running_ctx.dlines:
1101 lines_to_add.append((newconf_ctx_keys, line))
1102
1103 for line in running_ctx.lines:
1104 if line not in newconf_ctx.dlines:
1105 lines_to_del.append((newconf_ctx_keys, line))
1106
1c64265f 1107 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
2fc76430
DS
1108
1109 if newconf_ctx_keys not in running.contexts:
1110 lines_to_add.append((newconf_ctx_keys, None))
1111
1112 for line in newconf_ctx.lines:
1113 lines_to_add.append((newconf_ctx_keys, line))
1114
9b166171 1115 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
b05a1d3c 1116 (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del)
9fe88bc7 1117
926ea62e 1118 return (lines_to_add, lines_to_del)
2fc76430 1119
8ad1fe6c 1120
2f52ad96 1121
1a11d9cd 1122def vtysh_config_available(bindir, confdir):
2f52ad96
DW
1123 """
1124 Return False if no frr daemon is running or some other vtysh session is
1125 in 'configuration terminal' mode which will prevent us from making any
1126 configuration changes.
1127 """
1128
1129 try:
1a11d9cd 1130 cmd = [str(bindir + '/vtysh'), '--config_dir', confdir, '-c', 'conf t']
3fa139a6 1131 output = subprocess.check_output(cmd).strip()
2f52ad96 1132
1c64265f 1133 if 'VTY configuration is locked by other VTY' in output.decode('utf-8'):
1134 print(output)
2f52ad96
DW
1135 log.error("'%s' returned\n%s\n" % (' '.join(cmd), output))
1136 return False
1137
1138 except subprocess.CalledProcessError as e:
1139 msg = "vtysh could not connect with any frr daemons"
1c64265f 1140 print(msg)
2f52ad96
DW
1141 log.error(msg)
1142 return False
1143
1144 return True
1145
1146
2fc76430
DS
1147if __name__ == '__main__':
1148 # Command line options
d8e4c438 1149 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
2fc76430
DS
1150 parser.add_argument('--input', help='Read running config from file instead of "show running"')
1151 group = parser.add_mutually_exclusive_group(required=True)
1152 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
1153 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
1154 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
cc146ecc 1155 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
d8e4c438 1156 parser.add_argument('filename', help='Location of new frr config file')
e20dc2ba 1157 parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
1a11d9cd
EDP
1158 parser.add_argument('--bindir', help='path to the vtysh executable', default='/usr/bin')
1159 parser.add_argument('--confdir', help='path to the daemon config files', default='/etc/frr')
1160 parser.add_argument('--rundir', help='path for the temp config file', default='/var/run/frr')
d9730542
EDP
1161 parser.add_argument('--daemon', help='daemon for which want to replace the config', default='')
1162
2fc76430
DS
1163 args = parser.parse_args()
1164
1165 # Logging
1166 # For --test log to stdout
d8e4c438 1167 # For --reload log to /var/log/frr/frr-reload.log
cc146ecc 1168 if args.test or args.stdout:
c50aceee 1169 logging.basicConfig(level=logging.INFO,
2fc76430 1170 format='%(asctime)s %(levelname)5s: %(message)s')
926ea62e
DW
1171
1172 # Color the errors and warnings in red
1173 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
1174 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
1175
2fc76430 1176 elif args.reload:
d8e4c438
DS
1177 if not os.path.isdir('/var/log/frr/'):
1178 os.makedirs('/var/log/frr/')
2fc76430 1179
d8e4c438 1180 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
c50aceee 1181 level=logging.INFO,
2fc76430
DS
1182 format='%(asctime)s %(levelname)5s: %(message)s')
1183
1184 # argparse should prevent this from happening but just to be safe...
1185 else:
1186 raise Exception('Must specify --reload or --test')
a782e613 1187 log = logging.getLogger(__name__)
2fc76430 1188
76f69d1c
DW
1189 # Verify the new config file is valid
1190 if not os.path.isfile(args.filename):
825be4c2 1191 msg = "Filename %s does not exist" % args.filename
1c64265f 1192 print(msg)
825be4c2 1193 log.error(msg)
76f69d1c
DW
1194 sys.exit(1)
1195
1196 if not os.path.getsize(args.filename):
825be4c2 1197 msg = "Filename %s is an empty file" % args.filename
1c64265f 1198 print(msg)
825be4c2 1199 log.error(msg)
76f69d1c
DW
1200 sys.exit(1)
1201
1a11d9cd
EDP
1202 # Verify that confdir is correct
1203 if not os.path.isdir(args.confdir):
1204 msg = "Confdir %s is not a valid path" % args.confdir
1205 print(msg)
1206 log.error(msg)
1207 sys.exit(1)
1208
1209 # Verify that bindir is correct
1210 if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + '/vtysh'):
1211 msg = "Bindir %s is not a valid path to vtysh" % args.bindir
1212 print(msg)
1213 log.error(msg)
1214 sys.exit(1)
1215
d9730542
EDP
1216 # verify that the daemon, if specified, is valid
1217 if args.daemon and args.daemon not in ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']:
1218 msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon
1219 print(msg)
1220 log.error(msg)
1221 sys.exit(1)
1222
76f69d1c 1223 # Verify that 'service integrated-vtysh-config' is configured
1a11d9cd 1224 vtysh_filename = args.confdir + '/vtysh.conf'
6ac9179c 1225 service_integrated_vtysh_config = True
76f69d1c 1226
f850d14d
DW
1227 if os.path.isfile(vtysh_filename):
1228 with open(vtysh_filename, 'r') as fh:
1229 for line in fh.readlines():
1230 line = line.strip()
76f69d1c 1231
6ac9179c
DD
1232 if line == 'no service integrated-vtysh-config':
1233 service_integrated_vtysh_config = False
f850d14d 1234 break
76f69d1c 1235
d9730542 1236 if not service_integrated_vtysh_config and not args.daemon:
825be4c2 1237 msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1c64265f 1238 print(msg)
825be4c2 1239 log.error(msg)
76f69d1c 1240 sys.exit(1)
2fc76430 1241
c50aceee 1242 if args.debug:
a782e613 1243 log.setLevel(logging.DEBUG)
c50aceee 1244
a782e613 1245 log.info('Called via "%s"', str(args))
c50aceee 1246
2fc76430
DS
1247 # Create a Config object from the config generated by newconf
1248 newconf = Config()
1a11d9cd 1249 newconf.load_from_file(args.filename, args.bindir, args.confdir)
825be4c2 1250 reload_ok = True
2fc76430
DS
1251
1252 if args.test:
1253
1254 # Create a Config object from the running config
1255 running = Config()
1256
1257 if args.input:
1a11d9cd 1258 running.load_from_file(args.input, args.bindir, args.confdir)
2fc76430 1259 else:
d9730542 1260 running.load_from_show_running(args.bindir, args.confdir, args.daemon)
2fc76430 1261
926ea62e 1262 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
4a2587c6 1263 lines_to_configure = []
2fc76430
DS
1264
1265 if lines_to_del:
1c64265f 1266 print("\nLines To Delete")
1267 print("===============")
2fc76430
DS
1268
1269 for (ctx_keys, line) in lines_to_del:
1270
1271 if line == '!':
1272 continue
1273
4a2587c6
DW
1274 cmd = line_for_vtysh_file(ctx_keys, line, True)
1275 lines_to_configure.append(cmd)
1c64265f 1276 print(cmd)
2fc76430
DS
1277
1278 if lines_to_add:
1c64265f 1279 print("\nLines To Add")
1280 print("============")
2fc76430
DS
1281
1282 for (ctx_keys, line) in lines_to_add:
1283
1284 if line == '!':
1285 continue
1286
4a2587c6
DW
1287 cmd = line_for_vtysh_file(ctx_keys, line, False)
1288 lines_to_configure.append(cmd)
1c64265f 1289 print(cmd)
2fc76430 1290
2fc76430
DS
1291 elif args.reload:
1292
2f52ad96 1293 # We will not be able to do anything, go ahead and exit(1)
1a11d9cd 1294 if not vtysh_config_available(args.bindir, args.confdir):
2f52ad96
DW
1295 sys.exit(1)
1296
d8e4c438 1297 log.debug('New Frr Config\n%s', newconf.get_lines())
2fc76430
DS
1298
1299 # This looks a little odd but we have to do this twice...here is why
1300 # If the user had this running bgp config:
4a2587c6 1301 #
2fc76430
DS
1302 # router bgp 10
1303 # neighbor 1.1.1.1 remote-as 50
1304 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 1305 #
2fc76430 1306 # and this config in the newconf config file
4a2587c6 1307 #
2fc76430
DS
1308 # router bgp 10
1309 # neighbor 1.1.1.1 remote-as 999
1310 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
1311 #
1312 #
2fc76430
DS
1313 # Then the script will do
1314 # - no neighbor 1.1.1.1 remote-as 50
1315 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 1316 #
2fc76430
DS
1317 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1318 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1319 # configs again to put this line back.
1320
1a8c43f1 1321 # There are many keywords in FRR that can only appear one time under
2ce26af1
DW
1322 # a context, take "bgp router-id" for example. If the config that we are
1323 # reloading against has the following:
1324 #
1325 # router bgp 10
1326 # bgp router-id 1.1.1.1
1327 # bgp router-id 2.2.2.2
1328 #
1329 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1330 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1331 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1332 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1333 # second pass to include all of the "adds" from the first pass.
1334 lines_to_add_first_pass = []
1335
2fc76430
DS
1336 for x in range(2):
1337 running = Config()
d9730542 1338 running.load_from_show_running(args.bindir, args.confdir, args.daemon)
d8e4c438 1339 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 1340
926ea62e 1341 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2fc76430 1342
2ce26af1
DW
1343 if x == 0:
1344 lines_to_add_first_pass = lines_to_add
1345 else:
1346 lines_to_add.extend(lines_to_add_first_pass)
1347
53bddc22 1348 # Only do deletes on the first pass. The reason being if we
1a8c43f1 1349 # configure a bgp neighbor via "neighbor swp1 interface" FRR
53bddc22
DW
1350 # will automatically add:
1351 #
1352 # interface swp1
1353 # ipv6 nd ra-interval 10
1354 # no ipv6 nd suppress-ra
1355 # !
1356 #
1357 # but those lines aren't in the config we are reloading against so
1358 # on the 2nd pass they will show up in lines_to_del. This could
1359 # apply to other scenarios as well where configuring FOO adds BAR
1360 # to the config.
1361 if lines_to_del and x == 0:
2fc76430
DS
1362 for (ctx_keys, line) in lines_to_del:
1363
1364 if line == '!':
1365 continue
1366
4a2587c6
DW
1367 # 'no' commands are tricky, we can't just put them in a file and
1368 # vtysh -f that file. See the next comment for an explanation
1369 # of their quirks
1a11d9cd 1370 cmd = line_to_vtysh_conft(ctx_keys, line, True, args.bindir, args.confdir)
2fc76430
DS
1371 original_cmd = cmd
1372
d8e4c438 1373 # Some commands in frr are picky about taking a "no" of the entire line.
76f69d1c
DW
1374 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1375 # only the beginning. If we hit one of these command an exception will be
1376 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 1377 #
76f69d1c 1378 # Example:
d8e4c438
DS
1379 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1380 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 1381 # % Unknown command.
d8e4c438 1382 # frr(config-if)# no ip ospf authentication message-digest
76f69d1c 1383 # % Unknown command.
d8e4c438
DS
1384 # frr(config-if)# no ip ospf authentication
1385 # frr(config-if)#
2fc76430
DS
1386
1387 while True:
2fc76430 1388 try:
3fa139a6 1389 _ = subprocess.check_output(cmd)
2fc76430
DS
1390
1391 except subprocess.CalledProcessError:
1392
1393 # - Pull the last entry from cmd (this would be
1394 # 'no ip ospf authentication message-digest 1.1.1.1' in
1395 # our example above
1396 # - Split that last entry by whitespace and drop the last word
825be4c2 1397 log.info('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
1398 last_arg = cmd[-1].split(' ')
1399
1400 if len(last_arg) <= 2:
a782e613 1401 log.error('"%s" we failed to remove this command', original_cmd)
2fc76430
DS
1402 break
1403
1404 new_last_arg = last_arg[0:-1]
1405 cmd[-1] = ' '.join(new_last_arg)
1406 else:
a782e613 1407 log.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
1408 break
1409
2fc76430 1410 if lines_to_add:
4a2587c6
DW
1411 lines_to_configure = []
1412
2fc76430
DS
1413 for (ctx_keys, line) in lines_to_add:
1414
1415 if line == '!':
1416 continue
1417
6024e562
DS
1418 # Don't run "no" commands twice since they can error
1419 # out the second time due to first deletion
1420 if x == 1 and ctx_keys[0].startswith('no '):
1421 continue
1422
4a2587c6
DW
1423 cmd = line_for_vtysh_file(ctx_keys, line, False)
1424 lines_to_configure.append(cmd)
1425
1426 if lines_to_configure:
1427 random_string = ''.join(random.SystemRandom().choice(
1428 string.ascii_uppercase +
1429 string.digits) for _ in range(6))
1430
1a11d9cd 1431 filename = args.rundir + "/reload-%s.txt" % random_string
a782e613 1432 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
4a2587c6
DW
1433
1434 with open(filename, 'w') as fh:
1435 for line in lines_to_configure:
1436 fh.write(line + '\n')
825be4c2 1437
596074af 1438 try:
3fa139a6 1439 subprocess.check_output([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-f', filename])
596074af
DW
1440 except subprocess.CalledProcessError as e:
1441 log.warning("frr-reload.py failed due to\n%s" % e.output)
1442 reload_ok = False
4a2587c6 1443 os.unlink(filename)
2fc76430 1444
4b78098d 1445 # Make these changes persistent
d9730542
EDP
1446 target = str(args.confdir + '/frr.conf')
1447 if args.overwrite or (not args.daemon and args.filename != target):
1a11d9cd 1448 subprocess.call([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-c', 'write'])
825be4c2
DW
1449
1450 if not reload_ok:
1451 sys.exit(1)