]> git.proxmox.com Git - mirror_frr.git/blob - tools/frr-reload.py
doc: indicate non-support for dynamic pbr maps
[mirror_frr.git] / tools / frr-reload.py
1 #!/usr/bin/python
2 # Frr Reloader
3 # Copyright (C) 2014 Cumulus Networks, Inc.
4 #
5 # This file is part of Frr.
6 #
7 # Frr is free software; you can redistribute it and/or modify it
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 #
12 # Frr is distributed in the hope that it will be useful, but
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
18 # along with Frr; see the file COPYING. If not, write to the Free
19 # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
20 # 02111-1307, USA.
21 #
22 """
23 This program
24 - reads a frr configuration text file
25 - reads frr's current running configuration via "vtysh -c 'show running'"
26 - compares the two configs and determines what commands to execute to
27 synchronize frr's running configuration with the configuation in the
28 text file
29 """
30
31 from __future__ import print_function, unicode_literals
32 import argparse
33 import copy
34 import logging
35 import os
36 import random
37 import re
38 import string
39 import subprocess
40 import sys
41 from collections import OrderedDict
42 try:
43 from ipaddress import IPv6Address, ip_network
44 except ImportError:
45 from ipaddr import IPv6Address, IPNetwork
46 from pprint import pformat
47
48 try:
49 dict.iteritems
50 except AttributeError:
51 # Python 3
52 def iteritems(d):
53 return iter(d.items())
54 else:
55 # Python 2
56 def iteritems(d):
57 return d.iteritems()
58
59 log = logging.getLogger(__name__)
60
61
62 class VtyshMarkException(Exception):
63 pass
64
65
66 class Context(object):
67
68 """
69 A Context object represents a section of frr configuration such as:
70 !
71 interface swp3
72 description swp3 -> r8's swp1
73 ipv6 nd suppress-ra
74 link-detect
75 !
76
77 or a single line context object such as this:
78
79 ip 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
105 class Config(object):
106
107 """
108 A frr configuration is stored in a Config object. A Config object
109 contains a dictionary of Context objects where the Context keys
110 ('router ospf' for example) are our dictionary key.
111 """
112
113 def __init__(self):
114 self.lines = []
115 self.contexts = OrderedDict()
116
117 def load_from_file(self, filename, bindir, confdir):
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 """
123 log.info('Loading Config object from file %s', filename)
124
125 try:
126 file_output = subprocess.check_output([str(bindir + '/vtysh'), '-m', '-f', filename, '--config_dir', confdir],
127 stderr=subprocess.STDOUT)
128 except subprocess.CalledProcessError as e:
129 ve = VtyshMarkException(e)
130 ve.output = e.output
131 raise ve
132
133 for line in file_output.decode('utf-8').split('\n'):
134 line = line.strip()
135
136 # Compress duplicate whitespaces
137 line = ' '.join(line.split())
138
139 if ":" in line:
140 qv6_line = get_normalized_ipv6_line(line)
141 self.lines.append(qv6_line)
142 else:
143 self.lines.append(line)
144
145 self.load_contexts()
146
147 def load_from_show_running(self, bindir, confdir, daemon):
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 """
153 log.info('Loading Config object from vtysh show running')
154
155 try:
156 config_text = subprocess.check_output(
157 bindir + "/vtysh --config_dir " + confdir + " -c 'show run " + daemon + "' | /usr/bin/tail -n +4 | " + bindir + "/vtysh --config_dir " + confdir + " -m -f -",
158 shell=True)
159 except subprocess.CalledProcessError as e:
160 ve = VtyshMarkException(e)
161 ve.output = e.output
162 raise ve
163
164 for line in config_text.decode('utf-8').split('\n'):
165 line = line.strip()
166
167 if (line == 'Building configuration...' or
168 line == 'Current configuration:' or
169 not line):
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
188 for (_, ctx) in sorted(iteritems(self.contexts)):
189 print(str(ctx) + '\n')
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
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 '''
207 re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0])
208 if re_key_rt:
209 addr = re_key_rt.group(2)
210 if '/' in addr:
211 try:
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))
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:
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)
242 except ValueError:
243 newaddr = addr
244 else:
245 newaddr = addr
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:
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))
292 newlines.append(line)
293 except ValueError:
294 # Really this should be an error. Whats a network
295 # without an IP Address following it ?
296 newlines.append(line)
297 else:
298 newlines.append(line)
299 lines = newlines
300
301 '''
302 More fixups in user specification and what running config shows.
303 "null0" in routes must be replaced by Null0.
304 '''
305 if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and
306 'null0' in key[0]):
307 key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0])
308
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 !
334 interface swp52
335 ipv6 nd suppress-ra
336 link-detect
337 !
338 end
339 router 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 !
349 end
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 !
356 end
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 !
367 end
368 router ospf
369 ospf router-id 10.0.0.1
370 log-adjacency-changes detail
371 timers throttle spf 0 50 5000
372 !
373 end
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.
388 ctx_keys = []
389 main_ctx_key = []
390 new_ctx = True
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
394 oneline_ctx_keywords = ("access-list ",
395 "agentx",
396 "allow-external-route-update",
397 "bgp ",
398 "debug ",
399 "domainname ",
400 "dump ",
401 "enable ",
402 "frr ",
403 "hostname ",
404 "ip ",
405 "ipv6 ",
406 "log ",
407 "mpls lsp",
408 "mpls label",
409 "no ",
410 "password ",
411 "ptm-enable",
412 "router-id ",
413 "service ",
414 "table ",
415 "username ",
416 "zebra ",
417 "vrrp autoconfigure")
418
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
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 ")):
434 self.save_contexts(ctx_keys, current_context_lines)
435
436 # Start a new context
437 main_ctx_key = []
438 ctx_keys = [line, ]
439 current_context_lines = []
440
441 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
442 self.save_contexts(ctx_keys, current_context_lines)
443 new_ctx = True
444
445 elif line == "end":
446 self.save_contexts(ctx_keys, current_context_lines)
447 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
448
449 # Start a new context
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
461 new_ctx = True
462 main_ctx_key = []
463 ctx_keys = []
464 current_context_lines = []
465
466 elif line in ["exit-address-family", "exit", "exit-vnc"]:
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 = []
474 log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
475
476 elif line in ["exit-vni", "exit-ldp-if"]:
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
485 elif new_ctx is True:
486 if not main_ctx_key:
487 ctx_keys = [line, ]
488 else:
489 ctx_keys = copy.deepcopy(main_ctx_key)
490 main_ctx_key = []
491
492 current_context_lines = []
493 new_ctx = False
494 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
495 elif (line.startswith("address-family ") or
496 line.startswith("vnc defaults") or
497 line.startswith("vnc l2-group") or
498 line.startswith("vnc nve-group") or
499 line.startswith("member pseudowire")):
500 main_ctx_key = []
501
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)
506 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
507
508 if line == "address-family ipv6" and not ctx_keys[0].startswith("mpls ldp"):
509 ctx_keys.append("address-family ipv6 unicast")
510 elif line == "address-family ipv4" and not ctx_keys[0].startswith("mpls ldp"):
511 ctx_keys.append("address-family ipv4 unicast")
512 elif line == "address-family evpn":
513 ctx_keys.append("address-family l2vpn evpn")
514 else:
515 ctx_keys.append(line)
516
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)
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)
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
541 else:
542 # Continuing in an existing context, add non-commented lines to it
543 current_context_lines.append(line)
544 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
545
546 # Save the context of the last one
547 self.save_contexts(ctx_keys, current_context_lines)
548
549
550 def line_to_vtysh_conft(ctx_keys, line, delete, bindir, confdir):
551 """
552 Return the vtysh command for the specified context line
553 """
554
555 cmd = []
556 cmd.append(str(bindir + '/vtysh'))
557 cmd.append('--config_dir')
558 cmd.append(confdir)
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
602
603 def line_for_vtysh_file(ctx_keys, line, delete):
604 """
605 Return the command as it would appear in frr.conf
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
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
650
651
652 def get_normalized_ipv6_line(line):
653 """
654 Return a normalized IPv6 line as produced by frr,
655 with all letters in lower case and trailing and leading
656 zeros removed, and only the network portion present if
657 the IPv6 word is a network
658 """
659 norm_line = ""
660 words = line.split(' ')
661 for word in words:
662 if ":" in word:
663 norm_word = None
664 if "/" in word:
665 try:
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)
672 except ValueError:
673 pass
674 if not norm_word:
675 try:
676 norm_word = '%s' % IPv6Address(word)
677 except ValueError:
678 norm_word = word
679 else:
680 norm_word = word
681 norm_line = norm_line + " " + norm_word
682
683 return norm_line.strip()
684
685
686 def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
687 for (ctx_keys, line) in lines:
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
695 return False
696
697
698 def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
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:
705 deleted = False
706
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))
749
750 if not found_add_swpx_peergroup:
751 tmp_ctx_keys = list(ctx_keys)
752 tmp_ctx_keys.append('address-family ipv4 unicast')
753 tmp_ctx_keys = tuple(tmp_ctx_keys)
754 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
755
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
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
779 bfd_search_string = bfd_nbr + r' bfd (\S+) (\S+) (\S+)'
780
781 for (ctx_keys, add_line) in lines_to_add:
782 re_add_nbr_bfd_timers = re.search(bfd_search_string, add_line)
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
790 '''
791 We changed how we display the neighbor interface command. Older
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 '''
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.
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:
855 deleted = True
856 lines_to_del_to_del.append((ctx_keys, line))
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:
869 lines_to_del_to_del.append((ctx_keys, line))
870
871 '''
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.
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):
883 lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
884 lines_to_add_to_del.append((ctx[0], None))
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
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
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
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))
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
990 def 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
1018 def 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 = []
1026 delete_bgpd = False
1027
1028 # Find contexts that are in newconf but not in running
1029 # Find contexts that are in running but not in newconf
1030 for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
1031
1032 if running_ctx_keys not in newconf.contexts:
1033
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
1036 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1037 # running but not in newconf.
1038 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
1039 delete_bgpd = True
1040 lines_to_del.append((running_ctx_keys, None))
1041
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'):
1044 for line in running_ctx.lines:
1045 lines_to_del.append((running_ctx_keys, line))
1046
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:
1050 continue
1051
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
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
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
1080 # Non-global context
1081 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
1082 lines_to_del.append((running_ctx_keys, None))
1083
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
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
1094 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
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
1107 for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
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
1115 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
1116 (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del)
1117
1118 return (lines_to_add, lines_to_del)
1119
1120
1121
1122 def vtysh_config_available(bindir, confdir):
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:
1130 cmd = [str(bindir + '/vtysh'), '--config_dir', confdir, '-c', 'conf t']
1131 output = subprocess.check_output(cmd).strip()
1132
1133 if 'VTY configuration is locked by other VTY' in output.decode('utf-8'):
1134 print(output)
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"
1140 print(msg)
1141 log.error(msg)
1142 return False
1143
1144 return True
1145
1146
1147 if __name__ == '__main__':
1148 # Command line options
1149 parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
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)
1155 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
1156 parser.add_argument('filename', help='Location of new frr config file')
1157 parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
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')
1161 parser.add_argument('--daemon', help='daemon for which want to replace the config', default='')
1162
1163 args = parser.parse_args()
1164
1165 # Logging
1166 # For --test log to stdout
1167 # For --reload log to /var/log/frr/frr-reload.log
1168 if args.test or args.stdout:
1169 logging.basicConfig(level=logging.INFO,
1170 format='%(asctime)s %(levelname)5s: %(message)s')
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
1176 elif args.reload:
1177 if not os.path.isdir('/var/log/frr/'):
1178 os.makedirs('/var/log/frr/')
1179
1180 logging.basicConfig(filename='/var/log/frr/frr-reload.log',
1181 level=logging.INFO,
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')
1187 log = logging.getLogger(__name__)
1188
1189 # Verify the new config file is valid
1190 if not os.path.isfile(args.filename):
1191 msg = "Filename %s does not exist" % args.filename
1192 print(msg)
1193 log.error(msg)
1194 sys.exit(1)
1195
1196 if not os.path.getsize(args.filename):
1197 msg = "Filename %s is an empty file" % args.filename
1198 print(msg)
1199 log.error(msg)
1200 sys.exit(1)
1201
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
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
1223 # Verify that 'service integrated-vtysh-config' is configured
1224 vtysh_filename = args.confdir + '/vtysh.conf'
1225 service_integrated_vtysh_config = True
1226
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()
1231
1232 if line == 'no service integrated-vtysh-config':
1233 service_integrated_vtysh_config = False
1234 break
1235
1236 if not service_integrated_vtysh_config and not args.daemon:
1237 msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1238 print(msg)
1239 log.error(msg)
1240 sys.exit(1)
1241
1242 if args.debug:
1243 log.setLevel(logging.DEBUG)
1244
1245 log.info('Called via "%s"', str(args))
1246
1247 # Create a Config object from the config generated by newconf
1248 newconf = Config()
1249 newconf.load_from_file(args.filename, args.bindir, args.confdir)
1250 reload_ok = True
1251
1252 if args.test:
1253
1254 # Create a Config object from the running config
1255 running = Config()
1256
1257 if args.input:
1258 running.load_from_file(args.input, args.bindir, args.confdir)
1259 else:
1260 running.load_from_show_running(args.bindir, args.confdir, args.daemon)
1261
1262 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
1263 lines_to_configure = []
1264
1265 if lines_to_del:
1266 print("\nLines To Delete")
1267 print("===============")
1268
1269 for (ctx_keys, line) in lines_to_del:
1270
1271 if line == '!':
1272 continue
1273
1274 cmd = line_for_vtysh_file(ctx_keys, line, True)
1275 lines_to_configure.append(cmd)
1276 print(cmd)
1277
1278 if lines_to_add:
1279 print("\nLines To Add")
1280 print("============")
1281
1282 for (ctx_keys, line) in lines_to_add:
1283
1284 if line == '!':
1285 continue
1286
1287 cmd = line_for_vtysh_file(ctx_keys, line, False)
1288 lines_to_configure.append(cmd)
1289 print(cmd)
1290
1291 elif args.reload:
1292
1293 # We will not be able to do anything, go ahead and exit(1)
1294 if not vtysh_config_available(args.bindir, args.confdir):
1295 sys.exit(1)
1296
1297 log.debug('New Frr Config\n%s', newconf.get_lines())
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:
1301 #
1302 # router bgp 10
1303 # neighbor 1.1.1.1 remote-as 50
1304 # neighbor 1.1.1.1 route-map FOO out
1305 #
1306 # and this config in the newconf config file
1307 #
1308 # router bgp 10
1309 # neighbor 1.1.1.1 remote-as 999
1310 # neighbor 1.1.1.1 route-map FOO out
1311 #
1312 #
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
1316 #
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
1321 # There are many keywords in FRR that can only appear one time under
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
1336 for x in range(2):
1337 running = Config()
1338 running.load_from_show_running(args.bindir, args.confdir, args.daemon)
1339 log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
1340
1341 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
1342
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
1348 # Only do deletes on the first pass. The reason being if we
1349 # configure a bgp neighbor via "neighbor swp1 interface" FRR
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:
1362 for (ctx_keys, line) in lines_to_del:
1363
1364 if line == '!':
1365 continue
1366
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
1370 cmd = line_to_vtysh_conft(ctx_keys, line, True, args.bindir, args.confdir)
1371 original_cmd = cmd
1372
1373 # Some commands in frr are picky about taking a "no" of the entire line.
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.
1377 #
1378 # Example:
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
1381 # % Unknown command.
1382 # frr(config-if)# no ip ospf authentication message-digest
1383 # % Unknown command.
1384 # frr(config-if)# no ip ospf authentication
1385 # frr(config-if)#
1386
1387 while True:
1388 try:
1389 _ = subprocess.check_output(cmd)
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
1397 log.info('Failed to execute %s', ' '.join(cmd))
1398 last_arg = cmd[-1].split(' ')
1399
1400 if len(last_arg) <= 2:
1401 log.error('"%s" we failed to remove this command', original_cmd)
1402 break
1403
1404 new_last_arg = last_arg[0:-1]
1405 cmd[-1] = ' '.join(new_last_arg)
1406 else:
1407 log.info('Executed "%s"', ' '.join(cmd))
1408 break
1409
1410 if lines_to_add:
1411 lines_to_configure = []
1412
1413 for (ctx_keys, line) in lines_to_add:
1414
1415 if line == '!':
1416 continue
1417
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
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
1431 filename = args.rundir + "/reload-%s.txt" % random_string
1432 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
1433
1434 with open(filename, 'w') as fh:
1435 for line in lines_to_configure:
1436 fh.write(line + '\n')
1437
1438 try:
1439 subprocess.check_output([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-f', filename])
1440 except subprocess.CalledProcessError as e:
1441 log.warning("frr-reload.py failed due to\n%s" % e.output)
1442 reload_ok = False
1443 os.unlink(filename)
1444
1445 # Make these changes persistent
1446 target = str(args.confdir + '/frr.conf')
1447 if args.overwrite or (not args.daemon and args.filename != target):
1448 subprocess.call([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-c', 'write'])
1449
1450 if not reload_ok:
1451 sys.exit(1)