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 ",
362 for line
in self
.lines
:
367 if line
.startswith('!') or line
.startswith('#'):
371 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
372 self
.save_contexts(ctx_keys
, current_context_lines
)
374 # Start a new context
377 current_context_lines
= []
379 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
380 self
.save_contexts(ctx_keys
, current_context_lines
)
384 self
.save_contexts(ctx_keys
, current_context_lines
)
385 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
387 # Start a new context
391 current_context_lines
= []
393 elif line
== "exit-address-family" or line
== "exit":
394 # if this exit is for address-family ipv4 unicast, ignore the pop
396 self
.save_contexts(ctx_keys
, current_context_lines
)
398 # Start a new context
399 ctx_keys
= copy
.deepcopy(main_ctx_key
)
400 current_context_lines
= []
401 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
403 elif new_ctx
is True:
407 ctx_keys
= copy
.deepcopy(main_ctx_key
)
410 current_context_lines
= []
412 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
414 elif "address-family " in line
:
417 # Save old context first
418 self
.save_contexts(ctx_keys
, current_context_lines
)
419 current_context_lines
= []
420 main_ctx_key
= copy
.deepcopy(ctx_keys
)
421 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
423 if line
== "address-family ipv6":
424 ctx_keys
.append("address-family ipv6 unicast")
425 elif line
== "address-family ipv4":
426 ctx_keys
.append("address-family ipv4 unicast")
428 ctx_keys
.append(line
)
431 # Continuing in an existing context, add non-commented lines to it
432 current_context_lines
.append(line
)
433 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
435 # Save the context of the last one
436 self
.save_contexts(ctx_keys
, current_context_lines
)
439 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
441 Return the vtysh command for the specified context line
450 for ctx_key
in ctx_keys
:
459 if line
.startswith('no '):
460 cmd
.append('%s' % line
[3:])
462 cmd
.append('no %s' % line
)
468 # If line is None then we are typically deleting an entire
469 # context ('no router ospf' for example)
474 # Only put the 'no' on the last sub-context
475 for ctx_key
in ctx_keys
:
478 if ctx_key
== ctx_keys
[-1]:
479 cmd
.append('no %s' % ctx_key
)
481 cmd
.append('%s' % ctx_key
)
483 for ctx_key
in ctx_keys
:
490 def line_for_vtysh_file(ctx_keys
, line
, delete
):
492 Return the command as it would appear in frr.conf
497 for (i
, ctx_key
) in enumerate(ctx_keys
):
498 cmd
.append(' ' * i
+ ctx_key
)
501 indent
= len(ctx_keys
) * ' '
504 if line
.startswith('no '):
505 cmd
.append('%s%s' % (indent
, line
[3:]))
507 cmd
.append('%sno %s' % (indent
, line
))
510 cmd
.append(indent
+ line
)
512 # If line is None then we are typically deleting an entire
513 # context ('no router ospf' for example)
517 # Only put the 'no' on the last sub-context
518 for ctx_key
in ctx_keys
:
520 if ctx_key
== ctx_keys
[-1]:
521 cmd
.append('no %s' % ctx_key
)
523 cmd
.append('%s' % ctx_key
)
525 for ctx_key
in ctx_keys
:
528 return '\n' + '\n'.join(cmd
)
531 def get_normalized_ipv6_line(line
):
533 Return a normalized IPv6 line as produced by frr,
534 with all letters in lower case and trailing and leading
535 zeros removed, and only the network portion present if
536 the IPv6 word is a network
539 words
= line
.split(' ')
545 v6word
= IPNetwork(word
)
546 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
551 norm_word
= '%s' % IPv6Address(word
)
556 norm_line
= norm_line
+ " " + norm_word
558 return norm_line
.strip()
561 def line_exist(lines
, target_ctx_keys
, target_line
):
562 for (ctx_keys
, line
) in lines
:
563 if ctx_keys
== target_ctx_keys
and line
== target_line
:
568 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
570 # Quite possibly the most confusing (while accurate) variable names in history
571 lines_to_add_to_del
= []
572 lines_to_del_to_del
= []
574 for (ctx_keys
, line
) in lines_to_del
:
577 if ctx_keys
[0].startswith('router bgp') and line
and line
.startswith('neighbor '):
579 BGP changed how it displays swpX peers that are part of peer-group. Older
580 versions of frr would display these on separate lines:
581 neighbor swp1 interface
582 neighbor swp1 peer-group FOO
584 but today we display via a single line
585 neighbor swp1 interface peer-group FOO
587 This change confuses frr-reload.py so check to see if we are deleting
588 neighbor swp1 interface peer-group FOO
591 neighbor swp1 interface
592 neighbor swp1 peer-group FOO
594 If so then chop the del line and the corresponding add lines
597 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
598 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
600 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
601 swpx_interface
= None
602 swpx_peergroup
= None
604 if re_swpx_int_peergroup
:
605 swpx
= re_swpx_int_peergroup
.group(1)
606 peergroup
= re_swpx_int_peergroup
.group(2)
607 swpx_interface
= "neighbor %s interface" % swpx
608 elif re_swpx_int_v6only_peergroup
:
609 swpx
= re_swpx_int_v6only_peergroup
.group(1)
610 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
611 swpx_interface
= "neighbor %s interface v6only" % swpx
613 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
614 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
615 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
616 tmp_ctx_keys
= tuple(list(ctx_keys
))
618 if not found_add_swpx_peergroup
:
619 tmp_ctx_keys
= list(ctx_keys
)
620 tmp_ctx_keys
.append('address-family ipv4 unicast')
621 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
622 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
624 if not found_add_swpx_peergroup
:
625 tmp_ctx_keys
= list(ctx_keys
)
626 tmp_ctx_keys
.append('address-family ipv6 unicast')
627 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
628 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
630 if found_add_swpx_interface
and found_add_swpx_peergroup
:
632 lines_to_del_to_del
.append((ctx_keys
, line
))
633 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
634 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
637 In 3.0.1 we changed how we display neighbor interface command. Older
638 versions of frr would display the following:
639 neighbor swp1 interface
640 neighbor swp1 remote-as external
641 neighbor swp1 capability extended-nexthop
643 but today we display via a single line
644 neighbor swp1 interface remote-as external
646 and capability extended-nexthop is no longer needed because we
647 automatically enable it when the neighbor is of type interface.
649 This change confuses frr-reload.py so check to see if we are deleting
650 neighbor swp1 interface remote-as (external|internal|ASNUM)
653 neighbor swp1 interface
654 neighbor swp1 remote-as (external|internal|ASNUM)
655 neighbor swp1 capability extended-nexthop
657 If so then chop the del line and the corresponding add lines
659 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
660 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
662 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
663 swpx_interface
= None
666 if re_swpx_int_remoteas
:
667 swpx
= re_swpx_int_remoteas
.group(1)
668 remoteas
= re_swpx_int_remoteas
.group(2)
669 swpx_interface
= "neighbor %s interface" % swpx
670 elif re_swpx_int_v6only_remoteas
:
671 swpx
= re_swpx_int_v6only_remoteas
.group(1)
672 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
673 swpx_interface
= "neighbor %s interface v6only" % swpx
675 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
676 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
677 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
678 tmp_ctx_keys
= tuple(list(ctx_keys
))
680 if found_add_swpx_interface
and found_add_swpx_remoteas
:
682 lines_to_del_to_del
.append((ctx_keys
, line
))
683 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
684 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
687 In 3.0, we made bgp bestpath multipath as-relax command
688 automatically assume no-as-set since the lack of this option caused
689 weird routing problems and this problem was peculiar to this
690 implementation. When the running config is shown in relases after
691 3.0, the no-as-set is not shown as its the default. This causes
692 reload to unnecessarily unapply this option to only apply it back
693 again, causing unnecessary session resets. Handle this.
695 if ctx_keys
[0].startswith('router bgp') and line
and 'multipath-relax' in line
:
696 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
697 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
698 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
700 if re_asrelax_new
and found_asrelax_old
:
702 lines_to_del_to_del
.append((ctx_keys
, line
))
703 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
706 More old-to-new config handling. ip import-table no longer accepts
707 distance, but we honor the old syntax. But 'show running' shows only
708 the new syntax. This causes an unnecessary 'no import-table' followed
709 by the same old 'ip import-table' which causes perturbations in
710 announced routes leading to traffic blackholes. Fix this issue.
712 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
714 table_num
= re_importtbl
.group(1)
715 for ctx
in lines_to_add
:
716 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
717 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
718 lines_to_add_to_del
.append((ctx
[0], None))
721 ip/ipv6 prefix-list can be specified without a seq number. However,
722 the running config always adds 'seq x', where x is a number incremented
723 by 5 for every element, to the prefix list. So, ignore such lines as
724 well. Sample prefix-list lines:
725 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
726 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
727 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
729 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
732 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
733 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
734 re_ip_pfxlst
.group(6))
735 for ctx
in lines_to_add
:
736 if ctx
[0][0] == tmpline
:
737 lines_to_del_to_del
.append((ctx_keys
, None))
738 lines_to_add_to_del
.append(((tmpline
,), None))
741 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
744 lines_to_del_to_del
.append((ctx_keys
, line
))
745 lines_to_add_to_del
.append((ctx_keys
, line
))
748 We have commands that used to be displayed in the global part
749 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
753 neighbor ISL advertisement-interval 0
759 address-family ipv4 unicast
760 neighbor ISL advertisement-interval 0
762 Look to see if we are deleting it in one format just to add it back in the other
764 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
765 tmp_ctx_keys
= list(ctx_keys
)[:-1]
766 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
768 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
771 lines_to_del_to_del
.append((ctx_keys
, line
))
772 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
774 for (ctx_keys
, line
) in lines_to_del_to_del
:
775 lines_to_del
.remove((ctx_keys
, line
))
777 for (ctx_keys
, line
) in lines_to_add_to_del
:
778 lines_to_add
.remove((ctx_keys
, line
))
780 return (lines_to_add
, lines_to_del
)
783 def compare_context_objects(newconf
, running
):
785 Create a context diff for the two specified contexts
788 # Compare the two Config objects to find the lines that we need to add/del
793 # Find contexts that are in newconf but not in running
794 # Find contexts that are in running but not in newconf
795 for (running_ctx_keys
, running_ctx
) in running
.contexts
.iteritems():
797 if running_ctx_keys
not in newconf
.contexts
:
799 # We check that the len is 1 here so that we only look at ('router bgp 10')
800 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
801 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
802 # running but not in newconf.
803 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
805 lines_to_del
.append((running_ctx_keys
, None))
807 # We cannot do 'no interface' in quagga, and so deal with it
808 elif running_ctx_keys
[0].startswith('interface'):
809 for line
in running_ctx
.lines
:
810 lines_to_del
.append((running_ctx_keys
, line
))
812 # If this is an address-family under 'router bgp' and we are already deleting the
813 # entire 'router bgp' context then ignore this sub-context
814 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
818 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
819 lines_to_del
.append((running_ctx_keys
, None))
823 for line
in running_ctx
.lines
:
824 lines_to_del
.append((running_ctx_keys
, line
))
826 # Find the lines within each context to add
827 # Find the lines within each context to del
828 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
830 if newconf_ctx_keys
in running
.contexts
:
831 running_ctx
= running
.contexts
[newconf_ctx_keys
]
833 for line
in newconf_ctx
.lines
:
834 if line
not in running_ctx
.dlines
:
835 lines_to_add
.append((newconf_ctx_keys
, line
))
837 for line
in running_ctx
.lines
:
838 if line
not in newconf_ctx
.dlines
:
839 lines_to_del
.append((newconf_ctx_keys
, line
))
841 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
843 if newconf_ctx_keys
not in running
.contexts
:
844 lines_to_add
.append((newconf_ctx_keys
, None))
846 for line
in newconf_ctx
.lines
:
847 lines_to_add
.append((newconf_ctx_keys
, line
))
849 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
851 return (lines_to_add
, lines_to_del
)
853 if __name__
== '__main__':
854 # Command line options
855 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
856 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
857 group
= parser
.add_mutually_exclusive_group(required
=True)
858 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
859 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
860 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
861 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
862 parser
.add_argument('filename', help='Location of new frr config file')
863 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
864 args
= parser
.parse_args()
867 # For --test log to stdout
868 # For --reload log to /var/log/frr/frr-reload.log
869 if args
.test
or args
.stdout
:
870 logging
.basicConfig(level
=logging
.INFO
,
871 format
='%(asctime)s %(levelname)5s: %(message)s')
873 # Color the errors and warnings in red
874 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
875 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
878 if not os
.path
.isdir('/var/log/frr/'):
879 os
.makedirs('/var/log/frr/')
881 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
883 format
='%(asctime)s %(levelname)5s: %(message)s')
885 # argparse should prevent this from happening but just to be safe...
887 raise Exception('Must specify --reload or --test')
888 log
= logging
.getLogger(__name__
)
890 # Verify the new config file is valid
891 if not os
.path
.isfile(args
.filename
):
892 msg
= "Filename %s does not exist" % args
.filename
897 if not os
.path
.getsize(args
.filename
):
898 msg
= "Filename %s is an empty file" % args
.filename
903 # Verify that 'service integrated-vtysh-config' is configured
904 vtysh_filename
= '/etc/frr/vtysh.conf'
905 service_integrated_vtysh_config
= True
907 if os
.path
.isfile(vtysh_filename
):
908 with
open(vtysh_filename
, 'r') as fh
:
909 for line
in fh
.readlines():
912 if line
== 'no service integrated-vtysh-config':
913 service_integrated_vtysh_config
= False
916 if not service_integrated_vtysh_config
:
917 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
923 log
.setLevel(logging
.DEBUG
)
925 log
.info('Called via "%s"', str(args
))
927 # Create a Config object from the config generated by newconf
929 newconf
.load_from_file(args
.filename
)
934 # Create a Config object from the running config
938 running
.load_from_file(args
.input)
940 running
.load_from_show_running()
942 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
943 lines_to_configure
= []
946 print "\nLines To Delete"
947 print "==============="
949 for (ctx_keys
, line
) in lines_to_del
:
954 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
955 lines_to_configure
.append(cmd
)
959 print "\nLines To Add"
962 for (ctx_keys
, line
) in lines_to_add
:
967 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
968 lines_to_configure
.append(cmd
)
973 log
.debug('New Frr Config\n%s', newconf
.get_lines())
975 # This looks a little odd but we have to do this twice...here is why
976 # If the user had this running bgp config:
979 # neighbor 1.1.1.1 remote-as 50
980 # neighbor 1.1.1.1 route-map FOO out
982 # and this config in the newconf config file
985 # neighbor 1.1.1.1 remote-as 999
986 # neighbor 1.1.1.1 route-map FOO out
989 # Then the script will do
990 # - no neighbor 1.1.1.1 remote-as 50
991 # - neighbor 1.1.1.1 remote-as 999
993 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
994 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
995 # configs again to put this line back.
999 running
.load_from_show_running()
1000 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1002 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1005 for (ctx_keys
, line
) in lines_to_del
:
1010 # 'no' commands are tricky, we can't just put them in a file and
1011 # vtysh -f that file. See the next comment for an explanation
1013 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1016 # Some commands in frr are picky about taking a "no" of the entire line.
1017 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1018 # only the beginning. If we hit one of these command an exception will be
1019 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1022 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1023 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1024 # % Unknown command.
1025 # frr(config-if)# no ip ospf authentication message-digest
1026 # % Unknown command.
1027 # frr(config-if)# no ip ospf authentication
1032 _
= subprocess
.check_output(cmd
)
1034 except subprocess
.CalledProcessError
:
1036 # - Pull the last entry from cmd (this would be
1037 # 'no ip ospf authentication message-digest 1.1.1.1' in
1039 # - Split that last entry by whitespace and drop the last word
1040 log
.info('Failed to execute %s', ' '.join(cmd
))
1041 last_arg
= cmd
[-1].split(' ')
1043 if len(last_arg
) <= 2:
1044 log
.error('"%s" we failed to remove this command', original_cmd
)
1047 new_last_arg
= last_arg
[0:-1]
1048 cmd
[-1] = ' '.join(new_last_arg
)
1050 log
.info('Executed "%s"', ' '.join(cmd
))
1054 lines_to_configure
= []
1056 for (ctx_keys
, line
) in lines_to_add
:
1061 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1062 lines_to_configure
.append(cmd
)
1064 if lines_to_configure
:
1065 random_string
= ''.join(random
.SystemRandom().choice(
1066 string
.ascii_uppercase
+
1067 string
.digits
) for _
in range(6))
1069 filename
= "/var/run/frr/reload-%s.txt" % random_string
1070 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1072 with
open(filename
, 'w') as fh
:
1073 for line
in lines_to_configure
:
1074 fh
.write(line
+ '\n')
1076 output
= subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
])
1078 # exit non-zero if we see these errors
1079 for x
in ('BGP instance name and AS number mismatch',
1080 'BGP instance is already running',
1081 '% not a local address'):
1082 for line
in output
.splitlines():
1084 msg
= "ERROR: %s" % x
1091 # Make these changes persistent
1092 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1093 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])