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