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
, IPNetwork
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 IP addresses specified in "network" statements, "ip prefix-lists"
178 etc. can differ in the host part of the specification the user
179 provides and what the running config displays. For example, user
180 can specify 11.1.1.1/24, and the running config displays this as
181 11.1.1.0/24. Ensure we don't do a needless operation for such
182 lines. IS-IS & OSPFv3 have no "network" support.
184 re_key_rt
= re
.match(r
'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key
[0])
186 addr
= re_key_rt
.group(2)
189 newaddr
= IPNetwork(addr
)
190 key
[0] = '%s route %s/%s%s' % (re_key_rt
.group(1),
197 re_key_rt
= re
.match(
198 r
'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
202 addr
= re_key_rt
.group(4)
205 newaddr
= '%s/%s' % (IPNetwork(addr
).network
,
206 IPNetwork(addr
).prefixlen
)
212 legestr
= re_key_rt
.group(5)
213 re_lege
= re
.search(r
'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr
)
215 legestr
= '%sge %s le %s%s' % (re_lege
.group(1),
219 re_lege
= re
.search(r
'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr
)
221 if (re_lege
and ((re_key_rt
.group(1) == "ip" and
222 re_lege
.group(3) == "32") or
223 (re_key_rt
.group(1) == "ipv6" and
224 re_lege
.group(3) == "128"))):
225 legestr
= '%sge %s%s' % (re_lege
.group(1),
229 key
[0] = '%s prefix-list%s%s %s%s' % (re_key_rt
.group(1),
235 if lines
and key
[0].startswith('router bgp'):
238 re_net
= re
.match(r
'network\s+([A-Fa-f:.0-9/]+)(.*)$', line
)
240 addr
= re_net
.group(1)
241 if '/' not in addr
and key
[0].startswith('router bgp'):
242 # This is most likely an error because with no
243 # prefixlen, BGP treats the prefixlen as 8
247 newaddr
= IPNetwork(addr
)
248 line
= 'network %s/%s %s' % (newaddr
.network
,
251 newlines
.append(line
)
253 # Really this should be an error. Whats a network
254 # without an IP Address following it ?
255 newlines
.append(line
)
257 newlines
.append(line
)
261 More fixups in user specification and what running config shows.
262 "null0" in routes must be replaced by Null0, and "blackhole" must
263 be replaced by Null0 as well.
265 if (key
[0].startswith('ip route') or key
[0].startswith('ipv6 route') and
266 'null0' in key
[0] or 'blackhole' in key
[0]):
267 key
[0] = re
.sub(r
'\s+null0(\s*$)', ' Null0', key
[0])
268 key
[0] = re
.sub(r
'\s+blackhole(\s*$)', ' Null0', key
[0])
271 if tuple(key
) not in self
.contexts
:
272 ctx
= Context(tuple(key
), lines
)
273 self
.contexts
[tuple(key
)] = ctx
275 ctx
= self
.contexts
[tuple(key
)]
279 if tuple(key
) not in self
.contexts
:
280 ctx
= Context(tuple(key
), [])
281 self
.contexts
[tuple(key
)] = ctx
283 def load_contexts(self
):
285 Parse the configuration and create contexts for each appropriate block
288 current_context_lines
= []
292 The end of a context is flagged via the 'end' keyword:
301 bgp router-id 10.0.0.1
302 bgp log-neighbor-changes
303 no bgp default ipv4-unicast
304 neighbor EBGP peer-group
305 neighbor EBGP advertisement-interval 1
306 neighbor EBGP timers connect 10
307 neighbor 2001:40:1:4::6 remote-as 40
308 neighbor 2001:40:1:8::a remote-as 40
312 neighbor IBGPv6 activate
313 neighbor 2001:10::2 peer-group IBGPv6
314 neighbor 2001:10::3 peer-group IBGPv6
319 ospf router-id 10.0.0.1
320 log-adjacency-changes detail
321 timers throttle spf 0 50 5000
326 # The code assumes that its working on the output from the "vtysh -m"
327 # command. That provides the appropriate markers to signify end of
328 # a context. This routine uses that to build the contexts for the
331 # There are single line contexts such as "log file /media/node/zebra.log"
332 # and multi-line contexts such as "router ospf" and subcontexts
333 # within a context such as "address-family" within "router bgp"
334 # In each of these cases, the first line of the context becomes the
335 # key of the context. So "router bgp 10" is the key for the non-address
336 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
337 # the key for the subcontext and so on.
342 # the keywords that we know are single line contexts. bgp in this case
343 # is not the main router bgp block, but enabling multi-instance
344 oneline_ctx_keywords
= ("access-list ",
361 for line
in self
.lines
:
366 if line
.startswith('!') or line
.startswith('#'):
370 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
371 self
.save_contexts(ctx_keys
, current_context_lines
)
373 # Start a new context
376 current_context_lines
= []
378 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
379 self
.save_contexts(ctx_keys
, current_context_lines
)
383 self
.save_contexts(ctx_keys
, current_context_lines
)
384 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
386 # Start a new context
390 current_context_lines
= []
392 elif line
== "exit-address-family" or line
== "exit":
393 # if this exit is for address-family ipv4 unicast, ignore the pop
395 self
.save_contexts(ctx_keys
, current_context_lines
)
397 # Start a new context
398 ctx_keys
= copy
.deepcopy(main_ctx_key
)
399 current_context_lines
= []
400 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
402 elif new_ctx
is True:
406 ctx_keys
= copy
.deepcopy(main_ctx_key
)
409 current_context_lines
= []
411 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
413 elif "address-family " in line
:
416 # Save old context first
417 self
.save_contexts(ctx_keys
, current_context_lines
)
418 current_context_lines
= []
419 main_ctx_key
= copy
.deepcopy(ctx_keys
)
420 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
422 if line
== "address-family ipv6":
423 ctx_keys
.append("address-family ipv6 unicast")
424 elif line
== "address-family ipv4":
425 ctx_keys
.append("address-family ipv4 unicast")
427 ctx_keys
.append(line
)
430 # Continuing in an existing context, add non-commented lines to it
431 current_context_lines
.append(line
)
432 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
434 # Save the context of the last one
435 self
.save_contexts(ctx_keys
, current_context_lines
)
438 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
440 Return the vtysh command for the specified context line
449 for ctx_key
in ctx_keys
:
458 if line
.startswith('no '):
459 cmd
.append('%s' % line
[3:])
461 cmd
.append('no %s' % line
)
467 # If line is None then we are typically deleting an entire
468 # context ('no router ospf' for example)
473 # Only put the 'no' on the last sub-context
474 for ctx_key
in ctx_keys
:
477 if ctx_key
== ctx_keys
[-1]:
478 cmd
.append('no %s' % ctx_key
)
480 cmd
.append('%s' % ctx_key
)
482 for ctx_key
in ctx_keys
:
489 def line_for_vtysh_file(ctx_keys
, line
, delete
):
491 Return the command as it would appear in Frr.conf
496 for (i
, ctx_key
) in enumerate(ctx_keys
):
497 cmd
.append(' ' * i
+ ctx_key
)
500 indent
= len(ctx_keys
) * ' '
503 if line
.startswith('no '):
504 cmd
.append('%s%s' % (indent
, line
[3:]))
506 cmd
.append('%sno %s' % (indent
, line
))
509 cmd
.append(indent
+ line
)
511 # If line is None then we are typically deleting an entire
512 # context ('no router ospf' for example)
516 # Only put the 'no' on the last sub-context
517 for ctx_key
in ctx_keys
:
519 if ctx_key
== ctx_keys
[-1]:
520 cmd
.append('no %s' % ctx_key
)
522 cmd
.append('%s' % ctx_key
)
524 for ctx_key
in ctx_keys
:
527 return '\n' + '\n'.join(cmd
)
530 def get_normalized_ipv6_line(line
):
532 Return a normalized IPv6 line as produced by frr,
533 with all letters in lower case and trailing and leading
534 zeros removed, and only the network portion present if
535 the IPv6 word is a network
538 words
= line
.split(' ')
544 v6word
= IPNetwork(word
)
545 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
550 norm_word
= '%s' % IPv6Address(word
)
555 norm_line
= norm_line
+ " " + norm_word
557 return norm_line
.strip()
560 def line_exist(lines
, target_ctx_keys
, target_line
):
561 for (ctx_keys
, line
) in lines
:
562 if ctx_keys
== target_ctx_keys
and line
== target_line
:
567 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
569 # Quite possibly the most confusing (while accurate) variable names in history
570 lines_to_add_to_del
= []
571 lines_to_del_to_del
= []
573 for (ctx_keys
, line
) in lines_to_del
:
576 if ctx_keys
[0].startswith('router bgp') and line
and line
.startswith('neighbor '):
578 BGP changed how it displays swpX peers that are part of peer-group. Older
579 versions of frr would display these on separate lines:
580 neighbor swp1 interface
581 neighbor swp1 peer-group FOO
583 but today we display via a single line
584 neighbor swp1 interface peer-group FOO
586 This change confuses frr-reload.py so check to see if we are deleting
587 neighbor swp1 interface peer-group FOO
590 neighbor swp1 interface
591 neighbor swp1 peer-group FOO
593 If so then chop the del line and the corresponding add lines
596 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
597 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
599 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
600 swpx_interface
= None
601 swpx_peergroup
= None
603 if re_swpx_int_peergroup
:
604 swpx
= re_swpx_int_peergroup
.group(1)
605 peergroup
= re_swpx_int_peergroup
.group(2)
606 swpx_interface
= "neighbor %s interface" % swpx
607 elif re_swpx_int_v6only_peergroup
:
608 swpx
= re_swpx_int_v6only_peergroup
.group(1)
609 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
610 swpx_interface
= "neighbor %s interface v6only" % swpx
612 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
613 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
614 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
615 tmp_ctx_keys
= tuple(list(ctx_keys
))
617 if not found_add_swpx_peergroup
:
618 tmp_ctx_keys
= list(ctx_keys
)
619 tmp_ctx_keys
.append('address-family ipv4 unicast')
620 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
621 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
623 if not found_add_swpx_peergroup
:
624 tmp_ctx_keys
= list(ctx_keys
)
625 tmp_ctx_keys
.append('address-family ipv6 unicast')
626 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
627 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
629 if found_add_swpx_interface
and found_add_swpx_peergroup
:
631 lines_to_del_to_del
.append((ctx_keys
, line
))
632 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
633 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
636 In 3.0.1 we changed how we display neighbor interface command. Older
637 versions of frr would display the following:
638 neighbor swp1 interface
639 neighbor swp1 remote-as external
640 neighbor swp1 capability extended-nexthop
642 but today we display via a single line
643 neighbor swp1 interface remote-as external
645 and capability extended-nexthop is no longer needed because we
646 automatically enable it when the neighbor is of type interface.
648 This change confuses frr-reload.py so check to see if we are deleting
649 neighbor swp1 interface remote-as (external|internal|ASNUM)
652 neighbor swp1 interface
653 neighbor swp1 remote-as (external|internal|ASNUM)
654 neighbor swp1 capability extended-nexthop
656 If so then chop the del line and the corresponding add lines
658 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
659 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
661 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
662 swpx_interface
= None
665 if re_swpx_int_remoteas
:
666 swpx
= re_swpx_int_remoteas
.group(1)
667 remoteas
= re_swpx_int_remoteas
.group(2)
668 swpx_interface
= "neighbor %s interface" % swpx
669 elif re_swpx_int_v6only_remoteas
:
670 swpx
= re_swpx_int_v6only_remoteas
.group(1)
671 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
672 swpx_interface
= "neighbor %s interface v6only" % swpx
674 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
675 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
676 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
677 tmp_ctx_keys
= tuple(list(ctx_keys
))
679 if found_add_swpx_interface
and found_add_swpx_remoteas
:
681 lines_to_del_to_del
.append((ctx_keys
, line
))
682 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
683 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
686 In 3.0, we made bgp bestpath multipath as-relax command
687 automatically assume no-as-set since the lack of this option caused
688 weird routing problems and this problem was peculiar to this
689 implementation. When the running config is shown in relases after
690 3.0, the no-as-set is not shown as its the default. This causes
691 reload to unnecessarily unapply this option to only apply it back
692 again, causing unnecessary session resets. Handle this.
694 if ctx_keys
[0].startswith('router bgp') and line
and 'multipath-relax' in line
:
695 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
696 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
697 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
699 if re_asrelax_new
and found_asrelax_old
:
701 lines_to_del_to_del
.append((ctx_keys
, line
))
702 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
705 More old-to-new config handling. ip import-table no longer accepts
706 distance, but we honor the old syntax. But 'show running' shows only
707 the new syntax. This causes an unnecessary 'no import-table' followed
708 by the same old 'ip import-table' which causes perturbations in
709 announced routes leading to traffic blackholes. Fix this issue.
711 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
713 table_num
= re_importtbl
.group(1)
714 for ctx
in lines_to_add
:
715 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
716 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
717 lines_to_add_to_del
.append((ctx
[0], None))
720 ip/ipv6 prefix-list can be specified without a seq number. However,
721 the running config always adds 'seq x', where x is a number incremented
722 by 5 for every element, to the prefix list. So, ignore such lines as
723 well. Sample prefix-list lines:
724 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
725 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
726 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
728 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
731 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
732 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
733 re_ip_pfxlst
.group(6))
734 for ctx
in lines_to_add
:
735 if ctx
[0][0] == tmpline
:
736 lines_to_del_to_del
.append((ctx_keys
, None))
737 lines_to_add_to_del
.append(((tmpline
,), None))
740 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
743 lines_to_del_to_del
.append((ctx_keys
, line
))
744 lines_to_add_to_del
.append((ctx_keys
, line
))
747 We have commands that used to be displayed in the global part
748 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
752 neighbor ISL advertisement-interval 0
758 address-family ipv4 unicast
759 neighbor ISL advertisement-interval 0
761 Look to see if we are deleting it in one format just to add it back in the other
763 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
764 tmp_ctx_keys
= list(ctx_keys
)[:-1]
765 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
767 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
770 lines_to_del_to_del
.append((ctx_keys
, line
))
771 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
773 for (ctx_keys
, line
) in lines_to_del_to_del
:
774 lines_to_del
.remove((ctx_keys
, line
))
776 for (ctx_keys
, line
) in lines_to_add_to_del
:
777 lines_to_add
.remove((ctx_keys
, line
))
779 return (lines_to_add
, lines_to_del
)
782 def compare_context_objects(newconf
, running
):
784 Create a context diff for the two specified contexts
787 # Compare the two Config objects to find the lines that we need to add/del
792 # Find contexts that are in newconf but not in running
793 # Find contexts that are in running but not in newconf
794 for (running_ctx_keys
, running_ctx
) in running
.contexts
.iteritems():
796 if running_ctx_keys
not in newconf
.contexts
:
798 # We check that the len is 1 here so that we only look at ('router bgp 10')
799 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
800 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
801 # running but not in newconf.
802 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
804 lines_to_del
.append((running_ctx_keys
, None))
806 # We cannot do 'no interface' in quagga, and so deal with it
807 elif running_ctx_keys
[0].startswith('interface'):
808 for line
in running_ctx
.lines
:
809 lines_to_del
.append((running_ctx_keys
, line
))
811 # If this is an address-family under 'router bgp' and we are already deleting the
812 # entire 'router bgp' context then ignore this sub-context
813 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
817 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
818 lines_to_del
.append((running_ctx_keys
, None))
822 for line
in running_ctx
.lines
:
823 lines_to_del
.append((running_ctx_keys
, line
))
825 # Find the lines within each context to add
826 # Find the lines within each context to del
827 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
829 if newconf_ctx_keys
in running
.contexts
:
830 running_ctx
= running
.contexts
[newconf_ctx_keys
]
832 for line
in newconf_ctx
.lines
:
833 if line
not in running_ctx
.dlines
:
834 lines_to_add
.append((newconf_ctx_keys
, line
))
836 for line
in running_ctx
.lines
:
837 if line
not in newconf_ctx
.dlines
:
838 lines_to_del
.append((newconf_ctx_keys
, line
))
840 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
842 if newconf_ctx_keys
not in running
.contexts
:
843 lines_to_add
.append((newconf_ctx_keys
, None))
845 for line
in newconf_ctx
.lines
:
846 lines_to_add
.append((newconf_ctx_keys
, line
))
848 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
850 return (lines_to_add
, lines_to_del
)
852 if __name__
== '__main__':
853 # Command line options
854 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
855 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
856 group
= parser
.add_mutually_exclusive_group(required
=True)
857 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
858 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
859 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
860 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
861 parser
.add_argument('filename', help='Location of new frr config file')
862 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite Quagga.conf with running config output', default
=False)
863 args
= parser
.parse_args()
866 # For --test log to stdout
867 # For --reload log to /var/log/frr/frr-reload.log
868 if args
.test
or args
.stdout
:
869 logging
.basicConfig(level
=logging
.INFO
,
870 format
='%(asctime)s %(levelname)5s: %(message)s')
872 # Color the errors and warnings in red
873 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
874 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
877 if not os
.path
.isdir('/var/log/frr/'):
878 os
.makedirs('/var/log/frr/')
880 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
882 format
='%(asctime)s %(levelname)5s: %(message)s')
884 # argparse should prevent this from happening but just to be safe...
886 raise Exception('Must specify --reload or --test')
887 log
= logging
.getLogger(__name__
)
889 # Verify the new config file is valid
890 if not os
.path
.isfile(args
.filename
):
891 print "Filename %s does not exist" % args
.filename
894 if not os
.path
.getsize(args
.filename
):
895 print "Filename %s is an empty file" % args
.filename
898 # Verify that 'service integrated-vtysh-config' is configured
899 vtysh_filename
= '/etc/frr/vtysh.conf'
900 service_integrated_vtysh_config
= True
902 if os
.path
.isfile(vtysh_filename
):
903 with
open(vtysh_filename
, 'r') as fh
:
904 for line
in fh
.readlines():
907 if line
== 'no service integrated-vtysh-config':
908 service_integrated_vtysh_config
= False
911 if not service_integrated_vtysh_config
:
912 print "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
916 log
.setLevel(logging
.DEBUG
)
918 log
.info('Called via "%s"', str(args
))
920 # Create a Config object from the config generated by newconf
922 newconf
.load_from_file(args
.filename
)
926 # Create a Config object from the running config
930 running
.load_from_file(args
.input)
932 running
.load_from_show_running()
934 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
935 lines_to_configure
= []
938 print "\nLines To Delete"
939 print "==============="
941 for (ctx_keys
, line
) in lines_to_del
:
946 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
947 lines_to_configure
.append(cmd
)
951 print "\nLines To Add"
954 for (ctx_keys
, line
) in lines_to_add
:
959 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
960 lines_to_configure
.append(cmd
)
965 log
.debug('New Frr Config\n%s', newconf
.get_lines())
967 # This looks a little odd but we have to do this twice...here is why
968 # If the user had this running bgp config:
971 # neighbor 1.1.1.1 remote-as 50
972 # neighbor 1.1.1.1 route-map FOO out
974 # and this config in the newconf config file
977 # neighbor 1.1.1.1 remote-as 999
978 # neighbor 1.1.1.1 route-map FOO out
981 # Then the script will do
982 # - no neighbor 1.1.1.1 remote-as 50
983 # - neighbor 1.1.1.1 remote-as 999
985 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
986 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
987 # configs again to put this line back.
991 running
.load_from_show_running()
992 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
994 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
997 for (ctx_keys
, line
) in lines_to_del
:
1002 # 'no' commands are tricky, we can't just put them in a file and
1003 # vtysh -f that file. See the next comment for an explanation
1005 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1008 # Some commands in frr are picky about taking a "no" of the entire line.
1009 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1010 # only the beginning. If we hit one of these command an exception will be
1011 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1014 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1015 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1016 # % Unknown command.
1017 # frr(config-if)# no ip ospf authentication message-digest
1018 # % Unknown command.
1019 # frr(config-if)# no ip ospf authentication
1024 _
= subprocess
.check_output(cmd
)
1026 except subprocess
.CalledProcessError
:
1028 # - Pull the last entry from cmd (this would be
1029 # 'no ip ospf authentication message-digest 1.1.1.1' in
1031 # - Split that last entry by whitespace and drop the last word
1032 log
.warning('Failed to execute %s', ' '.join(cmd
))
1033 last_arg
= cmd
[-1].split(' ')
1035 if len(last_arg
) <= 2:
1036 log
.error('"%s" we failed to remove this command', original_cmd
)
1039 new_last_arg
= last_arg
[0:-1]
1040 cmd
[-1] = ' '.join(new_last_arg
)
1042 log
.info('Executed "%s"', ' '.join(cmd
))
1046 lines_to_configure
= []
1048 for (ctx_keys
, line
) in lines_to_add
:
1053 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1054 lines_to_configure
.append(cmd
)
1056 if lines_to_configure
:
1057 random_string
= ''.join(random
.SystemRandom().choice(
1058 string
.ascii_uppercase
+
1059 string
.digits
) for _
in range(6))
1061 filename
= "/var/run/frr/reload-%s.txt" % random_string
1062 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1064 with
open(filename
, 'w') as fh
:
1065 for line
in lines_to_configure
:
1066 fh
.write(line
+ '\n')
1067 subprocess
.call(['/usr/bin/vtysh', '-f', filename
])
1070 # Make these changes persistent
1071 if args
.overwrite
or args
.filename
!= '/etc/quagga/Quagga.conf':
1072 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])