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
31 from __future__
import print_function
, unicode_literals
41 from collections
import OrderedDict
43 from ipaddress
import IPv6Address
, ip_network
45 from ipaddr
import IPv6Address
, IPNetwork
46 from pprint
import pformat
50 except AttributeError:
53 return iter(d
.items())
59 log
= logging
.getLogger(__name__
)
62 class VtyshMarkException(Exception):
66 class Context(object):
69 A Context object represents a section of frr configuration such as:
72 description swp3 -> r8's swp1
77 or a single line context object such as this:
83 def __init__(self
, keys
, lines
):
87 # Keep a dictionary of the lines, this is to make it easy to tell if a
88 # line exists in this Context
89 self
.dlines
= OrderedDict()
92 self
.dlines
[ligne
] = True
94 def add_lines(self
, lines
):
96 Add lines to specified context
99 self
.lines
.extend(lines
)
102 self
.dlines
[ligne
] = True
105 class Config(object):
108 A frr configuration is stored in a Config object. A Config object
109 contains a dictionary of Context objects where the Context keys
110 ('router ospf' for example) are our dictionary key.
115 self
.contexts
= OrderedDict()
117 def load_from_file(self
, filename
):
119 Read configuration from specified file and slurp it into internal memory
120 The internal representation has been marked appropriately by passing it
121 through vtysh with the -m parameter
123 log
.info('Loading Config object from file %s', filename
)
126 file_output
= subprocess
.check_output(['/usr/bin/vtysh', '-m', '-f', filename
],
127 stderr
=subprocess
.STDOUT
)
128 except subprocess
.CalledProcessError
as e
:
129 ve
= VtyshMarkException(e
)
133 for line
in file_output
.decode('utf-8').split('\n'):
136 # Compress duplicate whitespaces
137 line
= ' '.join(line
.split())
140 qv6_line
= get_normalized_ipv6_line(line
)
141 self
.lines
.append(qv6_line
)
143 self
.lines
.append(line
)
147 def load_from_show_running(self
):
149 Read running configuration and slurp it into internal memory
150 The internal representation has been marked appropriately by passing it
151 through vtysh with the -m parameter
153 log
.info('Loading Config object from vtysh show running')
156 config_text
= subprocess
.check_output(
157 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
158 shell
=True, stderr
=subprocess
.STDOUT
)
159 except subprocess
.CalledProcessError
as e
:
160 ve
= VtyshMarkException(e
)
164 for line
in config_text
.decode('utf-8').split('\n'):
167 if (line
== 'Building configuration...' or
168 line
== 'Current configuration:' or
172 self
.lines
.append(line
)
178 Return the lines read in from the configuration
181 return '\n'.join(self
.lines
)
183 def get_contexts(self
):
185 Return the parsed context as strings for display, log etc.
188 for (_
, ctx
) in sorted(iteritems(self
.contexts
)):
189 print(str(ctx
) + '\n')
191 def save_contexts(self
, key
, lines
):
193 Save the provided key and lines as a context
200 IP addresses specified in "network" statements, "ip prefix-lists"
201 etc. can differ in the host part of the specification the user
202 provides and what the running config displays. For example, user
203 can specify 11.1.1.1/24, and the running config displays this as
204 11.1.1.0/24. Ensure we don't do a needless operation for such
205 lines. IS-IS & OSPFv3 have no "network" support.
207 re_key_rt
= re
.match(r
'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key
[0])
209 addr
= re_key_rt
.group(2)
212 if 'ipaddress' not in sys
.modules
:
213 newaddr
= IPNetwork(addr
)
214 key
[0] = '%s route %s/%s%s' % (re_key_rt
.group(1),
219 newaddr
= ip_network(addr
, strict
=False)
220 key
[0] = '%s route %s/%s%s' % (re_key_rt
.group(1),
221 str(newaddr
.network_address
),
227 re_key_rt
= re
.match(
228 r
'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
232 addr
= re_key_rt
.group(4)
235 if 'ipaddress' not in sys
.modules
:
236 newaddr
= '%s/%s' % (IPNetwork(addr
).network
,
237 IPNetwork(addr
).prefixlen
)
239 network_addr
= ip_network(addr
, strict
=False)
240 newaddr
= '%s/%s' % (str(network_addr
.network_address
),
241 network_addr
.prefixlen
)
247 legestr
= re_key_rt
.group(5)
248 re_lege
= re
.search(r
'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr
)
250 legestr
= '%sge %s le %s%s' % (re_lege
.group(1),
254 re_lege
= re
.search(r
'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr
)
256 if (re_lege
and ((re_key_rt
.group(1) == "ip" and
257 re_lege
.group(3) == "32") or
258 (re_key_rt
.group(1) == "ipv6" and
259 re_lege
.group(3) == "128"))):
260 legestr
= '%sge %s%s' % (re_lege
.group(1),
264 key
[0] = '%s prefix-list%s%s %s%s' % (re_key_rt
.group(1),
270 if lines
and key
[0].startswith('router bgp'):
273 re_net
= re
.match(r
'network\s+([A-Fa-f:.0-9/]+)(.*)$', line
)
275 addr
= re_net
.group(1)
276 if '/' not in addr
and key
[0].startswith('router bgp'):
277 # This is most likely an error because with no
278 # prefixlen, BGP treats the prefixlen as 8
282 if 'ipaddress' not in sys
.modules
:
283 newaddr
= IPNetwork(addr
)
284 line
= 'network %s/%s %s' % (newaddr
.network
,
288 network_addr
= ip_network(addr
, strict
=False)
289 line
= 'network %s/%s %s' % (str(network_addr
.network_address
),
290 network_addr
.prefixlen
,
292 newlines
.append(line
)
294 # Really this should be an error. Whats a network
295 # without an IP Address following it ?
296 newlines
.append(line
)
298 newlines
.append(line
)
302 More fixups in user specification and what running config shows.
303 "null0" in routes must be replaced by Null0.
305 if (key
[0].startswith('ip route') or key
[0].startswith('ipv6 route') and
307 key
[0] = re
.sub(r
'\s+null0(\s*$)', ' Null0', key
[0])
310 if tuple(key
) not in self
.contexts
:
311 ctx
= Context(tuple(key
), lines
)
312 self
.contexts
[tuple(key
)] = ctx
314 ctx
= self
.contexts
[tuple(key
)]
318 if tuple(key
) not in self
.contexts
:
319 ctx
= Context(tuple(key
), [])
320 self
.contexts
[tuple(key
)] = ctx
322 def load_contexts(self
):
324 Parse the configuration and create contexts for each appropriate block
327 current_context_lines
= []
331 The end of a context is flagged via the 'end' keyword:
340 bgp router-id 10.0.0.1
341 bgp log-neighbor-changes
342 no bgp default ipv4-unicast
343 neighbor EBGP peer-group
344 neighbor EBGP advertisement-interval 1
345 neighbor EBGP timers connect 10
346 neighbor 2001:40:1:4::6 remote-as 40
347 neighbor 2001:40:1:8::a remote-as 40
351 neighbor IBGPv6 activate
352 neighbor 2001:10::2 peer-group IBGPv6
353 neighbor 2001:10::3 peer-group IBGPv6
358 neighbor LEAF activate
362 route-target import 10.1.1.1:10100
363 route-target export 10.1.1.1:10100
369 ospf router-id 10.0.0.1
370 log-adjacency-changes detail
371 timers throttle spf 0 50 5000
376 # The code assumes that its working on the output from the "vtysh -m"
377 # command. That provides the appropriate markers to signify end of
378 # a context. This routine uses that to build the contexts for the
381 # There are single line contexts such as "log file /media/node/zebra.log"
382 # and multi-line contexts such as "router ospf" and subcontexts
383 # within a context such as "address-family" within "router bgp"
384 # In each of these cases, the first line of the context becomes the
385 # key of the context. So "router bgp 10" is the key for the non-address
386 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
387 # the key for the subcontext and so on.
392 # the keywords that we know are single line contexts. bgp in this case
393 # is not the main router bgp block, but enabling multi-instance
394 oneline_ctx_keywords
= ("access-list ",
415 for line
in self
.lines
:
420 if line
.startswith('!') or line
.startswith('#'):
424 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
425 self
.save_contexts(ctx_keys
, current_context_lines
)
427 # Start a new context
430 current_context_lines
= []
432 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
433 self
.save_contexts(ctx_keys
, current_context_lines
)
437 self
.save_contexts(ctx_keys
, current_context_lines
)
438 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
440 # Start a new context
444 current_context_lines
= []
446 elif line
== "exit-vrf":
447 self
.save_contexts(ctx_keys
, current_context_lines
)
448 current_context_lines
.append(line
)
449 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
455 current_context_lines
= []
457 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
458 # if this exit is for address-family ipv4 unicast, ignore the pop
460 self
.save_contexts(ctx_keys
, current_context_lines
)
462 # Start a new context
463 ctx_keys
= copy
.deepcopy(main_ctx_key
)
464 current_context_lines
= []
465 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
467 elif line
== "exit-vni":
469 self
.save_contexts(ctx_keys
, current_context_lines
)
471 # Start a new context
472 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
473 current_context_lines
= []
474 log
.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line
, ctx_keys
)
476 elif new_ctx
is True:
480 ctx_keys
= copy
.deepcopy(main_ctx_key
)
483 current_context_lines
= []
485 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
486 elif (line
.startswith("address-family ") or
487 line
.startswith("vnc defaults") or
488 line
.startswith("vnc l2-group") or
489 line
.startswith("vnc nve-group")):
492 # Save old context first
493 self
.save_contexts(ctx_keys
, current_context_lines
)
494 current_context_lines
= []
495 main_ctx_key
= copy
.deepcopy(ctx_keys
)
496 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
498 if line
== "address-family ipv6":
499 ctx_keys
.append("address-family ipv6 unicast")
500 elif line
== "address-family ipv4":
501 ctx_keys
.append("address-family ipv4 unicast")
502 elif line
== "address-family evpn":
503 ctx_keys
.append("address-family l2vpn evpn")
505 ctx_keys
.append(line
)
507 elif ((line
.startswith("vni ") and
508 len(ctx_keys
) == 2 and
509 ctx_keys
[0].startswith('router bgp') and
510 ctx_keys
[1] == 'address-family l2vpn evpn')):
512 # Save old context first
513 self
.save_contexts(ctx_keys
, current_context_lines
)
514 current_context_lines
= []
515 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
516 log
.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line
)
517 ctx_keys
.append(line
)
520 # Continuing in an existing context, add non-commented lines to it
521 current_context_lines
.append(line
)
522 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
524 # Save the context of the last one
525 self
.save_contexts(ctx_keys
, current_context_lines
)
528 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
530 Return the vtysh command for the specified context line
539 for ctx_key
in ctx_keys
:
548 if line
.startswith('no '):
549 cmd
.append('%s' % line
[3:])
551 cmd
.append('no %s' % line
)
557 # If line is None then we are typically deleting an entire
558 # context ('no router ospf' for example)
563 # Only put the 'no' on the last sub-context
564 for ctx_key
in ctx_keys
:
567 if ctx_key
== ctx_keys
[-1]:
568 cmd
.append('no %s' % ctx_key
)
570 cmd
.append('%s' % ctx_key
)
572 for ctx_key
in ctx_keys
:
579 def line_for_vtysh_file(ctx_keys
, line
, delete
):
581 Return the command as it would appear in frr.conf
586 for (i
, ctx_key
) in enumerate(ctx_keys
):
587 cmd
.append(' ' * i
+ ctx_key
)
590 indent
= len(ctx_keys
) * ' '
593 if line
.startswith('no '):
594 cmd
.append('%s%s' % (indent
, line
[3:]))
596 cmd
.append('%sno %s' % (indent
, line
))
599 cmd
.append(indent
+ line
)
601 # If line is None then we are typically deleting an entire
602 # context ('no router ospf' for example)
606 # Only put the 'no' on the last sub-context
607 for ctx_key
in ctx_keys
:
609 if ctx_key
== ctx_keys
[-1]:
610 cmd
.append('no %s' % ctx_key
)
612 cmd
.append('%s' % ctx_key
)
614 for ctx_key
in ctx_keys
:
617 cmd
= '\n' + '\n'.join(cmd
)
619 # There are some commands that are on by default so their "no" form will be
620 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
621 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
622 # not by doing a "no no bgp default ipv4-unicast"
623 cmd
= cmd
.replace('no no ', '')
628 def get_normalized_ipv6_line(line
):
630 Return a normalized IPv6 line as produced by frr,
631 with all letters in lower case and trailing and leading
632 zeros removed, and only the network portion present if
633 the IPv6 word is a network
636 words
= line
.split(' ')
642 if 'ipaddress' not in sys
.modules
:
643 v6word
= IPNetwork(word
)
644 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
646 v6word
= ip_network(word
, strict
=False)
647 norm_word
= '%s/%s' % (str(v6word
.network_address
), v6word
.prefixlen
)
652 norm_word
= '%s' % IPv6Address(word
)
657 norm_line
= norm_line
+ " " + norm_word
659 return norm_line
.strip()
662 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
663 for (ctx_keys
, line
) in lines
:
664 if ctx_keys
== target_ctx_keys
:
666 if line
== target_line
:
669 if line
.startswith(target_line
):
674 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
676 # Quite possibly the most confusing (while accurate) variable names in history
677 lines_to_add_to_del
= []
678 lines_to_del_to_del
= []
680 for (ctx_keys
, line
) in lines_to_del
:
683 if ctx_keys
[0].startswith('router bgp') and line
:
685 if line
.startswith('neighbor '):
687 BGP changed how it displays swpX peers that are part of peer-group. Older
688 versions of frr would display these on separate lines:
689 neighbor swp1 interface
690 neighbor swp1 peer-group FOO
692 but today we display via a single line
693 neighbor swp1 interface peer-group FOO
695 This change confuses frr-reload.py so check to see if we are deleting
696 neighbor swp1 interface peer-group FOO
699 neighbor swp1 interface
700 neighbor swp1 peer-group FOO
702 If so then chop the del line and the corresponding add lines
705 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
706 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
708 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
709 swpx_interface
= None
710 swpx_peergroup
= None
712 if re_swpx_int_peergroup
:
713 swpx
= re_swpx_int_peergroup
.group(1)
714 peergroup
= re_swpx_int_peergroup
.group(2)
715 swpx_interface
= "neighbor %s interface" % swpx
716 elif re_swpx_int_v6only_peergroup
:
717 swpx
= re_swpx_int_v6only_peergroup
.group(1)
718 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
719 swpx_interface
= "neighbor %s interface v6only" % swpx
721 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
722 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
723 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
724 tmp_ctx_keys
= tuple(list(ctx_keys
))
726 if not found_add_swpx_peergroup
:
727 tmp_ctx_keys
= list(ctx_keys
)
728 tmp_ctx_keys
.append('address-family ipv4 unicast')
729 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
730 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
732 if not found_add_swpx_peergroup
:
733 tmp_ctx_keys
= list(ctx_keys
)
734 tmp_ctx_keys
.append('address-family ipv6 unicast')
735 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
736 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
738 if found_add_swpx_interface
and found_add_swpx_peergroup
:
740 lines_to_del_to_del
.append((ctx_keys
, line
))
741 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
742 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
745 We changed how we display the neighbor interface command. Older
746 versions of frr would display the following:
747 neighbor swp1 interface
748 neighbor swp1 remote-as external
749 neighbor swp1 capability extended-nexthop
751 but today we display via a single line
752 neighbor swp1 interface remote-as external
754 and capability extended-nexthop is no longer needed because we
755 automatically enable it when the neighbor is of type interface.
757 This change confuses frr-reload.py so check to see if we are deleting
758 neighbor swp1 interface remote-as (external|internal|ASNUM)
761 neighbor swp1 interface
762 neighbor swp1 remote-as (external|internal|ASNUM)
763 neighbor swp1 capability extended-nexthop
765 If so then chop the del line and the corresponding add lines
767 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
768 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
770 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
771 swpx_interface
= None
774 if re_swpx_int_remoteas
:
775 swpx
= re_swpx_int_remoteas
.group(1)
776 remoteas
= re_swpx_int_remoteas
.group(2)
777 swpx_interface
= "neighbor %s interface" % swpx
778 elif re_swpx_int_v6only_remoteas
:
779 swpx
= re_swpx_int_v6only_remoteas
.group(1)
780 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
781 swpx_interface
= "neighbor %s interface v6only" % swpx
783 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
784 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
785 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
786 tmp_ctx_keys
= tuple(list(ctx_keys
))
788 if found_add_swpx_interface
and found_add_swpx_remoteas
:
790 lines_to_del_to_del
.append((ctx_keys
, line
))
791 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
792 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
795 We made the 'bgp bestpath as-path multipath-relax' command
796 automatically assume 'no-as-set' since the lack of this option caused
797 weird routing problems. When the running config is shown in
798 releases with this change, the no-as-set keyword is not shown as it
799 is the default. This causes frr-reload to unnecessarily unapply
800 this option only to apply it back again, causing unnecessary session
803 if 'multipath-relax' in line
:
804 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
805 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
806 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
808 if re_asrelax_new
and found_asrelax_old
:
810 lines_to_del_to_del
.append((ctx_keys
, line
))
811 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
814 If we are modifying the BGP table-map we need to avoid a del/add and
815 instead modify the table-map in place via an add. This is needed to
816 avoid installing all routes in the RIB the second the 'no table-map'
819 if line
.startswith('table-map'):
820 found_table_map
= line_exist(lines_to_add
, ctx_keys
, 'table-map', False)
823 lines_to_del_to_del
.append((ctx_keys
, line
))
826 More old-to-new config handling. ip import-table no longer accepts
827 distance, but we honor the old syntax. But 'show running' shows only
828 the new syntax. This causes an unnecessary 'no import-table' followed
829 by the same old 'ip import-table' which causes perturbations in
830 announced routes leading to traffic blackholes. Fix this issue.
832 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
834 table_num
= re_importtbl
.group(1)
835 for ctx
in lines_to_add
:
836 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
837 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
838 lines_to_add_to_del
.append((ctx
[0], None))
841 ip/ipv6 prefix-list can be specified without a seq number. However,
842 the running config always adds 'seq x', where x is a number incremented
843 by 5 for every element, to the prefix list. So, ignore such lines as
844 well. Sample prefix-list lines:
845 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
846 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
847 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
849 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
852 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
853 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
854 re_ip_pfxlst
.group(6))
855 for ctx
in lines_to_add
:
856 if ctx
[0][0] == tmpline
:
857 lines_to_del_to_del
.append((ctx_keys
, None))
858 lines_to_add_to_del
.append(((tmpline
,), None))
860 if (len(ctx_keys
) == 3 and
861 ctx_keys
[0].startswith('router bgp') and
862 ctx_keys
[1] == 'address-family l2vpn evpn' and
863 ctx_keys
[2].startswith('vni')):
865 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
868 rt
= re_route_target
.group(1).strip()
869 route_target_import_line
= line
870 route_target_export_line
= "route-target export %s" % rt
871 route_target_both_line
= "route-target both %s" % rt
873 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
874 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
877 If the running configs has
878 route-target import 1:1
879 route-target export 1:1
881 and the config we are reloading against has
882 route-target both 1:1
884 then we can ignore deleting the import/export and ignore adding the 'both'
886 if found_route_target_export_line
and found_route_target_both_line
:
887 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
888 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
889 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
892 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
895 lines_to_del_to_del
.append((ctx_keys
, line
))
896 lines_to_add_to_del
.append((ctx_keys
, line
))
899 We have commands that used to be displayed in the global part
900 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
904 neighbor ISL advertisement-interval 0
910 address-family ipv4 unicast
911 neighbor ISL advertisement-interval 0
913 Look to see if we are deleting it in one format just to add it back in the other
915 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
916 tmp_ctx_keys
= list(ctx_keys
)[:-1]
917 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
919 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
922 lines_to_del_to_del
.append((ctx_keys
, line
))
923 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
925 for (ctx_keys
, line
) in lines_to_del_to_del
:
926 lines_to_del
.remove((ctx_keys
, line
))
928 for (ctx_keys
, line
) in lines_to_add_to_del
:
929 lines_to_add
.remove((ctx_keys
, line
))
931 return (lines_to_add
, lines_to_del
)
934 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
936 There are certain commands that cannot be removed. Remove
937 those commands from lines_to_del.
939 lines_to_del_to_del
= []
941 for (ctx_keys
, line
) in lines_to_del
:
943 if (ctx_keys
[0].startswith('frr version') or
944 ctx_keys
[0].startswith('frr defaults') or
945 ctx_keys
[0].startswith('password') or
946 ctx_keys
[0].startswith('line vty') or
948 # This is technically "no"able but if we did so frr-reload would
949 # stop working so do not let the user shoot themselves in the foot
951 ctx_keys
[0].startswith('service integrated-vtysh-config')):
953 log
.info("(%s, %s) cannot be removed" % (pformat(ctx_keys
), line
))
954 lines_to_del_to_del
.append((ctx_keys
, line
))
956 for (ctx_keys
, line
) in lines_to_del_to_del
:
957 lines_to_del
.remove((ctx_keys
, line
))
959 return (lines_to_add
, lines_to_del
)
962 def compare_context_objects(newconf
, running
):
964 Create a context diff for the two specified contexts
967 # Compare the two Config objects to find the lines that we need to add/del
972 # Find contexts that are in newconf but not in running
973 # Find contexts that are in running but not in newconf
974 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
976 if running_ctx_keys
not in newconf
.contexts
:
978 # We check that the len is 1 here so that we only look at ('router bgp 10')
979 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
980 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
981 # running but not in newconf.
982 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
984 lines_to_del
.append((running_ctx_keys
, None))
986 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
987 elif running_ctx_keys
[0].startswith('interface') or running_ctx_keys
[0].startswith('vrf'):
988 for line
in running_ctx
.lines
:
989 lines_to_del
.append((running_ctx_keys
, line
))
991 # If this is an address-family under 'router bgp' and we are already deleting the
992 # entire 'router bgp' context then ignore this sub-context
993 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
996 # Delete an entire vni sub-context under "address-family l2vpn evpn"
997 elif ("router bgp" in running_ctx_keys
[0] and
998 len(running_ctx_keys
) > 2 and
999 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
1000 running_ctx_keys
[2].startswith('vni ')):
1001 lines_to_del
.append((running_ctx_keys
, None))
1003 elif ("router bgp" in running_ctx_keys
[0] and
1004 len(running_ctx_keys
) > 1 and
1005 running_ctx_keys
[1].startswith('address-family')):
1006 # There's no 'no address-family' support and so we have to
1007 # delete each line individually again
1008 for line
in running_ctx
.lines
:
1009 lines_to_del
.append((running_ctx_keys
, line
))
1011 # Non-global context
1012 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
1013 lines_to_del
.append((running_ctx_keys
, None))
1015 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1016 lines_to_del
.append((running_ctx_keys
, None))
1020 for line
in running_ctx
.lines
:
1021 lines_to_del
.append((running_ctx_keys
, line
))
1023 # Find the lines within each context to add
1024 # Find the lines within each context to del
1025 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1027 if newconf_ctx_keys
in running
.contexts
:
1028 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1030 for line
in newconf_ctx
.lines
:
1031 if line
not in running_ctx
.dlines
:
1032 lines_to_add
.append((newconf_ctx_keys
, line
))
1034 for line
in running_ctx
.lines
:
1035 if line
not in newconf_ctx
.dlines
:
1036 lines_to_del
.append((newconf_ctx_keys
, line
))
1038 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1040 if newconf_ctx_keys
not in running
.contexts
:
1041 lines_to_add
.append((newconf_ctx_keys
, None))
1043 for line
in newconf_ctx
.lines
:
1044 lines_to_add
.append((newconf_ctx_keys
, line
))
1046 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
1047 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(lines_to_add
, lines_to_del
)
1049 return (lines_to_add
, lines_to_del
)
1053 def vtysh_config_available():
1055 Return False if no frr daemon is running or some other vtysh session is
1056 in 'configuration terminal' mode which will prevent us from making any
1057 configuration changes.
1061 cmd
= ['/usr/bin/vtysh', '-c', 'conf t']
1062 output
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
).strip()
1064 if 'VTY configuration is locked by other VTY' in output
.decode('utf-8'):
1066 log
.error("'%s' returned\n%s\n" % (' '.join(cmd
), output
))
1069 except subprocess
.CalledProcessError
as e
:
1070 msg
= "vtysh could not connect with any frr daemons"
1078 if __name__
== '__main__':
1079 # Command line options
1080 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
1081 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
1082 group
= parser
.add_mutually_exclusive_group(required
=True)
1083 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
1084 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
1085 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
1086 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
1087 parser
.add_argument('filename', help='Location of new frr config file')
1088 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
1089 args
= parser
.parse_args()
1092 # For --test log to stdout
1093 # For --reload log to /var/log/frr/frr-reload.log
1094 if args
.test
or args
.stdout
:
1095 logging
.basicConfig(level
=logging
.INFO
,
1096 format
='%(asctime)s %(levelname)5s: %(message)s')
1098 # Color the errors and warnings in red
1099 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
1100 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
1103 if not os
.path
.isdir('/var/log/frr/'):
1104 os
.makedirs('/var/log/frr/')
1106 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
1108 format
='%(asctime)s %(levelname)5s: %(message)s')
1110 # argparse should prevent this from happening but just to be safe...
1112 raise Exception('Must specify --reload or --test')
1113 log
= logging
.getLogger(__name__
)
1115 # Verify the new config file is valid
1116 if not os
.path
.isfile(args
.filename
):
1117 msg
= "Filename %s does not exist" % args
.filename
1122 if not os
.path
.getsize(args
.filename
):
1123 msg
= "Filename %s is an empty file" % args
.filename
1128 # Verify that 'service integrated-vtysh-config' is configured
1129 vtysh_filename
= '/etc/frr/vtysh.conf'
1130 service_integrated_vtysh_config
= True
1132 if os
.path
.isfile(vtysh_filename
):
1133 with
open(vtysh_filename
, 'r') as fh
:
1134 for line
in fh
.readlines():
1137 if line
== 'no service integrated-vtysh-config':
1138 service_integrated_vtysh_config
= False
1141 if not service_integrated_vtysh_config
:
1142 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1148 log
.setLevel(logging
.DEBUG
)
1150 log
.info('Called via "%s"', str(args
))
1152 # Create a Config object from the config generated by newconf
1154 newconf
.load_from_file(args
.filename
)
1159 # Create a Config object from the running config
1163 running
.load_from_file(args
.input)
1165 running
.load_from_show_running()
1167 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1168 lines_to_configure
= []
1171 print("\nLines To Delete")
1172 print("===============")
1174 for (ctx_keys
, line
) in lines_to_del
:
1179 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1180 lines_to_configure
.append(cmd
)
1184 print("\nLines To Add")
1185 print("============")
1187 for (ctx_keys
, line
) in lines_to_add
:
1192 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1193 lines_to_configure
.append(cmd
)
1198 # We will not be able to do anything, go ahead and exit(1)
1199 if not vtysh_config_available():
1202 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1204 # This looks a little odd but we have to do this twice...here is why
1205 # If the user had this running bgp config:
1208 # neighbor 1.1.1.1 remote-as 50
1209 # neighbor 1.1.1.1 route-map FOO out
1211 # and this config in the newconf config file
1214 # neighbor 1.1.1.1 remote-as 999
1215 # neighbor 1.1.1.1 route-map FOO out
1218 # Then the script will do
1219 # - no neighbor 1.1.1.1 remote-as 50
1220 # - neighbor 1.1.1.1 remote-as 999
1222 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1223 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1224 # configs again to put this line back.
1226 # There are many keywords in FRR that can only appear one time under
1227 # a context, take "bgp router-id" for example. If the config that we are
1228 # reloading against has the following:
1231 # bgp router-id 1.1.1.1
1232 # bgp router-id 2.2.2.2
1234 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1235 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1236 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1237 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1238 # second pass to include all of the "adds" from the first pass.
1239 lines_to_add_first_pass
= []
1243 running
.load_from_show_running()
1244 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1246 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1249 lines_to_add_first_pass
= lines_to_add
1251 lines_to_add
.extend(lines_to_add_first_pass
)
1253 # Only do deletes on the first pass. The reason being if we
1254 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1255 # will automatically add:
1258 # ipv6 nd ra-interval 10
1259 # no ipv6 nd suppress-ra
1262 # but those lines aren't in the config we are reloading against so
1263 # on the 2nd pass they will show up in lines_to_del. This could
1264 # apply to other scenarios as well where configuring FOO adds BAR
1266 if lines_to_del
and x
== 0:
1267 for (ctx_keys
, line
) in lines_to_del
:
1272 # 'no' commands are tricky, we can't just put them in a file and
1273 # vtysh -f that file. See the next comment for an explanation
1275 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1278 # Some commands in frr are picky about taking a "no" of the entire line.
1279 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1280 # only the beginning. If we hit one of these command an exception will be
1281 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1284 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1285 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1286 # % Unknown command.
1287 # frr(config-if)# no ip ospf authentication message-digest
1288 # % Unknown command.
1289 # frr(config-if)# no ip ospf authentication
1294 _
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
)
1296 except subprocess
.CalledProcessError
:
1298 # - Pull the last entry from cmd (this would be
1299 # 'no ip ospf authentication message-digest 1.1.1.1' in
1301 # - Split that last entry by whitespace and drop the last word
1302 log
.info('Failed to execute %s', ' '.join(cmd
))
1303 last_arg
= cmd
[-1].split(' ')
1305 if len(last_arg
) <= 2:
1306 log
.error('"%s" we failed to remove this command', original_cmd
)
1309 new_last_arg
= last_arg
[0:-1]
1310 cmd
[-1] = ' '.join(new_last_arg
)
1312 log
.info('Executed "%s"', ' '.join(cmd
))
1316 lines_to_configure
= []
1318 for (ctx_keys
, line
) in lines_to_add
:
1323 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1324 lines_to_configure
.append(cmd
)
1326 if lines_to_configure
:
1327 random_string
= ''.join(random
.SystemRandom().choice(
1328 string
.ascii_uppercase
+
1329 string
.digits
) for _
in range(6))
1331 filename
= "/var/run/frr/reload-%s.txt" % random_string
1332 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1334 with
open(filename
, 'w') as fh
:
1335 for line
in lines_to_configure
:
1336 fh
.write(line
+ '\n')
1339 subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
], stderr
=subprocess
.STDOUT
)
1340 except subprocess
.CalledProcessError
as e
:
1341 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1345 # Make these changes persistent
1346 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1347 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])