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