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 stderr
=subprocess
.STDOUT
)
114 except subprocess
.CalledProcessError
as e
:
115 ve
= VtyshMarkException(e
)
119 for line
in file_output
.split('\n'):
122 # Compress duplicate whitespaces
123 line
= ' '.join(line
.split())
126 qv6_line
= get_normalized_ipv6_line(line
)
127 self
.lines
.append(qv6_line
)
129 self
.lines
.append(line
)
133 def load_from_show_running(self
):
135 Read running configuration and slurp it into internal memory
136 The internal representation has been marked appropriately by passing it
137 through vtysh with the -m parameter
139 log
.info('Loading Config object from vtysh show running')
142 config_text
= subprocess
.check_output(
143 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
144 shell
=True, stderr
=subprocess
.STDOUT
)
145 except subprocess
.CalledProcessError
as e
:
146 ve
= VtyshMarkException(e
)
150 for line
in config_text
.split('\n'):
153 if (line
== 'Building configuration...' or
154 line
== 'Current configuration:' or
158 self
.lines
.append(line
)
164 Return the lines read in from the configuration
167 return '\n'.join(self
.lines
)
169 def get_contexts(self
):
171 Return the parsed context as strings for display, log etc.
174 for (_
, ctx
) in sorted(self
.contexts
.iteritems()):
175 print str(ctx
) + '\n'
177 def save_contexts(self
, key
, lines
):
179 Save the provided key and lines as a context
186 IP addresses specified in "network" statements, "ip prefix-lists"
187 etc. can differ in the host part of the specification the user
188 provides and what the running config displays. For example, user
189 can specify 11.1.1.1/24, and the running config displays this as
190 11.1.1.0/24. Ensure we don't do a needless operation for such
191 lines. IS-IS & OSPFv3 have no "network" support.
193 re_key_rt
= re
.match(r
'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key
[0])
195 addr
= re_key_rt
.group(2)
198 newaddr
= IPNetwork(addr
)
199 key
[0] = '%s route %s/%s%s' % (re_key_rt
.group(1),
206 re_key_rt
= re
.match(
207 r
'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
211 addr
= re_key_rt
.group(4)
214 newaddr
= '%s/%s' % (IPNetwork(addr
).network
,
215 IPNetwork(addr
).prefixlen
)
221 legestr
= re_key_rt
.group(5)
222 re_lege
= re
.search(r
'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr
)
224 legestr
= '%sge %s le %s%s' % (re_lege
.group(1),
228 re_lege
= re
.search(r
'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr
)
230 if (re_lege
and ((re_key_rt
.group(1) == "ip" and
231 re_lege
.group(3) == "32") or
232 (re_key_rt
.group(1) == "ipv6" and
233 re_lege
.group(3) == "128"))):
234 legestr
= '%sge %s%s' % (re_lege
.group(1),
238 key
[0] = '%s prefix-list%s%s %s%s' % (re_key_rt
.group(1),
244 if lines
and key
[0].startswith('router bgp'):
247 re_net
= re
.match(r
'network\s+([A-Fa-f:.0-9/]+)(.*)$', line
)
249 addr
= re_net
.group(1)
250 if '/' not in addr
and key
[0].startswith('router bgp'):
251 # This is most likely an error because with no
252 # prefixlen, BGP treats the prefixlen as 8
256 newaddr
= IPNetwork(addr
)
257 line
= 'network %s/%s %s' % (newaddr
.network
,
260 newlines
.append(line
)
262 # Really this should be an error. Whats a network
263 # without an IP Address following it ?
264 newlines
.append(line
)
266 newlines
.append(line
)
270 More fixups in user specification and what running config shows.
271 "null0" in routes must be replaced by Null0, and "blackhole" must
272 be replaced by Null0 as well.
274 if (key
[0].startswith('ip route') or key
[0].startswith('ipv6 route') and
275 'null0' in key
[0] or 'blackhole' in key
[0]):
276 key
[0] = re
.sub(r
'\s+null0(\s*$)', ' Null0', key
[0])
277 key
[0] = re
.sub(r
'\s+blackhole(\s*$)', ' Null0', key
[0])
280 if tuple(key
) not in self
.contexts
:
281 ctx
= Context(tuple(key
), lines
)
282 self
.contexts
[tuple(key
)] = ctx
284 ctx
= self
.contexts
[tuple(key
)]
288 if tuple(key
) not in self
.contexts
:
289 ctx
= Context(tuple(key
), [])
290 self
.contexts
[tuple(key
)] = ctx
292 def load_contexts(self
):
294 Parse the configuration and create contexts for each appropriate block
297 current_context_lines
= []
301 The end of a context is flagged via the 'end' keyword:
310 bgp router-id 10.0.0.1
311 bgp log-neighbor-changes
312 no bgp default ipv4-unicast
313 neighbor EBGP peer-group
314 neighbor EBGP advertisement-interval 1
315 neighbor EBGP timers connect 10
316 neighbor 2001:40:1:4::6 remote-as 40
317 neighbor 2001:40:1:8::a remote-as 40
321 neighbor IBGPv6 activate
322 neighbor 2001:10::2 peer-group IBGPv6
323 neighbor 2001:10::3 peer-group IBGPv6
328 neighbor LEAF activate
332 route-target import 10.1.1.1:10100
333 route-target export 10.1.1.1:10100
339 ospf router-id 10.0.0.1
340 log-adjacency-changes detail
341 timers throttle spf 0 50 5000
346 # The code assumes that its working on the output from the "vtysh -m"
347 # command. That provides the appropriate markers to signify end of
348 # a context. This routine uses that to build the contexts for the
351 # There are single line contexts such as "log file /media/node/zebra.log"
352 # and multi-line contexts such as "router ospf" and subcontexts
353 # within a context such as "address-family" within "router bgp"
354 # In each of these cases, the first line of the context becomes the
355 # key of the context. So "router bgp 10" is the key for the non-address
356 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
357 # the key for the subcontext and so on.
362 # the keywords that we know are single line contexts. bgp in this case
363 # is not the main router bgp block, but enabling multi-instance
364 oneline_ctx_keywords
= ("access-list ",
385 for line
in self
.lines
:
390 if line
.startswith('!') or line
.startswith('#'):
394 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
395 self
.save_contexts(ctx_keys
, current_context_lines
)
397 # Start a new context
400 current_context_lines
= []
402 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
403 self
.save_contexts(ctx_keys
, current_context_lines
)
406 elif line
in ["end", "exit-vrf"]:
407 self
.save_contexts(ctx_keys
, current_context_lines
)
408 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
410 # Start a new context
414 current_context_lines
= []
416 elif line
in ["exit-address-family", "exit", "exit-vnc", "exit-vni"]:
417 # if this exit is for address-family ipv4 unicast, ignore the pop
419 self
.save_contexts(ctx_keys
, current_context_lines
)
421 # Start a new context
422 ctx_keys
= copy
.deepcopy(main_ctx_key
)
423 current_context_lines
= []
424 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
426 elif new_ctx
is True:
430 ctx_keys
= copy
.deepcopy(main_ctx_key
)
433 current_context_lines
= []
435 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
436 elif (line
.startswith("address-family ") or
437 line
.startswith("vnc defaults") or
438 line
.startswith("vnc l2-group") or
439 line
.startswith("vnc nve-group") or
440 (line
.startswith("vni ") and
441 len(ctx_keys
) == 2 and
442 ctx_keys
[0].startswith('router bgp') and
443 ctx_keys
[1] == 'address-family l2vpn evpn')):
446 # Save old context first
447 self
.save_contexts(ctx_keys
, current_context_lines
)
448 current_context_lines
= []
449 main_ctx_key
= copy
.deepcopy(ctx_keys
)
450 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
452 if line
== "address-family ipv6":
453 ctx_keys
.append("address-family ipv6 unicast")
454 elif line
== "address-family ipv4":
455 ctx_keys
.append("address-family ipv4 unicast")
456 elif line
== "address-family evpn":
457 ctx_keys
.append("address-family l2vpn evpn")
459 ctx_keys
.append(line
)
462 # Continuing in an existing context, add non-commented lines to it
463 current_context_lines
.append(line
)
464 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
466 # Save the context of the last one
467 self
.save_contexts(ctx_keys
, current_context_lines
)
470 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
472 Return the vtysh command for the specified context line
481 for ctx_key
in ctx_keys
:
490 if line
.startswith('no '):
491 cmd
.append('%s' % line
[3:])
493 cmd
.append('no %s' % line
)
499 # If line is None then we are typically deleting an entire
500 # context ('no router ospf' for example)
505 # Only put the 'no' on the last sub-context
506 for ctx_key
in ctx_keys
:
509 if ctx_key
== ctx_keys
[-1]:
510 cmd
.append('no %s' % ctx_key
)
512 cmd
.append('%s' % ctx_key
)
514 for ctx_key
in ctx_keys
:
521 def line_for_vtysh_file(ctx_keys
, line
, delete
):
523 Return the command as it would appear in frr.conf
528 for (i
, ctx_key
) in enumerate(ctx_keys
):
529 cmd
.append(' ' * i
+ ctx_key
)
532 indent
= len(ctx_keys
) * ' '
535 if line
.startswith('no '):
536 cmd
.append('%s%s' % (indent
, line
[3:]))
538 cmd
.append('%sno %s' % (indent
, line
))
541 cmd
.append(indent
+ line
)
543 # If line is None then we are typically deleting an entire
544 # context ('no router ospf' for example)
548 # Only put the 'no' on the last sub-context
549 for ctx_key
in ctx_keys
:
551 if ctx_key
== ctx_keys
[-1]:
552 cmd
.append('no %s' % ctx_key
)
554 cmd
.append('%s' % ctx_key
)
556 for ctx_key
in ctx_keys
:
559 cmd
= '\n' + '\n'.join(cmd
)
561 # There are some commands that are on by default so their "no" form will be
562 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
563 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
564 # not by doing a "no no bgp default ipv4-unicast"
565 cmd
= cmd
.replace('no no ', '')
570 def get_normalized_ipv6_line(line
):
572 Return a normalized IPv6 line as produced by frr,
573 with all letters in lower case and trailing and leading
574 zeros removed, and only the network portion present if
575 the IPv6 word is a network
578 words
= line
.split(' ')
584 v6word
= IPNetwork(word
)
585 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
590 norm_word
= '%s' % IPv6Address(word
)
595 norm_line
= norm_line
+ " " + norm_word
597 return norm_line
.strip()
600 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
601 for (ctx_keys
, line
) in lines
:
602 if ctx_keys
== target_ctx_keys
:
604 if line
== target_line
:
607 if line
.startswith(target_line
):
612 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
614 # Quite possibly the most confusing (while accurate) variable names in history
615 lines_to_add_to_del
= []
616 lines_to_del_to_del
= []
618 for (ctx_keys
, line
) in lines_to_del
:
621 if ctx_keys
[0].startswith('router bgp') and line
:
623 if line
.startswith('neighbor '):
625 BGP changed how it displays swpX peers that are part of peer-group. Older
626 versions of frr would display these on separate lines:
627 neighbor swp1 interface
628 neighbor swp1 peer-group FOO
630 but today we display via a single line
631 neighbor swp1 interface peer-group FOO
633 This change confuses frr-reload.py so check to see if we are deleting
634 neighbor swp1 interface peer-group FOO
637 neighbor swp1 interface
638 neighbor swp1 peer-group FOO
640 If so then chop the del line and the corresponding add lines
643 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
644 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
646 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
647 swpx_interface
= None
648 swpx_peergroup
= None
650 if re_swpx_int_peergroup
:
651 swpx
= re_swpx_int_peergroup
.group(1)
652 peergroup
= re_swpx_int_peergroup
.group(2)
653 swpx_interface
= "neighbor %s interface" % swpx
654 elif re_swpx_int_v6only_peergroup
:
655 swpx
= re_swpx_int_v6only_peergroup
.group(1)
656 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
657 swpx_interface
= "neighbor %s interface v6only" % swpx
659 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
660 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
661 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
662 tmp_ctx_keys
= tuple(list(ctx_keys
))
664 if not found_add_swpx_peergroup
:
665 tmp_ctx_keys
= list(ctx_keys
)
666 tmp_ctx_keys
.append('address-family ipv4 unicast')
667 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
668 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
670 if not found_add_swpx_peergroup
:
671 tmp_ctx_keys
= list(ctx_keys
)
672 tmp_ctx_keys
.append('address-family ipv6 unicast')
673 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
674 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
676 if found_add_swpx_interface
and found_add_swpx_peergroup
:
678 lines_to_del_to_del
.append((ctx_keys
, line
))
679 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
680 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
683 We changed how we display the neighbor interface command. Older
684 versions of frr would display the following:
685 neighbor swp1 interface
686 neighbor swp1 remote-as external
687 neighbor swp1 capability extended-nexthop
689 but today we display via a single line
690 neighbor swp1 interface remote-as external
692 and capability extended-nexthop is no longer needed because we
693 automatically enable it when the neighbor is of type interface.
695 This change confuses frr-reload.py so check to see if we are deleting
696 neighbor swp1 interface remote-as (external|internal|ASNUM)
699 neighbor swp1 interface
700 neighbor swp1 remote-as (external|internal|ASNUM)
701 neighbor swp1 capability extended-nexthop
703 If so then chop the del line and the corresponding add lines
705 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
706 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
708 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
709 swpx_interface
= None
712 if re_swpx_int_remoteas
:
713 swpx
= re_swpx_int_remoteas
.group(1)
714 remoteas
= re_swpx_int_remoteas
.group(2)
715 swpx_interface
= "neighbor %s interface" % swpx
716 elif re_swpx_int_v6only_remoteas
:
717 swpx
= re_swpx_int_v6only_remoteas
.group(1)
718 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
719 swpx_interface
= "neighbor %s interface v6only" % swpx
721 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
722 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
723 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
724 tmp_ctx_keys
= tuple(list(ctx_keys
))
726 if found_add_swpx_interface
and found_add_swpx_remoteas
:
728 lines_to_del_to_del
.append((ctx_keys
, line
))
729 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
730 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
733 We made the 'bgp bestpath as-path multipath-relax' command
734 automatically assume 'no-as-set' since the lack of this option caused
735 weird routing problems. When the running config is shown in
736 releases with this change, the no-as-set keyword is not shown as it
737 is the default. This causes frr-reload to unnecessarily unapply
738 this option only to apply it back again, causing unnecessary session
741 if 'multipath-relax' in line
:
742 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
743 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
744 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
746 if re_asrelax_new
and found_asrelax_old
:
748 lines_to_del_to_del
.append((ctx_keys
, line
))
749 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
752 If we are modifying the BGP table-map we need to avoid a del/add and
753 instead modify the table-map in place via an add. This is needed to
754 avoid installing all routes in the RIB the second the 'no table-map'
757 if line
.startswith('table-map'):
758 found_table_map
= line_exist(lines_to_add
, ctx_keys
, 'table-map', False)
761 lines_to_del_to_del
.append((ctx_keys
, line
))
764 More old-to-new config handling. ip import-table no longer accepts
765 distance, but we honor the old syntax. But 'show running' shows only
766 the new syntax. This causes an unnecessary 'no import-table' followed
767 by the same old 'ip import-table' which causes perturbations in
768 announced routes leading to traffic blackholes. Fix this issue.
770 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
772 table_num
= re_importtbl
.group(1)
773 for ctx
in lines_to_add
:
774 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
775 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
776 lines_to_add_to_del
.append((ctx
[0], None))
779 ip/ipv6 prefix-list can be specified without a seq number. However,
780 the running config always adds 'seq x', where x is a number incremented
781 by 5 for every element, to the prefix list. So, ignore such lines as
782 well. Sample prefix-list lines:
783 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
784 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
785 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
787 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
790 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
791 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
792 re_ip_pfxlst
.group(6))
793 for ctx
in lines_to_add
:
794 if ctx
[0][0] == tmpline
:
795 lines_to_del_to_del
.append((ctx_keys
, None))
796 lines_to_add_to_del
.append(((tmpline
,), None))
798 if (len(ctx_keys
) == 3 and
799 ctx_keys
[0].startswith('router bgp') and
800 ctx_keys
[1] == 'address-family l2vpn evpn' and
801 ctx_keys
[2].startswith('vni')):
803 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
806 rt
= re_route_target
.group(1).strip()
807 route_target_import_line
= line
808 route_target_export_line
= "route-target export %s" % rt
809 route_target_both_line
= "route-target both %s" % rt
811 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
812 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
815 If the running configs has
816 route-target import 1:1
817 route-target export 1:1
819 and the config we are reloading against has
820 route-target both 1:1
822 then we can ignore deleting the import/export and ignore adding the 'both'
824 if found_route_target_export_line
and found_route_target_both_line
:
825 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
826 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
827 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
830 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
833 lines_to_del_to_del
.append((ctx_keys
, line
))
834 lines_to_add_to_del
.append((ctx_keys
, line
))
837 We have commands that used to be displayed in the global part
838 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
842 neighbor ISL advertisement-interval 0
848 address-family ipv4 unicast
849 neighbor ISL advertisement-interval 0
851 Look to see if we are deleting it in one format just to add it back in the other
853 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
854 tmp_ctx_keys
= list(ctx_keys
)[:-1]
855 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
857 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
860 lines_to_del_to_del
.append((ctx_keys
, line
))
861 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
863 for (ctx_keys
, line
) in lines_to_del_to_del
:
864 lines_to_del
.remove((ctx_keys
, line
))
866 for (ctx_keys
, line
) in lines_to_add_to_del
:
867 lines_to_add
.remove((ctx_keys
, line
))
869 return (lines_to_add
, lines_to_del
)
872 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
874 There are certain commands that cannot be removed. Remove
875 those commands from lines_to_del.
877 lines_to_del_to_del
= []
879 for (ctx_keys
, line
) in lines_to_del
:
881 if (ctx_keys
[0].startswith('frr version') or
882 ctx_keys
[0].startswith('frr defaults') or
883 ctx_keys
[0].startswith('password') or
884 ctx_keys
[0].startswith('line vty') or
886 # This is technically "no"able but if we did so frr-reload would
887 # stop working so do not let the user shoot themselves in the foot
889 ctx_keys
[0].startswith('service integrated-vtysh-config')):
891 log
.info("(%s, %s) cannot be removed" % (pformat(ctx_keys
), line
))
892 lines_to_del_to_del
.append((ctx_keys
, line
))
894 for (ctx_keys
, line
) in lines_to_del_to_del
:
895 lines_to_del
.remove((ctx_keys
, line
))
897 return (lines_to_add
, lines_to_del
)
900 def compare_context_objects(newconf
, running
):
902 Create a context diff for the two specified contexts
905 # Compare the two Config objects to find the lines that we need to add/del
910 # Find contexts that are in newconf but not in running
911 # Find contexts that are in running but not in newconf
912 for (running_ctx_keys
, running_ctx
) in running
.contexts
.iteritems():
914 if running_ctx_keys
not in newconf
.contexts
:
916 # We check that the len is 1 here so that we only look at ('router bgp 10')
917 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
918 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
919 # running but not in newconf.
920 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
922 lines_to_del
.append((running_ctx_keys
, None))
924 # We cannot do 'no interface' in FRR, and so deal with it
925 elif running_ctx_keys
[0].startswith('interface'):
926 for line
in running_ctx
.lines
:
927 lines_to_del
.append((running_ctx_keys
, line
))
929 # If this is an address-family under 'router bgp' and we are already deleting the
930 # entire 'router bgp' context then ignore this sub-context
931 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
934 # Delete an entire vni sub-context under "address-family l2vpn evpn"
935 elif ("router bgp" in running_ctx_keys
[0] and
936 len(running_ctx_keys
) > 2 and
937 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
938 running_ctx_keys
[2].startswith('vni ')):
939 lines_to_del
.append((running_ctx_keys
, None))
941 elif ("router bgp" in running_ctx_keys
[0] and
942 len(running_ctx_keys
) > 1 and
943 running_ctx_keys
[1].startswith('address-family')):
944 # There's no 'no address-family' support and so we have to
945 # delete each line individually again
946 for line
in running_ctx
.lines
:
947 lines_to_del
.append((running_ctx_keys
, line
))
950 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
951 lines_to_del
.append((running_ctx_keys
, None))
953 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
954 lines_to_del
.append((running_ctx_keys
, None))
958 for line
in running_ctx
.lines
:
959 lines_to_del
.append((running_ctx_keys
, line
))
961 # Find the lines within each context to add
962 # Find the lines within each context to del
963 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
965 if newconf_ctx_keys
in running
.contexts
:
966 running_ctx
= running
.contexts
[newconf_ctx_keys
]
968 for line
in newconf_ctx
.lines
:
969 if line
not in running_ctx
.dlines
:
970 lines_to_add
.append((newconf_ctx_keys
, line
))
972 for line
in running_ctx
.lines
:
973 if line
not in newconf_ctx
.dlines
:
974 lines_to_del
.append((newconf_ctx_keys
, line
))
976 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
978 if newconf_ctx_keys
not in running
.contexts
:
979 lines_to_add
.append((newconf_ctx_keys
, None))
981 for line
in newconf_ctx
.lines
:
982 lines_to_add
.append((newconf_ctx_keys
, line
))
984 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
985 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(lines_to_add
, lines_to_del
)
987 return (lines_to_add
, lines_to_del
)
991 def vtysh_config_available():
993 Return False if no frr daemon is running or some other vtysh session is
994 in 'configuration terminal' mode which will prevent us from making any
995 configuration changes.
999 cmd
= ['/usr/bin/vtysh', '-c', 'conf t']
1000 output
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
).strip()
1002 if 'VTY configuration is locked by other VTY' in output
:
1004 log
.error("'%s' returned\n%s\n" % (' '.join(cmd
), output
))
1007 except subprocess
.CalledProcessError
as e
:
1008 msg
= "vtysh could not connect with any frr daemons"
1016 if __name__
== '__main__':
1017 # Command line options
1018 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
1019 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
1020 group
= parser
.add_mutually_exclusive_group(required
=True)
1021 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
1022 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
1023 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
1024 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
1025 parser
.add_argument('filename', help='Location of new frr config file')
1026 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
1027 args
= parser
.parse_args()
1030 # For --test log to stdout
1031 # For --reload log to /var/log/frr/frr-reload.log
1032 if args
.test
or args
.stdout
:
1033 logging
.basicConfig(level
=logging
.INFO
,
1034 format
='%(asctime)s %(levelname)5s: %(message)s')
1036 # Color the errors and warnings in red
1037 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
1038 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
1041 if not os
.path
.isdir('/var/log/frr/'):
1042 os
.makedirs('/var/log/frr/')
1044 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
1046 format
='%(asctime)s %(levelname)5s: %(message)s')
1048 # argparse should prevent this from happening but just to be safe...
1050 raise Exception('Must specify --reload or --test')
1051 log
= logging
.getLogger(__name__
)
1053 # Verify the new config file is valid
1054 if not os
.path
.isfile(args
.filename
):
1055 msg
= "Filename %s does not exist" % args
.filename
1060 if not os
.path
.getsize(args
.filename
):
1061 msg
= "Filename %s is an empty file" % args
.filename
1066 # Verify that 'service integrated-vtysh-config' is configured
1067 vtysh_filename
= '/etc/frr/vtysh.conf'
1068 service_integrated_vtysh_config
= True
1070 if os
.path
.isfile(vtysh_filename
):
1071 with
open(vtysh_filename
, 'r') as fh
:
1072 for line
in fh
.readlines():
1075 if line
== 'no service integrated-vtysh-config':
1076 service_integrated_vtysh_config
= False
1079 if not service_integrated_vtysh_config
:
1080 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1086 log
.setLevel(logging
.DEBUG
)
1088 log
.info('Called via "%s"', str(args
))
1090 # Create a Config object from the config generated by newconf
1092 newconf
.load_from_file(args
.filename
)
1097 # Create a Config object from the running config
1101 running
.load_from_file(args
.input)
1103 running
.load_from_show_running()
1105 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1106 lines_to_configure
= []
1109 print "\nLines To Delete"
1110 print "==============="
1112 for (ctx_keys
, line
) in lines_to_del
:
1117 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1118 lines_to_configure
.append(cmd
)
1122 print "\nLines To Add"
1123 print "============"
1125 for (ctx_keys
, line
) in lines_to_add
:
1130 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1131 lines_to_configure
.append(cmd
)
1136 # We will not be able to do anything, go ahead and exit(1)
1137 if not vtysh_config_available():
1140 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1142 # This looks a little odd but we have to do this twice...here is why
1143 # If the user had this running bgp config:
1146 # neighbor 1.1.1.1 remote-as 50
1147 # neighbor 1.1.1.1 route-map FOO out
1149 # and this config in the newconf config file
1152 # neighbor 1.1.1.1 remote-as 999
1153 # neighbor 1.1.1.1 route-map FOO out
1156 # Then the script will do
1157 # - no neighbor 1.1.1.1 remote-as 50
1158 # - neighbor 1.1.1.1 remote-as 999
1160 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1161 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1162 # configs again to put this line back.
1164 # There are many keywords in FRR that can only appear one time under
1165 # a context, take "bgp router-id" for example. If the config that we are
1166 # reloading against has the following:
1169 # bgp router-id 1.1.1.1
1170 # bgp router-id 2.2.2.2
1172 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1173 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1174 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1175 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1176 # second pass to include all of the "adds" from the first pass.
1177 lines_to_add_first_pass
= []
1181 running
.load_from_show_running()
1182 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1184 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1187 lines_to_add_first_pass
= lines_to_add
1189 lines_to_add
.extend(lines_to_add_first_pass
)
1191 # Only do deletes on the first pass. The reason being if we
1192 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1193 # will automatically add:
1196 # ipv6 nd ra-interval 10
1197 # no ipv6 nd suppress-ra
1200 # but those lines aren't in the config we are reloading against so
1201 # on the 2nd pass they will show up in lines_to_del. This could
1202 # apply to other scenarios as well where configuring FOO adds BAR
1204 if lines_to_del
and x
== 0:
1205 for (ctx_keys
, line
) in lines_to_del
:
1210 # 'no' commands are tricky, we can't just put them in a file and
1211 # vtysh -f that file. See the next comment for an explanation
1213 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1216 # Some commands in frr are picky about taking a "no" of the entire line.
1217 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1218 # only the beginning. If we hit one of these command an exception will be
1219 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1222 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1223 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1224 # % Unknown command.
1225 # frr(config-if)# no ip ospf authentication message-digest
1226 # % Unknown command.
1227 # frr(config-if)# no ip ospf authentication
1232 _
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
)
1234 except subprocess
.CalledProcessError
:
1236 # - Pull the last entry from cmd (this would be
1237 # 'no ip ospf authentication message-digest 1.1.1.1' in
1239 # - Split that last entry by whitespace and drop the last word
1240 log
.info('Failed to execute %s', ' '.join(cmd
))
1241 last_arg
= cmd
[-1].split(' ')
1243 if len(last_arg
) <= 2:
1244 log
.error('"%s" we failed to remove this command', original_cmd
)
1247 new_last_arg
= last_arg
[0:-1]
1248 cmd
[-1] = ' '.join(new_last_arg
)
1250 log
.info('Executed "%s"', ' '.join(cmd
))
1254 lines_to_configure
= []
1256 for (ctx_keys
, line
) in lines_to_add
:
1261 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1262 lines_to_configure
.append(cmd
)
1264 if lines_to_configure
:
1265 random_string
= ''.join(random
.SystemRandom().choice(
1266 string
.ascii_uppercase
+
1267 string
.digits
) for _
in range(6))
1269 filename
= "/var/run/frr/reload-%s.txt" % random_string
1270 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1272 with
open(filename
, 'w') as fh
:
1273 for line
in lines_to_configure
:
1274 fh
.write(line
+ '\n')
1277 subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
], stderr
=subprocess
.STDOUT
)
1278 except subprocess
.CalledProcessError
as e
:
1279 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1283 # Make these changes persistent
1284 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1285 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])