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
)
436 elif line
in ["end", "exit-vrf"]:
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
in ["exit-address-family", "exit", "exit-vnc"]:
447 # if this exit is for address-family ipv4 unicast, ignore the pop
449 self
.save_contexts(ctx_keys
, current_context_lines
)
451 # Start a new context
452 ctx_keys
= copy
.deepcopy(main_ctx_key
)
453 current_context_lines
= []
454 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
456 elif line
== "exit-vni":
458 self
.save_contexts(ctx_keys
, current_context_lines
)
460 # Start a new context
461 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
462 current_context_lines
= []
463 log
.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line
, ctx_keys
)
465 elif new_ctx
is True:
469 ctx_keys
= copy
.deepcopy(main_ctx_key
)
472 current_context_lines
= []
474 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
475 elif (line
.startswith("address-family ") or
476 line
.startswith("vnc defaults") or
477 line
.startswith("vnc l2-group") or
478 line
.startswith("vnc nve-group")):
481 # Save old context first
482 self
.save_contexts(ctx_keys
, current_context_lines
)
483 current_context_lines
= []
484 main_ctx_key
= copy
.deepcopy(ctx_keys
)
485 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
487 if line
== "address-family ipv6":
488 ctx_keys
.append("address-family ipv6 unicast")
489 elif line
== "address-family ipv4":
490 ctx_keys
.append("address-family ipv4 unicast")
491 elif line
== "address-family evpn":
492 ctx_keys
.append("address-family l2vpn evpn")
494 ctx_keys
.append(line
)
496 elif ((line
.startswith("vni ") and
497 len(ctx_keys
) == 2 and
498 ctx_keys
[0].startswith('router bgp') and
499 ctx_keys
[1] == 'address-family l2vpn evpn')):
501 # Save old context first
502 self
.save_contexts(ctx_keys
, current_context_lines
)
503 current_context_lines
= []
504 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
505 log
.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line
)
506 ctx_keys
.append(line
)
509 # Continuing in an existing context, add non-commented lines to it
510 current_context_lines
.append(line
)
511 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
513 # Save the context of the last one
514 self
.save_contexts(ctx_keys
, current_context_lines
)
517 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
519 Return the vtysh command for the specified context line
528 for ctx_key
in ctx_keys
:
537 if line
.startswith('no '):
538 cmd
.append('%s' % line
[3:])
540 cmd
.append('no %s' % line
)
546 # If line is None then we are typically deleting an entire
547 # context ('no router ospf' for example)
552 # Only put the 'no' on the last sub-context
553 for ctx_key
in ctx_keys
:
556 if ctx_key
== ctx_keys
[-1]:
557 cmd
.append('no %s' % ctx_key
)
559 cmd
.append('%s' % ctx_key
)
561 for ctx_key
in ctx_keys
:
568 def line_for_vtysh_file(ctx_keys
, line
, delete
):
570 Return the command as it would appear in frr.conf
575 for (i
, ctx_key
) in enumerate(ctx_keys
):
576 cmd
.append(' ' * i
+ ctx_key
)
579 indent
= len(ctx_keys
) * ' '
582 if line
.startswith('no '):
583 cmd
.append('%s%s' % (indent
, line
[3:]))
585 cmd
.append('%sno %s' % (indent
, line
))
588 cmd
.append(indent
+ line
)
590 # If line is None then we are typically deleting an entire
591 # context ('no router ospf' for example)
595 # Only put the 'no' on the last sub-context
596 for ctx_key
in ctx_keys
:
598 if ctx_key
== ctx_keys
[-1]:
599 cmd
.append('no %s' % ctx_key
)
601 cmd
.append('%s' % ctx_key
)
603 for ctx_key
in ctx_keys
:
606 cmd
= '\n' + '\n'.join(cmd
)
608 # There are some commands that are on by default so their "no" form will be
609 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
610 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
611 # not by doing a "no no bgp default ipv4-unicast"
612 cmd
= cmd
.replace('no no ', '')
617 def get_normalized_ipv6_line(line
):
619 Return a normalized IPv6 line as produced by frr,
620 with all letters in lower case and trailing and leading
621 zeros removed, and only the network portion present if
622 the IPv6 word is a network
625 words
= line
.split(' ')
631 if 'ipaddress' not in sys
.modules
:
632 v6word
= IPNetwork(word
)
633 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
635 v6word
= ip_network(word
, strict
=False)
636 norm_word
= '%s/%s' % (str(v6word
.network_address
), v6word
.prefixlen
)
641 norm_word
= '%s' % IPv6Address(word
)
646 norm_line
= norm_line
+ " " + norm_word
648 return norm_line
.strip()
651 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
652 for (ctx_keys
, line
) in lines
:
653 if ctx_keys
== target_ctx_keys
:
655 if line
== target_line
:
658 if line
.startswith(target_line
):
663 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
665 # Quite possibly the most confusing (while accurate) variable names in history
666 lines_to_add_to_del
= []
667 lines_to_del_to_del
= []
669 for (ctx_keys
, line
) in lines_to_del
:
672 if ctx_keys
[0].startswith('router bgp') and line
:
674 if line
.startswith('neighbor '):
676 BGP changed how it displays swpX peers that are part of peer-group. Older
677 versions of frr would display these on separate lines:
678 neighbor swp1 interface
679 neighbor swp1 peer-group FOO
681 but today we display via a single line
682 neighbor swp1 interface peer-group FOO
684 This change confuses frr-reload.py so check to see if we are deleting
685 neighbor swp1 interface peer-group FOO
688 neighbor swp1 interface
689 neighbor swp1 peer-group FOO
691 If so then chop the del line and the corresponding add lines
694 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
695 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
697 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
698 swpx_interface
= None
699 swpx_peergroup
= None
701 if re_swpx_int_peergroup
:
702 swpx
= re_swpx_int_peergroup
.group(1)
703 peergroup
= re_swpx_int_peergroup
.group(2)
704 swpx_interface
= "neighbor %s interface" % swpx
705 elif re_swpx_int_v6only_peergroup
:
706 swpx
= re_swpx_int_v6only_peergroup
.group(1)
707 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
708 swpx_interface
= "neighbor %s interface v6only" % swpx
710 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
711 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
712 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
713 tmp_ctx_keys
= tuple(list(ctx_keys
))
715 if not found_add_swpx_peergroup
:
716 tmp_ctx_keys
= list(ctx_keys
)
717 tmp_ctx_keys
.append('address-family ipv4 unicast')
718 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
719 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
721 if not found_add_swpx_peergroup
:
722 tmp_ctx_keys
= list(ctx_keys
)
723 tmp_ctx_keys
.append('address-family ipv6 unicast')
724 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
725 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
727 if found_add_swpx_interface
and found_add_swpx_peergroup
:
729 lines_to_del_to_del
.append((ctx_keys
, line
))
730 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
731 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
734 We changed how we display the neighbor interface command. Older
735 versions of frr would display the following:
736 neighbor swp1 interface
737 neighbor swp1 remote-as external
738 neighbor swp1 capability extended-nexthop
740 but today we display via a single line
741 neighbor swp1 interface remote-as external
743 and capability extended-nexthop is no longer needed because we
744 automatically enable it when the neighbor is of type interface.
746 This change confuses frr-reload.py so check to see if we are deleting
747 neighbor swp1 interface remote-as (external|internal|ASNUM)
750 neighbor swp1 interface
751 neighbor swp1 remote-as (external|internal|ASNUM)
752 neighbor swp1 capability extended-nexthop
754 If so then chop the del line and the corresponding add lines
756 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
757 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
759 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
760 swpx_interface
= None
763 if re_swpx_int_remoteas
:
764 swpx
= re_swpx_int_remoteas
.group(1)
765 remoteas
= re_swpx_int_remoteas
.group(2)
766 swpx_interface
= "neighbor %s interface" % swpx
767 elif re_swpx_int_v6only_remoteas
:
768 swpx
= re_swpx_int_v6only_remoteas
.group(1)
769 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
770 swpx_interface
= "neighbor %s interface v6only" % swpx
772 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
773 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
774 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
775 tmp_ctx_keys
= tuple(list(ctx_keys
))
777 if found_add_swpx_interface
and found_add_swpx_remoteas
:
779 lines_to_del_to_del
.append((ctx_keys
, line
))
780 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
781 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
784 We made the 'bgp bestpath as-path multipath-relax' command
785 automatically assume 'no-as-set' since the lack of this option caused
786 weird routing problems. When the running config is shown in
787 releases with this change, the no-as-set keyword is not shown as it
788 is the default. This causes frr-reload to unnecessarily unapply
789 this option only to apply it back again, causing unnecessary session
792 if 'multipath-relax' in line
:
793 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
794 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
795 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
797 if re_asrelax_new
and found_asrelax_old
:
799 lines_to_del_to_del
.append((ctx_keys
, line
))
800 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
803 If we are modifying the BGP table-map we need to avoid a del/add and
804 instead modify the table-map in place via an add. This is needed to
805 avoid installing all routes in the RIB the second the 'no table-map'
808 if line
.startswith('table-map'):
809 found_table_map
= line_exist(lines_to_add
, ctx_keys
, 'table-map', False)
812 lines_to_del_to_del
.append((ctx_keys
, line
))
815 More old-to-new config handling. ip import-table no longer accepts
816 distance, but we honor the old syntax. But 'show running' shows only
817 the new syntax. This causes an unnecessary 'no import-table' followed
818 by the same old 'ip import-table' which causes perturbations in
819 announced routes leading to traffic blackholes. Fix this issue.
821 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
823 table_num
= re_importtbl
.group(1)
824 for ctx
in lines_to_add
:
825 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
826 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
827 lines_to_add_to_del
.append((ctx
[0], None))
830 ip/ipv6 prefix-list can be specified without a seq number. However,
831 the running config always adds 'seq x', where x is a number incremented
832 by 5 for every element, to the prefix list. So, ignore such lines as
833 well. Sample prefix-list lines:
834 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
835 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
836 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
838 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
841 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
842 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
843 re_ip_pfxlst
.group(6))
844 for ctx
in lines_to_add
:
845 if ctx
[0][0] == tmpline
:
846 lines_to_del_to_del
.append((ctx_keys
, None))
847 lines_to_add_to_del
.append(((tmpline
,), None))
849 if (len(ctx_keys
) == 3 and
850 ctx_keys
[0].startswith('router bgp') and
851 ctx_keys
[1] == 'address-family l2vpn evpn' and
852 ctx_keys
[2].startswith('vni')):
854 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
857 rt
= re_route_target
.group(1).strip()
858 route_target_import_line
= line
859 route_target_export_line
= "route-target export %s" % rt
860 route_target_both_line
= "route-target both %s" % rt
862 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
863 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
866 If the running configs has
867 route-target import 1:1
868 route-target export 1:1
870 and the config we are reloading against has
871 route-target both 1:1
873 then we can ignore deleting the import/export and ignore adding the 'both'
875 if found_route_target_export_line
and found_route_target_both_line
:
876 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
877 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
878 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
881 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
884 lines_to_del_to_del
.append((ctx_keys
, line
))
885 lines_to_add_to_del
.append((ctx_keys
, line
))
888 We have commands that used to be displayed in the global part
889 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
893 neighbor ISL advertisement-interval 0
899 address-family ipv4 unicast
900 neighbor ISL advertisement-interval 0
902 Look to see if we are deleting it in one format just to add it back in the other
904 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
905 tmp_ctx_keys
= list(ctx_keys
)[:-1]
906 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
908 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
911 lines_to_del_to_del
.append((ctx_keys
, line
))
912 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
914 for (ctx_keys
, line
) in lines_to_del_to_del
:
915 lines_to_del
.remove((ctx_keys
, line
))
917 for (ctx_keys
, line
) in lines_to_add_to_del
:
918 lines_to_add
.remove((ctx_keys
, line
))
920 return (lines_to_add
, lines_to_del
)
923 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
925 There are certain commands that cannot be removed. Remove
926 those commands from lines_to_del.
928 lines_to_del_to_del
= []
930 for (ctx_keys
, line
) in lines_to_del
:
932 if (ctx_keys
[0].startswith('frr version') or
933 ctx_keys
[0].startswith('frr defaults') or
934 ctx_keys
[0].startswith('password') or
935 ctx_keys
[0].startswith('line vty') or
937 # This is technically "no"able but if we did so frr-reload would
938 # stop working so do not let the user shoot themselves in the foot
940 ctx_keys
[0].startswith('service integrated-vtysh-config')):
942 log
.info("(%s, %s) cannot be removed" % (pformat(ctx_keys
), line
))
943 lines_to_del_to_del
.append((ctx_keys
, line
))
945 for (ctx_keys
, line
) in lines_to_del_to_del
:
946 lines_to_del
.remove((ctx_keys
, line
))
948 return (lines_to_add
, lines_to_del
)
951 def compare_context_objects(newconf
, running
):
953 Create a context diff for the two specified contexts
956 # Compare the two Config objects to find the lines that we need to add/del
961 # Find contexts that are in newconf but not in running
962 # Find contexts that are in running but not in newconf
963 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
965 if running_ctx_keys
not in newconf
.contexts
:
967 # We check that the len is 1 here so that we only look at ('router bgp 10')
968 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
969 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
970 # running but not in newconf.
971 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
973 lines_to_del
.append((running_ctx_keys
, None))
975 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
976 elif running_ctx_keys
[0].startswith('interface') or running_ctx_keys
[0].startswith('vrf'):
977 for line
in running_ctx
.lines
:
978 lines_to_del
.append((running_ctx_keys
, line
))
980 # If this is an address-family under 'router bgp' and we are already deleting the
981 # entire 'router bgp' context then ignore this sub-context
982 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
985 # Delete an entire vni sub-context under "address-family l2vpn evpn"
986 elif ("router bgp" in running_ctx_keys
[0] and
987 len(running_ctx_keys
) > 2 and
988 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
989 running_ctx_keys
[2].startswith('vni ')):
990 lines_to_del
.append((running_ctx_keys
, None))
992 elif ("router bgp" in running_ctx_keys
[0] and
993 len(running_ctx_keys
) > 1 and
994 running_ctx_keys
[1].startswith('address-family')):
995 # There's no 'no address-family' support and so we have to
996 # delete each line individually again
997 for line
in running_ctx
.lines
:
998 lines_to_del
.append((running_ctx_keys
, line
))
1000 # Non-global context
1001 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
1002 lines_to_del
.append((running_ctx_keys
, None))
1004 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1005 lines_to_del
.append((running_ctx_keys
, None))
1009 for line
in running_ctx
.lines
:
1010 lines_to_del
.append((running_ctx_keys
, line
))
1012 # Find the lines within each context to add
1013 # Find the lines within each context to del
1014 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1016 if newconf_ctx_keys
in running
.contexts
:
1017 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1019 for line
in newconf_ctx
.lines
:
1020 if line
not in running_ctx
.dlines
:
1021 lines_to_add
.append((newconf_ctx_keys
, line
))
1023 for line
in running_ctx
.lines
:
1024 if line
not in newconf_ctx
.dlines
:
1025 lines_to_del
.append((newconf_ctx_keys
, line
))
1027 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1029 if newconf_ctx_keys
not in running
.contexts
:
1030 lines_to_add
.append((newconf_ctx_keys
, None))
1032 for line
in newconf_ctx
.lines
:
1033 lines_to_add
.append((newconf_ctx_keys
, line
))
1035 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
1036 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(lines_to_add
, lines_to_del
)
1038 return (lines_to_add
, lines_to_del
)
1042 def vtysh_config_available():
1044 Return False if no frr daemon is running or some other vtysh session is
1045 in 'configuration terminal' mode which will prevent us from making any
1046 configuration changes.
1050 cmd
= ['/usr/bin/vtysh', '-c', 'conf t']
1051 output
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
).strip()
1053 if 'VTY configuration is locked by other VTY' in output
.decode('utf-8'):
1055 log
.error("'%s' returned\n%s\n" % (' '.join(cmd
), output
))
1058 except subprocess
.CalledProcessError
as e
:
1059 msg
= "vtysh could not connect with any frr daemons"
1067 if __name__
== '__main__':
1068 # Command line options
1069 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
1070 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
1071 group
= parser
.add_mutually_exclusive_group(required
=True)
1072 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
1073 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
1074 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
1075 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
1076 parser
.add_argument('filename', help='Location of new frr config file')
1077 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
1078 args
= parser
.parse_args()
1081 # For --test log to stdout
1082 # For --reload log to /var/log/frr/frr-reload.log
1083 if args
.test
or args
.stdout
:
1084 logging
.basicConfig(level
=logging
.INFO
,
1085 format
='%(asctime)s %(levelname)5s: %(message)s')
1087 # Color the errors and warnings in red
1088 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
1089 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
1092 if not os
.path
.isdir('/var/log/frr/'):
1093 os
.makedirs('/var/log/frr/')
1095 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
1097 format
='%(asctime)s %(levelname)5s: %(message)s')
1099 # argparse should prevent this from happening but just to be safe...
1101 raise Exception('Must specify --reload or --test')
1102 log
= logging
.getLogger(__name__
)
1104 # Verify the new config file is valid
1105 if not os
.path
.isfile(args
.filename
):
1106 msg
= "Filename %s does not exist" % args
.filename
1111 if not os
.path
.getsize(args
.filename
):
1112 msg
= "Filename %s is an empty file" % args
.filename
1117 # Verify that 'service integrated-vtysh-config' is configured
1118 vtysh_filename
= '/etc/frr/vtysh.conf'
1119 service_integrated_vtysh_config
= True
1121 if os
.path
.isfile(vtysh_filename
):
1122 with
open(vtysh_filename
, 'r') as fh
:
1123 for line
in fh
.readlines():
1126 if line
== 'no service integrated-vtysh-config':
1127 service_integrated_vtysh_config
= False
1130 if not service_integrated_vtysh_config
:
1131 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1137 log
.setLevel(logging
.DEBUG
)
1139 log
.info('Called via "%s"', str(args
))
1141 # Create a Config object from the config generated by newconf
1143 newconf
.load_from_file(args
.filename
)
1148 # Create a Config object from the running config
1152 running
.load_from_file(args
.input)
1154 running
.load_from_show_running()
1156 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1157 lines_to_configure
= []
1160 print("\nLines To Delete")
1161 print("===============")
1163 for (ctx_keys
, line
) in lines_to_del
:
1168 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1169 lines_to_configure
.append(cmd
)
1173 print("\nLines To Add")
1174 print("============")
1176 for (ctx_keys
, line
) in lines_to_add
:
1181 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1182 lines_to_configure
.append(cmd
)
1187 # We will not be able to do anything, go ahead and exit(1)
1188 if not vtysh_config_available():
1191 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1193 # This looks a little odd but we have to do this twice...here is why
1194 # If the user had this running bgp config:
1197 # neighbor 1.1.1.1 remote-as 50
1198 # neighbor 1.1.1.1 route-map FOO out
1200 # and this config in the newconf config file
1203 # neighbor 1.1.1.1 remote-as 999
1204 # neighbor 1.1.1.1 route-map FOO out
1207 # Then the script will do
1208 # - no neighbor 1.1.1.1 remote-as 50
1209 # - neighbor 1.1.1.1 remote-as 999
1211 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1212 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1213 # configs again to put this line back.
1215 # There are many keywords in FRR that can only appear one time under
1216 # a context, take "bgp router-id" for example. If the config that we are
1217 # reloading against has the following:
1220 # bgp router-id 1.1.1.1
1221 # bgp router-id 2.2.2.2
1223 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1224 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1225 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1226 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1227 # second pass to include all of the "adds" from the first pass.
1228 lines_to_add_first_pass
= []
1232 running
.load_from_show_running()
1233 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1235 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1238 lines_to_add_first_pass
= lines_to_add
1240 lines_to_add
.extend(lines_to_add_first_pass
)
1242 # Only do deletes on the first pass. The reason being if we
1243 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1244 # will automatically add:
1247 # ipv6 nd ra-interval 10
1248 # no ipv6 nd suppress-ra
1251 # but those lines aren't in the config we are reloading against so
1252 # on the 2nd pass they will show up in lines_to_del. This could
1253 # apply to other scenarios as well where configuring FOO adds BAR
1255 if lines_to_del
and x
== 0:
1256 for (ctx_keys
, line
) in lines_to_del
:
1261 # 'no' commands are tricky, we can't just put them in a file and
1262 # vtysh -f that file. See the next comment for an explanation
1264 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1267 # Some commands in frr are picky about taking a "no" of the entire line.
1268 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1269 # only the beginning. If we hit one of these command an exception will be
1270 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1273 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1274 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1275 # % Unknown command.
1276 # frr(config-if)# no ip ospf authentication message-digest
1277 # % Unknown command.
1278 # frr(config-if)# no ip ospf authentication
1283 _
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
)
1285 except subprocess
.CalledProcessError
:
1287 # - Pull the last entry from cmd (this would be
1288 # 'no ip ospf authentication message-digest 1.1.1.1' in
1290 # - Split that last entry by whitespace and drop the last word
1291 log
.info('Failed to execute %s', ' '.join(cmd
))
1292 last_arg
= cmd
[-1].split(' ')
1294 if len(last_arg
) <= 2:
1295 log
.error('"%s" we failed to remove this command', original_cmd
)
1298 new_last_arg
= last_arg
[0:-1]
1299 cmd
[-1] = ' '.join(new_last_arg
)
1301 log
.info('Executed "%s"', ' '.join(cmd
))
1305 lines_to_configure
= []
1307 for (ctx_keys
, line
) in lines_to_add
:
1312 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1313 lines_to_configure
.append(cmd
)
1315 if lines_to_configure
:
1316 random_string
= ''.join(random
.SystemRandom().choice(
1317 string
.ascii_uppercase
+
1318 string
.digits
) for _
in range(6))
1320 filename
= "/var/run/frr/reload-%s.txt" % random_string
1321 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1323 with
open(filename
, 'w') as fh
:
1324 for line
in lines_to_configure
:
1325 fh
.write(line
+ '\n')
1328 subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
], stderr
=subprocess
.STDOUT
)
1329 except subprocess
.CalledProcessError
as e
:
1330 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1334 # Make these changes persistent
1335 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1336 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])