3 # Copyright (C) 2014 Cumulus Networks, Inc.
5 # This file is part of Frr.
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
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.
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
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
40 from collections
import OrderedDict
41 from ipaddr
import IPv6Address
42 from pprint
import pformat
45 log
= logging
.getLogger(__name__
)
48 class VtyshMarkException(Exception):
52 class Context(object):
55 A Context object represents a section of frr configuration such as:
58 description swp3 -> r8's swp1
63 or a single line context object such as this:
69 def __init__(self
, keys
, lines
):
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()
78 self
.dlines
[ligne
] = True
80 def add_lines(self
, lines
):
82 Add lines to specified context
85 self
.lines
.extend(lines
)
88 self
.dlines
[ligne
] = True
94 A frr 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.
101 self
.contexts
= OrderedDict()
103 def load_from_file(self
, filename
):
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
109 log
.info('Loading Config object from file %s', filename
)
112 file_output
= subprocess
.check_output(['/usr/bin/vtysh', '-m', '-f', filename
])
113 except subprocess
.CalledProcessError
as e
:
114 raise VtyshMarkException(str(e
))
116 for line
in file_output
.split('\n'):
119 qv6_line
= get_normalized_ipv6_line(line
)
120 self
.lines
.append(qv6_line
)
122 self
.lines
.append(line
)
126 def load_from_show_running(self
):
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
132 log
.info('Loading Config object from vtysh show running')
135 config_text
= subprocess
.check_output(
136 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
138 except subprocess
.CalledProcessError
as e
:
139 raise VtyshMarkException(str(e
))
141 for line
in config_text
.split('\n'):
144 if (line
== 'Building configuration...' or
145 line
== 'Current configuration:' or
149 self
.lines
.append(line
)
155 Return the lines read in from the configuration
158 return '\n'.join(self
.lines
)
160 def get_contexts(self
):
162 Return the parsed context as strings for display, log etc.
165 for (_
, ctx
) in sorted(self
.contexts
.iteritems()):
166 print str(ctx
) + '\n'
168 def save_contexts(self
, key
, lines
):
170 Save the provided key and lines as a context
177 if tuple(key
) not in self
.contexts
:
178 ctx
= Context(tuple(key
), lines
)
179 self
.contexts
[tuple(key
)] = ctx
181 ctx
= self
.contexts
[tuple(key
)]
185 if tuple(key
) not in self
.contexts
:
186 ctx
= Context(tuple(key
), [])
187 self
.contexts
[tuple(key
)] = ctx
189 def load_contexts(self
):
191 Parse the configuration and create contexts for each appropriate block
194 current_context_lines
= []
198 The end of a context is flagged via the 'end' keyword:
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
218 neighbor IBGPv6 activate
219 neighbor 2001:10::2 peer-group IBGPv6
220 neighbor 2001:10::3 peer-group IBGPv6
225 ospf router-id 10.0.0.1
226 log-adjacency-changes detail
227 timers throttle spf 0 50 5000
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
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.
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
250 oneline_ctx_keywords
= ("access-list ",
267 for line
in self
.lines
:
272 if line
.startswith('!') or line
.startswith('#'):
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
)
279 # Start a new context
282 current_context_lines
= []
284 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
285 self
.save_contexts(ctx_keys
, current_context_lines
)
289 self
.save_contexts(ctx_keys
, current_context_lines
)
290 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
292 # Start a new context
296 current_context_lines
= []
298 elif line
== "exit-address-family" or line
== "exit":
299 # if this exit is for address-family ipv4 unicast, ignore the pop
301 self
.save_contexts(ctx_keys
, current_context_lines
)
303 # Start a new context
304 ctx_keys
= copy
.deepcopy(main_ctx_key
)
305 current_context_lines
= []
306 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
308 elif new_ctx
is True:
312 ctx_keys
= copy
.deepcopy(main_ctx_key
)
315 current_context_lines
= []
317 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
319 elif "address-family " in line
:
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
)
326 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
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")
333 ctx_keys
.append(line
)
336 # Continuing in an existing context, add non-commented lines to it
337 current_context_lines
.append(line
)
338 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
340 # Save the context of the last one
341 self
.save_contexts(ctx_keys
, current_context_lines
)
344 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
346 Return the vtysh command for the specified context line
355 for ctx_key
in ctx_keys
:
364 if line
.startswith('no '):
365 cmd
.append('%s' % line
[3:])
367 cmd
.append('no %s' % line
)
373 # If line is None then we are typically deleting an entire
374 # context ('no router ospf' for example)
379 # Only put the 'no' on the last sub-context
380 for ctx_key
in ctx_keys
:
383 if ctx_key
== ctx_keys
[-1]:
384 cmd
.append('no %s' % ctx_key
)
386 cmd
.append('%s' % ctx_key
)
388 for ctx_key
in ctx_keys
:
395 def line_for_vtysh_file(ctx_keys
, line
, delete
):
397 Return the command as it would appear in Frr.conf
402 for (i
, ctx_key
) in enumerate(ctx_keys
):
403 cmd
.append(' ' * i
+ ctx_key
)
406 indent
= len(ctx_keys
) * ' '
409 if line
.startswith('no '):
410 cmd
.append('%s%s' % (indent
, line
[3:]))
412 cmd
.append('%sno %s' % (indent
, line
))
415 cmd
.append(indent
+ line
)
417 # If line is None then we are typically deleting an entire
418 # context ('no router ospf' for example)
422 # Only put the 'no' on the last sub-context
423 for ctx_key
in ctx_keys
:
425 if ctx_key
== ctx_keys
[-1]:
426 cmd
.append('no %s' % ctx_key
)
428 cmd
.append('%s' % ctx_key
)
430 for ctx_key
in ctx_keys
:
433 return '\n' + '\n'.join(cmd
)
436 def get_normalized_ipv6_line(line
):
438 Return a normalized IPv6 line as produced by frr,
439 with all letters in lower case and trailing and leading
443 words
= line
.split(' ')
447 norm_word
= str(IPv6Address(word
)).lower()
452 norm_line
= norm_line
+ " " + norm_word
454 return norm_line
.strip()
457 def 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
:
464 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
466 # Quite possibly the most confusing (while accurate) variable names in history
467 lines_to_add_to_del
= []
468 lines_to_del_to_del
= []
470 for (ctx_keys
, line
) in lines_to_del
:
473 if ctx_keys
[0].startswith('router bgp') and line
and line
.startswith('neighbor '):
475 BGP changed how it displays swpX peers that are part of peer-group. Older
476 versions of frr would display these on separate lines:
477 neighbor swp1 interface
478 neighbor swp1 peer-group FOO
480 but today we display via a single line
481 neighbor swp1 interface peer-group FOO
483 This change confuses frr-reload.py so check to see if we are deleting
484 neighbor swp1 interface peer-group FOO
487 neighbor swp1 interface
488 neighbor swp1 peer-group FOO
490 If so then chop the del line and the corresponding add lines
493 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
494 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
496 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
497 swpx_interface
= None
498 swpx_peergroup
= None
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
509 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
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
)
512 tmp_ctx_keys
= tuple(list(ctx_keys
))
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
)
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
)
526 if found_add_swpx_interface
and found_add_swpx_peergroup
:
528 lines_to_del_to_del
.append((ctx_keys
, line
))
529 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
530 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
533 In 3.0.1 we changed how we display neighbor interface command. Older
534 versions of frr would display the following:
535 neighbor swp1 interface
536 neighbor swp1 remote-as external
537 neighbor swp1 capability extended-nexthop
539 but today we display via a single line
540 neighbor swp1 interface remote-as external
542 and capability extended-nexthop is no longer needed because we
543 automatically enable it when the neighbor is of type interface.
545 This change confuses frr-reload.py so check to see if we are deleting
546 neighbor swp1 interface remote-as (external|internal|ASNUM)
549 neighbor swp1 interface
550 neighbor swp1 remote-as (external|internal|ASNUM)
551 neighbor swp1 capability extended-nexthop
553 If so then chop the del line and the corresponding add lines
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
)
558 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
559 swpx_interface
= None
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
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
))
576 if found_add_swpx_interface
and found_add_swpx_remoteas
:
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
))
583 In 3.0, we made bgp bestpath multipath as-relax command
584 automatically assume no-as-set since the lack of this option caused
585 weird routing problems and this problem was peculiar to this
586 implementation. When the running config is shown in relases after
587 3.0, the no-as-set is not shown as its the default. This causes
588 reload to unnecessarily unapply this option to only apply it back
589 again, causing unnecessary session resets. Handle this.
591 if ctx_keys
[0].startswith('router bgp') and line
and 'multipath-relax' in line
:
592 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
593 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
594 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
596 if re_asrelax_new
and found_asrelax_old
:
598 lines_to_del_to_del
.append((ctx_keys
, line
))
599 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
602 More old-to-new config handling. ip import-table no longer accepts
603 distance, but we honor the old syntax. But 'show running' shows only
604 the new syntax. This causes an unnecessary 'no import-table' followed
605 by the same old 'ip import-table' which causes perturbations in
606 announced routes leading to traffic blackholes. Fix this issue.
608 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
610 table_num
= re_importtbl
.group(1)
611 for ctx
in lines_to_add
:
612 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
613 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
614 lines_to_add_to_del
.append((ctx
[0], None))
617 ip/ipv6 prefix-list can be specified without a seq number. However,
618 the running config always adds 'seq x', where x is a number incremented
619 by 5 for every element, to the prefix list. So, ignore such lines as
620 well. Sample prefix-list lines:
621 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
622 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
623 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
625 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
628 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
629 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
630 re_ip_pfxlst
.group(6))
631 for ctx
in lines_to_add
:
632 if ctx
[0][0] == tmpline
:
633 lines_to_del_to_del
.append((ctx_keys
, None))
634 lines_to_add_to_del
.append(((tmpline
,), None))
637 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
640 lines_to_del_to_del
.append((ctx_keys
, line
))
641 lines_to_add_to_del
.append((ctx_keys
, line
))
644 We have commands that used to be displayed in the global part
645 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
649 neighbor ISL advertisement-interval 0
655 address-family ipv4 unicast
656 neighbor ISL advertisement-interval 0
658 Look to see if we are deleting it in one format just to add it back in the other
660 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
661 tmp_ctx_keys
= list(ctx_keys
)[:-1]
662 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
664 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
667 lines_to_del_to_del
.append((ctx_keys
, line
))
668 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
670 for (ctx_keys
, line
) in lines_to_del_to_del
:
671 lines_to_del
.remove((ctx_keys
, line
))
673 for (ctx_keys
, line
) in lines_to_add_to_del
:
674 lines_to_add
.remove((ctx_keys
, line
))
676 return (lines_to_add
, lines_to_del
)
679 def compare_context_objects(newconf
, running
):
681 Create a context diff for the two specified contexts
684 # Compare the two Config objects to find the lines that we need to add/del
689 # Find contexts that are in newconf but not in running
690 # Find contexts that are in running but not in newconf
691 for (running_ctx_keys
, running_ctx
) in running
.contexts
.iteritems():
693 if running_ctx_keys
not in newconf
.contexts
:
695 # We check that the len is 1 here so that we only look at ('router bgp 10')
696 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
697 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
698 # running but not in newconf.
699 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
701 lines_to_del
.append((running_ctx_keys
, None))
703 # We cannot do 'no interface' in quagga, and so deal with it
704 elif running_ctx_keys
[0].startswith('interface'):
705 for line
in running_ctx
.lines
:
706 lines_to_del
.append((running_ctx_keys
, line
))
708 # If this is an address-family under 'router bgp' and we are already deleting the
709 # entire 'router bgp' context then ignore this sub-context
710 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
714 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
715 lines_to_del
.append((running_ctx_keys
, None))
719 for line
in running_ctx
.lines
:
720 lines_to_del
.append((running_ctx_keys
, line
))
722 # Find the lines within each context to add
723 # Find the lines within each context to del
724 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
726 if newconf_ctx_keys
in running
.contexts
:
727 running_ctx
= running
.contexts
[newconf_ctx_keys
]
729 for line
in newconf_ctx
.lines
:
730 if line
not in running_ctx
.dlines
:
731 lines_to_add
.append((newconf_ctx_keys
, line
))
733 for line
in running_ctx
.lines
:
734 if line
not in newconf_ctx
.dlines
:
735 lines_to_del
.append((newconf_ctx_keys
, line
))
737 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
739 if newconf_ctx_keys
not in running
.contexts
:
740 lines_to_add
.append((newconf_ctx_keys
, None))
742 for line
in newconf_ctx
.lines
:
743 lines_to_add
.append((newconf_ctx_keys
, line
))
745 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
747 return (lines_to_add
, lines_to_del
)
749 if __name__
== '__main__':
750 # Command line options
751 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
752 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
753 group
= parser
.add_mutually_exclusive_group(required
=True)
754 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
755 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
756 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
757 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
758 parser
.add_argument('filename', help='Location of new frr config file')
759 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite Quagga.conf with running config output', default
=False)
760 args
= parser
.parse_args()
763 # For --test log to stdout
764 # For --reload log to /var/log/frr/frr-reload.log
765 if args
.test
or args
.stdout
:
766 logging
.basicConfig(level
=logging
.INFO
,
767 format
='%(asctime)s %(levelname)5s: %(message)s')
769 # Color the errors and warnings in red
770 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
771 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
774 if not os
.path
.isdir('/var/log/frr/'):
775 os
.makedirs('/var/log/frr/')
777 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
779 format
='%(asctime)s %(levelname)5s: %(message)s')
781 # argparse should prevent this from happening but just to be safe...
783 raise Exception('Must specify --reload or --test')
784 log
= logging
.getLogger(__name__
)
786 # Verify the new config file is valid
787 if not os
.path
.isfile(args
.filename
):
788 print "Filename %s does not exist" % args
.filename
791 if not os
.path
.getsize(args
.filename
):
792 print "Filename %s is an empty file" % args
.filename
795 # Verify that 'service integrated-vtysh-config' is configured
796 vtysh_filename
= '/etc/frr/vtysh.conf'
797 service_integrated_vtysh_config
= True
799 if os
.path
.isfile(vtysh_filename
):
800 with
open(vtysh_filename
, 'r') as fh
:
801 for line
in fh
.readlines():
804 if line
== 'no service integrated-vtysh-config':
805 service_integrated_vtysh_config
= False
808 if not service_integrated_vtysh_config
:
809 print "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
813 log
.setLevel(logging
.DEBUG
)
815 log
.info('Called via "%s"', str(args
))
817 # Create a Config object from the config generated by newconf
819 newconf
.load_from_file(args
.filename
)
823 # Create a Config object from the running config
827 running
.load_from_file(args
.input)
829 running
.load_from_show_running()
831 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
832 lines_to_configure
= []
835 print "\nLines To Delete"
836 print "==============="
838 for (ctx_keys
, line
) in lines_to_del
:
843 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
844 lines_to_configure
.append(cmd
)
848 print "\nLines To Add"
851 for (ctx_keys
, line
) in lines_to_add
:
856 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
857 lines_to_configure
.append(cmd
)
862 log
.debug('New Frr Config\n%s', newconf
.get_lines())
864 # This looks a little odd but we have to do this twice...here is why
865 # If the user had this running bgp config:
868 # neighbor 1.1.1.1 remote-as 50
869 # neighbor 1.1.1.1 route-map FOO out
871 # and this config in the newconf config file
874 # neighbor 1.1.1.1 remote-as 999
875 # neighbor 1.1.1.1 route-map FOO out
878 # Then the script will do
879 # - no neighbor 1.1.1.1 remote-as 50
880 # - neighbor 1.1.1.1 remote-as 999
882 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
883 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
884 # configs again to put this line back.
888 running
.load_from_show_running()
889 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
891 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
894 for (ctx_keys
, line
) in lines_to_del
:
899 # 'no' commands are tricky, we can't just put them in a file and
900 # vtysh -f that file. See the next comment for an explanation
902 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
905 # Some commands in frr are picky about taking a "no" of the entire line.
906 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
907 # only the beginning. If we hit one of these command an exception will be
908 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
911 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
912 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
914 # frr(config-if)# no ip ospf authentication message-digest
916 # frr(config-if)# no ip ospf authentication
921 _
= subprocess
.check_output(cmd
)
923 except subprocess
.CalledProcessError
:
925 # - Pull the last entry from cmd (this would be
926 # 'no ip ospf authentication message-digest 1.1.1.1' in
928 # - Split that last entry by whitespace and drop the last word
929 log
.warning('Failed to execute %s', ' '.join(cmd
))
930 last_arg
= cmd
[-1].split(' ')
932 if len(last_arg
) <= 2:
933 log
.error('"%s" we failed to remove this command', original_cmd
)
936 new_last_arg
= last_arg
[0:-1]
937 cmd
[-1] = ' '.join(new_last_arg
)
939 log
.info('Executed "%s"', ' '.join(cmd
))
943 lines_to_configure
= []
945 for (ctx_keys
, line
) in lines_to_add
:
950 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
951 lines_to_configure
.append(cmd
)
953 if lines_to_configure
:
954 random_string
= ''.join(random
.SystemRandom().choice(
955 string
.ascii_uppercase
+
956 string
.digits
) for _
in range(6))
958 filename
= "/var/run/frr/reload-%s.txt" % random_string
959 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
961 with
open(filename
, 'w') as fh
:
962 for line
in lines_to_configure
:
963 fh
.write(line
+ '\n')
964 subprocess
.call(['/usr/bin/vtysh', '-f', filename
])
967 # Make these changes persistent
968 if args
.overwrite
or args
.filename
!= '/etc/quagga/Quagga.conf':
969 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])