]> git.proxmox.com Git - mirror_frr.git/blame - tools/quagga-reload.py
pimd: make the json output a bit more machine-friendly
[mirror_frr.git] / tools / quagga-reload.py
CommitLineData
2fc76430 1#!/usr/bin/python
50e24903
DS
2# Quagga Reloader
3# Copyright (C) 2014 Cumulus Networks, Inc.
4#
5# This file is part of Quagga.
6#
7# Quagga 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# Quagga 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 Quagga; 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#
2fc76430
DS
22"""
23This program
24- reads a quagga configuration text file
25- reads quagga's current running configuration via "vtysh -c 'show running'"
26- compares the two configs and determines what commands to execute to
27 synchronize quagga's running configuration with the configuation in the
28 text file
29"""
30
31import argparse
32import copy
33import logging
34import os
4a2587c6 35import random
9fe88bc7 36import re
4a2587c6 37import string
2fc76430
DS
38import subprocess
39import sys
40from collections import OrderedDict
41from ipaddr import IPv6Address
4a2587c6
DW
42from pprint import pformat
43
2fc76430 44
a782e613
DW
45log = logging.getLogger(__name__)
46
47
276887bb
DW
48class VtyshMarkException(Exception):
49 pass
50
51
2fc76430 52class Context(object):
4a2587c6 53
2fc76430
DS
54 """
55 A Context object represents a section of quagga configuration such as:
56!
57interface swp3
58 description swp3 -> r8's swp1
59 ipv6 nd suppress-ra
60 link-detect
61!
62
63or a single line context object such as this:
64
65ip forwarding
66
67 """
68
69 def __init__(self, keys, lines):
70 self.keys = keys
71 self.lines = lines
72
73 # Keep a dictionary of the lines, this is to make it easy to tell if a
74 # line exists in this Context
75 self.dlines = OrderedDict()
76
77 for ligne in lines:
78 self.dlines[ligne] = True
79
80 def add_lines(self, lines):
81 """
82 Add lines to specified context
83 """
84
85 self.lines.extend(lines)
86
87 for ligne in lines:
88 self.dlines[ligne] = True
89
90
91class Config(object):
4a2587c6 92
2fc76430
DS
93 """
94 A quagga configuration is stored in a Config object. A Config object
95 contains a dictionary of Context objects where the Context keys
96 ('router ospf' for example) are our dictionary key.
97 """
98
99 def __init__(self):
100 self.lines = []
101 self.contexts = OrderedDict()
102
103 def load_from_file(self, filename):
104 """
105 Read configuration from specified file and slurp it into internal memory
106 The internal representation has been marked appropriately by passing it
107 through vtysh with the -m parameter
108 """
a782e613 109 log.info('Loading Config object from file %s', filename)
2fc76430
DS
110
111 try:
4a2587c6 112 file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename])
2fc76430 113 except subprocess.CalledProcessError as e:
276887bb 114 raise VtyshMarkException(str(e))
2fc76430
DS
115
116 for line in file_output.split('\n'):
117 line = line.strip()
118 if ":" in line:
119 qv6_line = get_normalized_ipv6_line(line)
120 self.lines.append(qv6_line)
121 else:
122 self.lines.append(line)
123
124 self.load_contexts()
125
126 def load_from_show_running(self):
127 """
128 Read running configuration and slurp it into internal memory
129 The internal representation has been marked appropriately by passing it
130 through vtysh with the -m parameter
131 """
a782e613 132 log.info('Loading Config object from vtysh show running')
2fc76430
DS
133
134 try:
4a2587c6
DW
135 config_text = subprocess.check_output(
136 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
137 shell=True)
2fc76430 138 except subprocess.CalledProcessError as e:
276887bb 139 raise VtyshMarkException(str(e))
2fc76430 140
2fc76430
DS
141 for line in config_text.split('\n'):
142 line = line.strip()
143
144 if (line == 'Building configuration...' or
145 line == 'Current configuration:' or
4a2587c6 146 not line):
2fc76430
DS
147 continue
148
149 self.lines.append(line)
150
151 self.load_contexts()
152
153 def get_lines(self):
154 """
155 Return the lines read in from the configuration
156 """
157
158 return '\n'.join(self.lines)
159
160 def get_contexts(self):
161 """
162 Return the parsed context as strings for display, log etc.
163 """
164
165 for (_, ctx) in sorted(self.contexts.iteritems()):
166 print str(ctx) + '\n'
167
168 def save_contexts(self, key, lines):
169 """
170 Save the provided key and lines as a context
171 """
172
173 if not key:
174 return
175
176 if lines:
177 if tuple(key) not in self.contexts:
178 ctx = Context(tuple(key), lines)
179 self.contexts[tuple(key)] = ctx
180 else:
181 ctx = self.contexts[tuple(key)]
182 ctx.add_lines(lines)
183
184 else:
185 if tuple(key) not in self.contexts:
186 ctx = Context(tuple(key), [])
187 self.contexts[tuple(key)] = ctx
188
189 def load_contexts(self):
190 """
191 Parse the configuration and create contexts for each appropriate block
192 """
193
194 current_context_lines = []
195 ctx_keys = []
196
197 '''
198 The end of a context is flagged via the 'end' keyword:
199
200!
201interface swp52
202 ipv6 nd suppress-ra
203 link-detect
204!
205end
206router bgp 10
207 bgp router-id 10.0.0.1
208 bgp log-neighbor-changes
209 no bgp default ipv4-unicast
210 neighbor EBGP peer-group
211 neighbor EBGP advertisement-interval 1
212 neighbor EBGP timers connect 10
213 neighbor 2001:40:1:4::6 remote-as 40
214 neighbor 2001:40:1:8::a remote-as 40
215!
216end
217 address-family ipv6
218 neighbor IBGPv6 activate
219 neighbor 2001:10::2 peer-group IBGPv6
220 neighbor 2001:10::3 peer-group IBGPv6
221 exit-address-family
222!
223end
224router ospf
225 ospf router-id 10.0.0.1
226 log-adjacency-changes detail
227 timers throttle spf 0 50 5000
228!
229end
230 '''
231
232 # The code assumes that its working on the output from the "vtysh -m"
233 # command. That provides the appropriate markers to signify end of
234 # a context. This routine uses that to build the contexts for the
235 # config.
236 #
237 # There are single line contexts such as "log file /media/node/zebra.log"
238 # and multi-line contexts such as "router ospf" and subcontexts
239 # within a context such as "address-family" within "router bgp"
240 # In each of these cases, the first line of the context becomes the
241 # key of the context. So "router bgp 10" is the key for the non-address
242 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
243 # the key for the subcontext and so on.
2fc76430
DS
244 ctx_keys = []
245 main_ctx_key = []
246 new_ctx = True
2fc76430
DS
247
248 # the keywords that we know are single line contexts. bgp in this case
249 # is not the main router bgp block, but enabling multi-instance
2fed5dcd
DW
250 oneline_ctx_keywords = ("access-list ",
251 "bgp ",
252 "debug ",
253 "dump ",
254 "enable ",
255 "hostname ",
256 "ip ",
257 "ipv6 ",
258 "log ",
259 "password ",
260 "ptm-enable",
261 "router-id ",
262 "service ",
263 "table ",
264 "username ",
265 "zebra ")
266
2fc76430
DS
267 for line in self.lines:
268
269 if not line:
270 continue
271
272 if line.startswith('!') or line.startswith('#'):
273 continue
274
275 # one line contexts
276 if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords):
277 self.save_contexts(ctx_keys, current_context_lines)
278
279 # Start a new context
280 main_ctx_key = []
4a2587c6 281 ctx_keys = [line, ]
2fc76430
DS
282 current_context_lines = []
283
a782e613 284 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
2fc76430
DS
285 self.save_contexts(ctx_keys, current_context_lines)
286 new_ctx = True
287
288 elif line == "end":
289 self.save_contexts(ctx_keys, current_context_lines)
a782e613 290 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
2fc76430
DS
291
292 # Start a new context
293 new_ctx = True
294 main_ctx_key = []
295 ctx_keys = []
296 current_context_lines = []
297
298 elif line == "exit-address-family" or line == "exit":
299 # if this exit is for address-family ipv4 unicast, ignore the pop
300 if main_ctx_key:
301 self.save_contexts(ctx_keys, current_context_lines)
302
303 # Start a new context
304 ctx_keys = copy.deepcopy(main_ctx_key)
305 current_context_lines = []
a782e613 306 log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
2fc76430
DS
307
308 elif new_ctx is True:
309 if not main_ctx_key:
4a2587c6 310 ctx_keys = [line, ]
2fc76430
DS
311 else:
312 ctx_keys = copy.deepcopy(main_ctx_key)
313 main_ctx_key = []
314
315 current_context_lines = []
316 new_ctx = False
a782e613 317 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
2fc76430
DS
318
319 elif "address-family " in line:
320 main_ctx_key = []
321
0b960b4d
DW
322 # Save old context first
323 self.save_contexts(ctx_keys, current_context_lines)
324 current_context_lines = []
325 main_ctx_key = copy.deepcopy(ctx_keys)
a782e613 326 log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
2fc76430 327
0b960b4d
DW
328 if line == "address-family ipv6":
329 ctx_keys.append("address-family ipv6 unicast")
330 elif line == "address-family ipv4":
331 ctx_keys.append("address-family ipv4 unicast")
332 else:
333 ctx_keys.append(line)
2fc76430
DS
334
335 else:
336 # Continuing in an existing context, add non-commented lines to it
337 current_context_lines.append(line)
a782e613 338 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
2fc76430
DS
339
340 # Save the context of the last one
341 self.save_contexts(ctx_keys, current_context_lines)
342
4a2587c6 343
2fc76430
DS
344def line_to_vtysh_conft(ctx_keys, line, delete):
345 """
4a2587c6 346 Return the vtysh command for the specified context line
2fc76430
DS
347 """
348
349 cmd = []
350 cmd.append('vtysh')
351 cmd.append('-c')
352 cmd.append('conf t')
353
354 if line:
355 for ctx_key in ctx_keys:
356 cmd.append('-c')
357 cmd.append(ctx_key)
358
359 line = line.lstrip()
360
361 if delete:
362 cmd.append('-c')
363
364 if line.startswith('no '):
365 cmd.append('%s' % line[3:])
366 else:
367 cmd.append('no %s' % line)
368
369 else:
370 cmd.append('-c')
371 cmd.append(line)
372
373 # If line is None then we are typically deleting an entire
374 # context ('no router ospf' for example)
375 else:
376
377 if delete:
378
379 # Only put the 'no' on the last sub-context
380 for ctx_key in ctx_keys:
381 cmd.append('-c')
382
383 if ctx_key == ctx_keys[-1]:
384 cmd.append('no %s' % ctx_key)
385 else:
386 cmd.append('%s' % ctx_key)
387 else:
388 for ctx_key in ctx_keys:
389 cmd.append('-c')
390 cmd.append(ctx_key)
391
392 return cmd
393
4a2587c6
DW
394
395def line_for_vtysh_file(ctx_keys, line, delete):
396 """
397 Return the command as it would appear in Quagga.conf
398 """
399 cmd = []
400
401 if line:
402 for (i, ctx_key) in enumerate(ctx_keys):
403 cmd.append(' ' * i + ctx_key)
404
405 line = line.lstrip()
406 indent = len(ctx_keys) * ' '
407
408 if delete:
409 if line.startswith('no '):
410 cmd.append('%s%s' % (indent, line[3:]))
411 else:
412 cmd.append('%sno %s' % (indent, line))
413
414 else:
415 cmd.append(indent + line)
416
417 # If line is None then we are typically deleting an entire
418 # context ('no router ospf' for example)
419 else:
420 if delete:
421
422 # Only put the 'no' on the last sub-context
423 for ctx_key in ctx_keys:
424
425 if ctx_key == ctx_keys[-1]:
426 cmd.append('no %s' % ctx_key)
427 else:
428 cmd.append('%s' % ctx_key)
429 else:
430 for ctx_key in ctx_keys:
431 cmd.append(ctx_key)
432
433 return '\n' + '\n'.join(cmd)
434
435
2fc76430
DS
436def get_normalized_ipv6_line(line):
437 """
438 Return a normalized IPv6 line as produced by quagga,
439 with all letters in lower case and trailing and leading
440 zeros removed
441 """
442 norm_line = ""
443 words = line.split(' ')
444 for word in words:
445 if ":" in word:
446 try:
447 norm_word = str(IPv6Address(word)).lower()
448 except:
449 norm_word = word
450 else:
451 norm_word = word
452 norm_line = norm_line + " " + norm_word
453
454 return norm_line.strip()
455
4a2587c6 456
9fe88bc7
DW
457def line_exist(lines, target_ctx_keys, target_line):
458 for (ctx_keys, line) in lines:
459 if ctx_keys == target_ctx_keys and line == target_line:
460 return True
461 return False
462
463
9b166171 464def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
9fe88bc7
DW
465
466 # Quite possibly the most confusing (while accurate) variable names in history
467 lines_to_add_to_del = []
468 lines_to_del_to_del = []
469
470 for (ctx_keys, line) in lines_to_del:
9b166171
DW
471 deleted = False
472
926ea62e 473 if ctx_keys[0].startswith('router bgp') and line and line.startswith('neighbor '):
9b166171
DW
474 """
475 BGP changed how it displays swpX peers that are part of peer-group. Older
476 versions of quagga would display these on separate lines:
477 neighbor swp1 interface
478 neighbor swp1 peer-group FOO
479
480 but today we display via a single line
481 neighbor swp1 interface peer-group FOO
482
483 This change confuses quagga-reload.py so check to see if we are deleting
484 neighbor swp1 interface peer-group FOO
485
486 and adding
487 neighbor swp1 interface
488 neighbor swp1 peer-group FOO
489
490 If so then chop the del line and the corresponding add lines
491 """
492
9fe88bc7 493 re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
9b166171 494 re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
9fe88bc7 495
9b166171
DW
496 if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
497 swpx_interface = None
498 swpx_peergroup = None
499
500 if re_swpx_int_peergroup:
501 swpx = re_swpx_int_peergroup.group(1)
502 peergroup = re_swpx_int_peergroup.group(2)
503 swpx_interface = "neighbor %s interface" % swpx
504 elif re_swpx_int_v6only_peergroup:
505 swpx = re_swpx_int_v6only_peergroup.group(1)
506 peergroup = re_swpx_int_v6only_peergroup.group(2)
507 swpx_interface = "neighbor %s interface v6only" % swpx
9fe88bc7 508
9b166171 509 swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
9fe88bc7
DW
510 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
511 found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
b1e0634c 512 tmp_ctx_keys = tuple(list(ctx_keys))
9b166171
DW
513
514 if not found_add_swpx_peergroup:
515 tmp_ctx_keys = list(ctx_keys)
516 tmp_ctx_keys.append('address-family ipv4 unicast')
517 tmp_ctx_keys = tuple(tmp_ctx_keys)
518 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
519
520 if not found_add_swpx_peergroup:
521 tmp_ctx_keys = list(ctx_keys)
522 tmp_ctx_keys.append('address-family ipv6 unicast')
523 tmp_ctx_keys = tuple(tmp_ctx_keys)
524 found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
9fe88bc7
DW
525
526 if found_add_swpx_interface and found_add_swpx_peergroup:
9b166171 527 deleted = True
9fe88bc7
DW
528 lines_to_del_to_del.append((ctx_keys, line))
529 lines_to_add_to_del.append((ctx_keys, swpx_interface))
9b166171
DW
530 lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
531
b3a39dc5
DD
532 """
533 In 3.0.1 we changed how we display neighbor interface command. Older
534 versions of quagga would display the following:
535 neighbor swp1 interface
536 neighbor swp1 remote-as external
537 neighbor swp1 capability extended-nexthop
538
539 but today we display via a single line
540 neighbor swp1 interface remote-as external
541
542 and capability extended-nexthop is no longer needed because we
543 automatically enable it when the neighbor is of type interface.
544
545 This change confuses quagga-reload.py so check to see if we are deleting
546 neighbor swp1 interface remote-as (external|internal|ASNUM)
547
548 and adding
549 neighbor swp1 interface
550 neighbor swp1 remote-as (external|internal|ASNUM)
551 neighbor swp1 capability extended-nexthop
552
553 If so then chop the del line and the corresponding add lines
554 """
555 re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
556 re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
557
558 if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
559 swpx_interface = None
560 swpx_remoteas = None
561
562 if re_swpx_int_remoteas:
563 swpx = re_swpx_int_remoteas.group(1)
564 remoteas = re_swpx_int_remoteas.group(2)
565 swpx_interface = "neighbor %s interface" % swpx
566 elif re_swpx_int_v6only_remoteas:
567 swpx = re_swpx_int_v6only_remoteas.group(1)
568 remoteas = re_swpx_int_v6only_remoteas.group(2)
569 swpx_interface = "neighbor %s interface v6only" % swpx
570
571 swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
572 found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
573 found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
574 tmp_ctx_keys = tuple(list(ctx_keys))
575
576 if found_add_swpx_interface and found_add_swpx_remoteas:
577 deleted = True
578 lines_to_del_to_del.append((ctx_keys, line))
579 lines_to_add_to_del.append((ctx_keys, swpx_interface))
580 lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
581
9b166171
DW
582 if not deleted:
583 found_add_line = line_exist(lines_to_add, ctx_keys, line)
584
585 if found_add_line:
586 lines_to_del_to_del.append((ctx_keys, line))
587 lines_to_add_to_del.append((ctx_keys, line))
588 else:
589 '''
590 We have commands that used to be displayed in the global part
591 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
592
593 # old way
594 router bgp 64900
595 neighbor ISL advertisement-interval 0
596
597 vs.
598
599 # new way
600 router bgp 64900
601 address-family ipv4 unicast
602 neighbor ISL advertisement-interval 0
603
604 Look to see if we are deleting it in one format just to add it back in the other
605 '''
606 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
607 tmp_ctx_keys = list(ctx_keys)[:-1]
608 tmp_ctx_keys = tuple(tmp_ctx_keys)
609
610 found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
611
612 if found_add_line:
613 lines_to_del_to_del.append((ctx_keys, line))
614 lines_to_add_to_del.append((tmp_ctx_keys, line))
9fe88bc7
DW
615
616 for (ctx_keys, line) in lines_to_del_to_del:
617 lines_to_del.remove((ctx_keys, line))
618
619 for (ctx_keys, line) in lines_to_add_to_del:
620 lines_to_add.remove((ctx_keys, line))
621
622 return (lines_to_add, lines_to_del)
623
624
2fc76430
DS
625def compare_context_objects(newconf, running):
626 """
627 Create a context diff for the two specified contexts
628 """
629
630 # Compare the two Config objects to find the lines that we need to add/del
631 lines_to_add = []
632 lines_to_del = []
926ea62e 633 delete_bgpd = False
2fc76430
DS
634
635 # Find contexts that are in newconf but not in running
636 # Find contexts that are in running but not in newconf
637 for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
638
639 if running_ctx_keys not in newconf.contexts:
640
ab5f8310
DW
641 # We check that the len is 1 here so that we only look at ('router bgp 10')
642 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
926ea62e 643 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
ab5f8310
DW
644 # running but not in newconf.
645 if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
926ea62e
DW
646 delete_bgpd = True
647 lines_to_del.append((running_ctx_keys, None))
648
649 # If this is an address-family under 'router bgp' and we are already deleting the
650 # entire 'router bgp' context then ignore this sub-context
651 elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
76f69d1c 652 continue
514665b9 653
2fc76430 654 # Non-global context
926ea62e 655 elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
2fc76430
DS
656 lines_to_del.append((running_ctx_keys, None))
657
658 # Global context
659 else:
660 for line in running_ctx.lines:
661 lines_to_del.append((running_ctx_keys, line))
662
663 # Find the lines within each context to add
664 # Find the lines within each context to del
665 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
666
667 if newconf_ctx_keys in running.contexts:
668 running_ctx = running.contexts[newconf_ctx_keys]
669
670 for line in newconf_ctx.lines:
671 if line not in running_ctx.dlines:
672 lines_to_add.append((newconf_ctx_keys, line))
673
674 for line in running_ctx.lines:
675 if line not in newconf_ctx.dlines:
676 lines_to_del.append((newconf_ctx_keys, line))
677
678 for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
679
680 if newconf_ctx_keys not in running.contexts:
681 lines_to_add.append((newconf_ctx_keys, None))
682
683 for line in newconf_ctx.lines:
684 lines_to_add.append((newconf_ctx_keys, line))
685
9b166171 686 (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
9fe88bc7 687
926ea62e 688 return (lines_to_add, lines_to_del)
2fc76430
DS
689
690if __name__ == '__main__':
691 # Command line options
692 parser = argparse.ArgumentParser(description='Dynamically apply diff in quagga configs')
693 parser.add_argument('--input', help='Read running config from file instead of "show running"')
694 group = parser.add_mutually_exclusive_group(required=True)
695 group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
696 group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
697 parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
cc146ecc 698 parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
2fc76430
DS
699 parser.add_argument('filename', help='Location of new quagga config file')
700 args = parser.parse_args()
701
702 # Logging
703 # For --test log to stdout
704 # For --reload log to /var/log/quagga/quagga-reload.log
cc146ecc 705 if args.test or args.stdout:
c50aceee 706 logging.basicConfig(level=logging.INFO,
2fc76430 707 format='%(asctime)s %(levelname)5s: %(message)s')
926ea62e
DW
708
709 # Color the errors and warnings in red
710 logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
711 logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
712
2fc76430
DS
713 elif args.reload:
714 if not os.path.isdir('/var/log/quagga/'):
715 os.makedirs('/var/log/quagga/')
716
717 logging.basicConfig(filename='/var/log/quagga/quagga-reload.log',
c50aceee 718 level=logging.INFO,
2fc76430
DS
719 format='%(asctime)s %(levelname)5s: %(message)s')
720
721 # argparse should prevent this from happening but just to be safe...
722 else:
723 raise Exception('Must specify --reload or --test')
a782e613 724 log = logging.getLogger(__name__)
2fc76430 725
76f69d1c
DW
726 # Verify the new config file is valid
727 if not os.path.isfile(args.filename):
728 print "Filename %s does not exist" % args.filename
729 sys.exit(1)
730
731 if not os.path.getsize(args.filename):
732 print "Filename %s is an empty file" % args.filename
733 sys.exit(1)
734
76f69d1c
DW
735 # Verify that 'service integrated-vtysh-config' is configured
736 vtysh_filename = '/etc/quagga/vtysh.conf'
6ac9179c 737 service_integrated_vtysh_config = True
76f69d1c 738
f850d14d
DW
739 if os.path.isfile(vtysh_filename):
740 with open(vtysh_filename, 'r') as fh:
741 for line in fh.readlines():
742 line = line.strip()
76f69d1c 743
6ac9179c
DD
744 if line == 'no service integrated-vtysh-config':
745 service_integrated_vtysh_config = False
f850d14d 746 break
76f69d1c
DW
747
748 if not service_integrated_vtysh_config:
749 print "'service integrated-vtysh-config' is not configured, this is required for 'service quagga reload'"
750 sys.exit(1)
2fc76430 751
c50aceee 752 if args.debug:
a782e613 753 log.setLevel(logging.DEBUG)
c50aceee 754
a782e613 755 log.info('Called via "%s"', str(args))
c50aceee 756
2fc76430
DS
757 # Create a Config object from the config generated by newconf
758 newconf = Config()
759 newconf.load_from_file(args.filename)
2fc76430
DS
760
761 if args.test:
762
763 # Create a Config object from the running config
764 running = Config()
765
766 if args.input:
767 running.load_from_file(args.input)
768 else:
769 running.load_from_show_running()
770
926ea62e 771 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
4a2587c6 772 lines_to_configure = []
2fc76430
DS
773
774 if lines_to_del:
775 print "\nLines To Delete"
776 print "==============="
777
778 for (ctx_keys, line) in lines_to_del:
779
780 if line == '!':
781 continue
782
4a2587c6
DW
783 cmd = line_for_vtysh_file(ctx_keys, line, True)
784 lines_to_configure.append(cmd)
9fe88bc7 785 print cmd
2fc76430
DS
786
787 if lines_to_add:
788 print "\nLines To Add"
789 print "============"
790
791 for (ctx_keys, line) in lines_to_add:
792
793 if line == '!':
794 continue
795
4a2587c6
DW
796 cmd = line_for_vtysh_file(ctx_keys, line, False)
797 lines_to_configure.append(cmd)
9fe88bc7 798 print cmd
2fc76430 799
2fc76430
DS
800 elif args.reload:
801
a782e613 802 log.debug('New Quagga Config\n%s', newconf.get_lines())
2fc76430
DS
803
804 # This looks a little odd but we have to do this twice...here is why
805 # If the user had this running bgp config:
4a2587c6 806 #
2fc76430
DS
807 # router bgp 10
808 # neighbor 1.1.1.1 remote-as 50
809 # neighbor 1.1.1.1 route-map FOO out
4a2587c6 810 #
2fc76430 811 # and this config in the newconf config file
4a2587c6 812 #
2fc76430
DS
813 # router bgp 10
814 # neighbor 1.1.1.1 remote-as 999
815 # neighbor 1.1.1.1 route-map FOO out
4a2587c6
DW
816 #
817 #
2fc76430
DS
818 # Then the script will do
819 # - no neighbor 1.1.1.1 remote-as 50
820 # - neighbor 1.1.1.1 remote-as 999
4a2587c6 821 #
2fc76430
DS
822 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
823 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
824 # configs again to put this line back.
825
826 for x in range(2):
827 running = Config()
828 running.load_from_show_running()
a782e613 829 log.debug('Running Quagga Config (Pass #%d)\n%s', x, running.get_lines())
2fc76430 830
926ea62e 831 (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
2fc76430
DS
832
833 if lines_to_del:
834 for (ctx_keys, line) in lines_to_del:
835
836 if line == '!':
837 continue
838
4a2587c6
DW
839 # 'no' commands are tricky, we can't just put them in a file and
840 # vtysh -f that file. See the next comment for an explanation
841 # of their quirks
2fc76430
DS
842 cmd = line_to_vtysh_conft(ctx_keys, line, True)
843 original_cmd = cmd
844
76f69d1c
DW
845 # Some commands in quagga are picky about taking a "no" of the entire line.
846 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
847 # only the beginning. If we hit one of these command an exception will be
848 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
4a2587c6 849 #
76f69d1c 850 # Example:
4a2587c6
DW
851 # quagga(config-if)# ip ospf authentication message-digest 1.1.1.1
852 # quagga(config-if)# no ip ospf authentication message-digest 1.1.1.1
76f69d1c 853 # % Unknown command.
4a2587c6 854 # quagga(config-if)# no ip ospf authentication message-digest
76f69d1c 855 # % Unknown command.
4a2587c6
DW
856 # quagga(config-if)# no ip ospf authentication
857 # quagga(config-if)#
2fc76430
DS
858
859 while True:
2fc76430
DS
860 try:
861 _ = subprocess.check_output(cmd)
862
863 except subprocess.CalledProcessError:
864
865 # - Pull the last entry from cmd (this would be
866 # 'no ip ospf authentication message-digest 1.1.1.1' in
867 # our example above
868 # - Split that last entry by whitespace and drop the last word
a782e613 869 log.warning('Failed to execute %s', ' '.join(cmd))
2fc76430
DS
870 last_arg = cmd[-1].split(' ')
871
872 if len(last_arg) <= 2:
a782e613 873 log.error('"%s" we failed to remove this command', original_cmd)
2fc76430
DS
874 break
875
876 new_last_arg = last_arg[0:-1]
877 cmd[-1] = ' '.join(new_last_arg)
878 else:
a782e613 879 log.info('Executed "%s"', ' '.join(cmd))
2fc76430
DS
880 break
881
2fc76430 882 if lines_to_add:
4a2587c6
DW
883 lines_to_configure = []
884
2fc76430
DS
885 for (ctx_keys, line) in lines_to_add:
886
887 if line == '!':
888 continue
889
4a2587c6
DW
890 cmd = line_for_vtysh_file(ctx_keys, line, False)
891 lines_to_configure.append(cmd)
892
893 if lines_to_configure:
894 random_string = ''.join(random.SystemRandom().choice(
895 string.ascii_uppercase +
896 string.digits) for _ in range(6))
897
898 filename = "/var/run/quagga/reload-%s.txt" % random_string
a782e613 899 log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
4a2587c6
DW
900
901 with open(filename, 'w') as fh:
902 for line in lines_to_configure:
903 fh.write(line + '\n')
904 subprocess.call(['/usr/bin/vtysh', '-f', filename])
905 os.unlink(filename)
2fc76430 906
926ea62e
DW
907 # Make these changes persistent
908 subprocess.call(['/usr/bin/vtysh', '-c', 'write'])