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, and "blackhole" must
304 be replaced by Null0 as well.
306 if (key
[0].startswith('ip route') or key
[0].startswith('ipv6 route') and
307 'null0' in key
[0] or 'blackhole' in key
[0]):
308 key
[0] = re
.sub(r
'\s+null0(\s*$)', ' Null0', key
[0])
309 key
[0] = re
.sub(r
'\s+blackhole(\s*$)', ' Null0', key
[0])
312 if tuple(key
) not in self
.contexts
:
313 ctx
= Context(tuple(key
), lines
)
314 self
.contexts
[tuple(key
)] = ctx
316 ctx
= self
.contexts
[tuple(key
)]
320 if tuple(key
) not in self
.contexts
:
321 ctx
= Context(tuple(key
), [])
322 self
.contexts
[tuple(key
)] = ctx
324 def load_contexts(self
):
326 Parse the configuration and create contexts for each appropriate block
329 current_context_lines
= []
333 The end of a context is flagged via the 'end' keyword:
342 bgp router-id 10.0.0.1
343 bgp log-neighbor-changes
344 no bgp default ipv4-unicast
345 neighbor EBGP peer-group
346 neighbor EBGP advertisement-interval 1
347 neighbor EBGP timers connect 10
348 neighbor 2001:40:1:4::6 remote-as 40
349 neighbor 2001:40:1:8::a remote-as 40
353 neighbor IBGPv6 activate
354 neighbor 2001:10::2 peer-group IBGPv6
355 neighbor 2001:10::3 peer-group IBGPv6
360 neighbor LEAF activate
364 route-target import 10.1.1.1:10100
365 route-target export 10.1.1.1:10100
371 ospf router-id 10.0.0.1
372 log-adjacency-changes detail
373 timers throttle spf 0 50 5000
378 # The code assumes that its working on the output from the "vtysh -m"
379 # command. That provides the appropriate markers to signify end of
380 # a context. This routine uses that to build the contexts for the
383 # There are single line contexts such as "log file /media/node/zebra.log"
384 # and multi-line contexts such as "router ospf" and subcontexts
385 # within a context such as "address-family" within "router bgp"
386 # In each of these cases, the first line of the context becomes the
387 # key of the context. So "router bgp 10" is the key for the non-address
388 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
389 # the key for the subcontext and so on.
394 # the keywords that we know are single line contexts. bgp in this case
395 # is not the main router bgp block, but enabling multi-instance
396 oneline_ctx_keywords
= ("access-list ",
417 for line
in self
.lines
:
422 if line
.startswith('!') or line
.startswith('#'):
426 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
427 self
.save_contexts(ctx_keys
, current_context_lines
)
429 # Start a new context
432 current_context_lines
= []
434 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
435 self
.save_contexts(ctx_keys
, current_context_lines
)
438 elif line
in ["end", "exit-vrf"]:
439 self
.save_contexts(ctx_keys
, current_context_lines
)
440 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
442 # Start a new context
446 current_context_lines
= []
448 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
449 # if this exit is for address-family ipv4 unicast, ignore the pop
451 self
.save_contexts(ctx_keys
, current_context_lines
)
453 # Start a new context
454 ctx_keys
= copy
.deepcopy(main_ctx_key
)
455 current_context_lines
= []
456 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
458 elif line
== "exit-vni":
460 self
.save_contexts(ctx_keys
, current_context_lines
)
462 # Start a new context
463 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
464 current_context_lines
= []
465 log
.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line
, ctx_keys
)
467 elif new_ctx
is True:
471 ctx_keys
= copy
.deepcopy(main_ctx_key
)
474 current_context_lines
= []
476 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
477 elif (line
.startswith("address-family ") or
478 line
.startswith("vnc defaults") or
479 line
.startswith("vnc l2-group") or
480 line
.startswith("vnc nve-group")):
483 # Save old context first
484 self
.save_contexts(ctx_keys
, current_context_lines
)
485 current_context_lines
= []
486 main_ctx_key
= copy
.deepcopy(ctx_keys
)
487 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
489 if line
== "address-family ipv6":
490 ctx_keys
.append("address-family ipv6 unicast")
491 elif line
== "address-family ipv4":
492 ctx_keys
.append("address-family ipv4 unicast")
493 elif line
== "address-family evpn":
494 ctx_keys
.append("address-family l2vpn evpn")
496 ctx_keys
.append(line
)
498 elif ((line
.startswith("vni ") and
499 len(ctx_keys
) == 2 and
500 ctx_keys
[0].startswith('router bgp') and
501 ctx_keys
[1] == 'address-family l2vpn evpn')):
503 # Save old context first
504 self
.save_contexts(ctx_keys
, current_context_lines
)
505 current_context_lines
= []
506 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
507 log
.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line
)
508 ctx_keys
.append(line
)
511 # Continuing in an existing context, add non-commented lines to it
512 current_context_lines
.append(line
)
513 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
515 # Save the context of the last one
516 self
.save_contexts(ctx_keys
, current_context_lines
)
519 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
521 Return the vtysh command for the specified context line
530 for ctx_key
in ctx_keys
:
539 if line
.startswith('no '):
540 cmd
.append('%s' % line
[3:])
542 cmd
.append('no %s' % line
)
548 # If line is None then we are typically deleting an entire
549 # context ('no router ospf' for example)
554 # Only put the 'no' on the last sub-context
555 for ctx_key
in ctx_keys
:
558 if ctx_key
== ctx_keys
[-1]:
559 cmd
.append('no %s' % ctx_key
)
561 cmd
.append('%s' % ctx_key
)
563 for ctx_key
in ctx_keys
:
570 def line_for_vtysh_file(ctx_keys
, line
, delete
):
572 Return the command as it would appear in frr.conf
577 for (i
, ctx_key
) in enumerate(ctx_keys
):
578 cmd
.append(' ' * i
+ ctx_key
)
581 indent
= len(ctx_keys
) * ' '
584 if line
.startswith('no '):
585 cmd
.append('%s%s' % (indent
, line
[3:]))
587 cmd
.append('%sno %s' % (indent
, line
))
590 cmd
.append(indent
+ line
)
592 # If line is None then we are typically deleting an entire
593 # context ('no router ospf' for example)
597 # Only put the 'no' on the last sub-context
598 for ctx_key
in ctx_keys
:
600 if ctx_key
== ctx_keys
[-1]:
601 cmd
.append('no %s' % ctx_key
)
603 cmd
.append('%s' % ctx_key
)
605 for ctx_key
in ctx_keys
:
608 cmd
= '\n' + '\n'.join(cmd
)
610 # There are some commands that are on by default so their "no" form will be
611 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
612 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
613 # not by doing a "no no bgp default ipv4-unicast"
614 cmd
= cmd
.replace('no no ', '')
619 def get_normalized_ipv6_line(line
):
621 Return a normalized IPv6 line as produced by frr,
622 with all letters in lower case and trailing and leading
623 zeros removed, and only the network portion present if
624 the IPv6 word is a network
627 words
= line
.split(' ')
633 if 'ipaddress' not in sys
.modules
:
634 v6word
= IPNetwork(word
)
635 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
637 v6word
= ip_network(word
, strict
=False)
638 norm_word
= '%s/%s' % (str(v6word
.network_address
), v6word
.prefixlen
)
643 norm_word
= '%s' % IPv6Address(word
)
648 norm_line
= norm_line
+ " " + norm_word
650 return norm_line
.strip()
653 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
654 for (ctx_keys
, line
) in lines
:
655 if ctx_keys
== target_ctx_keys
:
657 if line
== target_line
:
660 if line
.startswith(target_line
):
665 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
667 # Quite possibly the most confusing (while accurate) variable names in history
668 lines_to_add_to_del
= []
669 lines_to_del_to_del
= []
671 for (ctx_keys
, line
) in lines_to_del
:
674 if ctx_keys
[0].startswith('router bgp') and line
:
676 if line
.startswith('neighbor '):
678 BGP changed how it displays swpX peers that are part of peer-group. Older
679 versions of frr would display these on separate lines:
680 neighbor swp1 interface
681 neighbor swp1 peer-group FOO
683 but today we display via a single line
684 neighbor swp1 interface peer-group FOO
686 This change confuses frr-reload.py so check to see if we are deleting
687 neighbor swp1 interface peer-group FOO
690 neighbor swp1 interface
691 neighbor swp1 peer-group FOO
693 If so then chop the del line and the corresponding add lines
696 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
697 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
699 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
700 swpx_interface
= None
701 swpx_peergroup
= None
703 if re_swpx_int_peergroup
:
704 swpx
= re_swpx_int_peergroup
.group(1)
705 peergroup
= re_swpx_int_peergroup
.group(2)
706 swpx_interface
= "neighbor %s interface" % swpx
707 elif re_swpx_int_v6only_peergroup
:
708 swpx
= re_swpx_int_v6only_peergroup
.group(1)
709 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
710 swpx_interface
= "neighbor %s interface v6only" % swpx
712 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
713 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
714 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
715 tmp_ctx_keys
= tuple(list(ctx_keys
))
717 if not found_add_swpx_peergroup
:
718 tmp_ctx_keys
= list(ctx_keys
)
719 tmp_ctx_keys
.append('address-family ipv4 unicast')
720 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
721 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
723 if not found_add_swpx_peergroup
:
724 tmp_ctx_keys
= list(ctx_keys
)
725 tmp_ctx_keys
.append('address-family ipv6 unicast')
726 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
727 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
729 if found_add_swpx_interface
and found_add_swpx_peergroup
:
731 lines_to_del_to_del
.append((ctx_keys
, line
))
732 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
733 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
736 We changed how we display the neighbor interface command. Older
737 versions of frr would display the following:
738 neighbor swp1 interface
739 neighbor swp1 remote-as external
740 neighbor swp1 capability extended-nexthop
742 but today we display via a single line
743 neighbor swp1 interface remote-as external
745 and capability extended-nexthop is no longer needed because we
746 automatically enable it when the neighbor is of type interface.
748 This change confuses frr-reload.py so check to see if we are deleting
749 neighbor swp1 interface remote-as (external|internal|ASNUM)
752 neighbor swp1 interface
753 neighbor swp1 remote-as (external|internal|ASNUM)
754 neighbor swp1 capability extended-nexthop
756 If so then chop the del line and the corresponding add lines
758 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
759 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
761 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
762 swpx_interface
= None
765 if re_swpx_int_remoteas
:
766 swpx
= re_swpx_int_remoteas
.group(1)
767 remoteas
= re_swpx_int_remoteas
.group(2)
768 swpx_interface
= "neighbor %s interface" % swpx
769 elif re_swpx_int_v6only_remoteas
:
770 swpx
= re_swpx_int_v6only_remoteas
.group(1)
771 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
772 swpx_interface
= "neighbor %s interface v6only" % swpx
774 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
775 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
776 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
777 tmp_ctx_keys
= tuple(list(ctx_keys
))
779 if found_add_swpx_interface
and found_add_swpx_remoteas
:
781 lines_to_del_to_del
.append((ctx_keys
, line
))
782 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
783 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
786 We made the 'bgp bestpath as-path multipath-relax' command
787 automatically assume 'no-as-set' since the lack of this option caused
788 weird routing problems. When the running config is shown in
789 releases with this change, the no-as-set keyword is not shown as it
790 is the default. This causes frr-reload to unnecessarily unapply
791 this option only to apply it back again, causing unnecessary session
794 if 'multipath-relax' in line
:
795 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
796 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
797 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
799 if re_asrelax_new
and found_asrelax_old
:
801 lines_to_del_to_del
.append((ctx_keys
, line
))
802 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
805 If we are modifying the BGP table-map we need to avoid a del/add and
806 instead modify the table-map in place via an add. This is needed to
807 avoid installing all routes in the RIB the second the 'no table-map'
810 if line
.startswith('table-map'):
811 found_table_map
= line_exist(lines_to_add
, ctx_keys
, 'table-map', False)
814 lines_to_del_to_del
.append((ctx_keys
, line
))
817 More old-to-new config handling. ip import-table no longer accepts
818 distance, but we honor the old syntax. But 'show running' shows only
819 the new syntax. This causes an unnecessary 'no import-table' followed
820 by the same old 'ip import-table' which causes perturbations in
821 announced routes leading to traffic blackholes. Fix this issue.
823 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
825 table_num
= re_importtbl
.group(1)
826 for ctx
in lines_to_add
:
827 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
828 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
829 lines_to_add_to_del
.append((ctx
[0], None))
832 ip/ipv6 prefix-list can be specified without a seq number. However,
833 the running config always adds 'seq x', where x is a number incremented
834 by 5 for every element, to the prefix list. So, ignore such lines as
835 well. Sample prefix-list lines:
836 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
837 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
838 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
840 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
843 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
844 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
845 re_ip_pfxlst
.group(6))
846 for ctx
in lines_to_add
:
847 if ctx
[0][0] == tmpline
:
848 lines_to_del_to_del
.append((ctx_keys
, None))
849 lines_to_add_to_del
.append(((tmpline
,), None))
851 if (len(ctx_keys
) == 3 and
852 ctx_keys
[0].startswith('router bgp') and
853 ctx_keys
[1] == 'address-family l2vpn evpn' and
854 ctx_keys
[2].startswith('vni')):
856 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
859 rt
= re_route_target
.group(1).strip()
860 route_target_import_line
= line
861 route_target_export_line
= "route-target export %s" % rt
862 route_target_both_line
= "route-target both %s" % rt
864 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
865 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
868 If the running configs has
869 route-target import 1:1
870 route-target export 1:1
872 and the config we are reloading against has
873 route-target both 1:1
875 then we can ignore deleting the import/export and ignore adding the 'both'
877 if found_route_target_export_line
and found_route_target_both_line
:
878 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
879 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
880 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
883 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
886 lines_to_del_to_del
.append((ctx_keys
, line
))
887 lines_to_add_to_del
.append((ctx_keys
, line
))
890 We have commands that used to be displayed in the global part
891 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
895 neighbor ISL advertisement-interval 0
901 address-family ipv4 unicast
902 neighbor ISL advertisement-interval 0
904 Look to see if we are deleting it in one format just to add it back in the other
906 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
907 tmp_ctx_keys
= list(ctx_keys
)[:-1]
908 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
910 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
913 lines_to_del_to_del
.append((ctx_keys
, line
))
914 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
916 for (ctx_keys
, line
) in lines_to_del_to_del
:
917 lines_to_del
.remove((ctx_keys
, line
))
919 for (ctx_keys
, line
) in lines_to_add_to_del
:
920 lines_to_add
.remove((ctx_keys
, line
))
922 return (lines_to_add
, lines_to_del
)
925 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
927 There are certain commands that cannot be removed. Remove
928 those commands from lines_to_del.
930 lines_to_del_to_del
= []
932 for (ctx_keys
, line
) in lines_to_del
:
934 if (ctx_keys
[0].startswith('frr version') or
935 ctx_keys
[0].startswith('frr defaults') or
936 ctx_keys
[0].startswith('password') or
937 ctx_keys
[0].startswith('line vty') or
939 # This is technically "no"able but if we did so frr-reload would
940 # stop working so do not let the user shoot themselves in the foot
942 ctx_keys
[0].startswith('service integrated-vtysh-config')):
944 log
.info("(%s, %s) cannot be removed" % (pformat(ctx_keys
), line
))
945 lines_to_del_to_del
.append((ctx_keys
, line
))
947 for (ctx_keys
, line
) in lines_to_del_to_del
:
948 lines_to_del
.remove((ctx_keys
, line
))
950 return (lines_to_add
, lines_to_del
)
953 def compare_context_objects(newconf
, running
):
955 Create a context diff for the two specified contexts
958 # Compare the two Config objects to find the lines that we need to add/del
963 # Find contexts that are in newconf but not in running
964 # Find contexts that are in running but not in newconf
965 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
967 if running_ctx_keys
not in newconf
.contexts
:
969 # We check that the len is 1 here so that we only look at ('router bgp 10')
970 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
971 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
972 # running but not in newconf.
973 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
975 lines_to_del
.append((running_ctx_keys
, None))
977 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
978 elif running_ctx_keys
[0].startswith('interface') or running_ctx_keys
[0].startswith('vrf'):
979 for line
in running_ctx
.lines
:
980 lines_to_del
.append((running_ctx_keys
, line
))
982 # If this is an address-family under 'router bgp' and we are already deleting the
983 # entire 'router bgp' context then ignore this sub-context
984 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
987 # Delete an entire vni sub-context under "address-family l2vpn evpn"
988 elif ("router bgp" in running_ctx_keys
[0] and
989 len(running_ctx_keys
) > 2 and
990 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
991 running_ctx_keys
[2].startswith('vni ')):
992 lines_to_del
.append((running_ctx_keys
, None))
994 elif ("router bgp" in running_ctx_keys
[0] and
995 len(running_ctx_keys
) > 1 and
996 running_ctx_keys
[1].startswith('address-family')):
997 # There's no 'no address-family' support and so we have to
998 # delete each line individually again
999 for line
in running_ctx
.lines
:
1000 lines_to_del
.append((running_ctx_keys
, line
))
1002 # Non-global context
1003 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
1004 lines_to_del
.append((running_ctx_keys
, None))
1006 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1007 lines_to_del
.append((running_ctx_keys
, None))
1011 for line
in running_ctx
.lines
:
1012 lines_to_del
.append((running_ctx_keys
, line
))
1014 # Find the lines within each context to add
1015 # Find the lines within each context to del
1016 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1018 if newconf_ctx_keys
in running
.contexts
:
1019 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1021 for line
in newconf_ctx
.lines
:
1022 if line
not in running_ctx
.dlines
:
1023 lines_to_add
.append((newconf_ctx_keys
, line
))
1025 for line
in running_ctx
.lines
:
1026 if line
not in newconf_ctx
.dlines
:
1027 lines_to_del
.append((newconf_ctx_keys
, line
))
1029 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1031 if newconf_ctx_keys
not in running
.contexts
:
1032 lines_to_add
.append((newconf_ctx_keys
, None))
1034 for line
in newconf_ctx
.lines
:
1035 lines_to_add
.append((newconf_ctx_keys
, line
))
1037 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
1038 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(lines_to_add
, lines_to_del
)
1040 return (lines_to_add
, lines_to_del
)
1044 def vtysh_config_available():
1046 Return False if no frr daemon is running or some other vtysh session is
1047 in 'configuration terminal' mode which will prevent us from making any
1048 configuration changes.
1052 cmd
= ['/usr/bin/vtysh', '-c', 'conf t']
1053 output
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
).strip()
1055 if 'VTY configuration is locked by other VTY' in output
.decode('utf-8'):
1057 log
.error("'%s' returned\n%s\n" % (' '.join(cmd
), output
))
1060 except subprocess
.CalledProcessError
as e
:
1061 msg
= "vtysh could not connect with any frr daemons"
1069 if __name__
== '__main__':
1070 # Command line options
1071 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
1072 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
1073 group
= parser
.add_mutually_exclusive_group(required
=True)
1074 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
1075 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
1076 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
1077 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
1078 parser
.add_argument('filename', help='Location of new frr config file')
1079 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
1080 args
= parser
.parse_args()
1083 # For --test log to stdout
1084 # For --reload log to /var/log/frr/frr-reload.log
1085 if args
.test
or args
.stdout
:
1086 logging
.basicConfig(level
=logging
.INFO
,
1087 format
='%(asctime)s %(levelname)5s: %(message)s')
1089 # Color the errors and warnings in red
1090 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
1091 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
1094 if not os
.path
.isdir('/var/log/frr/'):
1095 os
.makedirs('/var/log/frr/')
1097 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
1099 format
='%(asctime)s %(levelname)5s: %(message)s')
1101 # argparse should prevent this from happening but just to be safe...
1103 raise Exception('Must specify --reload or --test')
1104 log
= logging
.getLogger(__name__
)
1106 # Verify the new config file is valid
1107 if not os
.path
.isfile(args
.filename
):
1108 msg
= "Filename %s does not exist" % args
.filename
1113 if not os
.path
.getsize(args
.filename
):
1114 msg
= "Filename %s is an empty file" % args
.filename
1119 # Verify that 'service integrated-vtysh-config' is configured
1120 vtysh_filename
= '/etc/frr/vtysh.conf'
1121 service_integrated_vtysh_config
= True
1123 if os
.path
.isfile(vtysh_filename
):
1124 with
open(vtysh_filename
, 'r') as fh
:
1125 for line
in fh
.readlines():
1128 if line
== 'no service integrated-vtysh-config':
1129 service_integrated_vtysh_config
= False
1132 if not service_integrated_vtysh_config
:
1133 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1139 log
.setLevel(logging
.DEBUG
)
1141 log
.info('Called via "%s"', str(args
))
1143 # Create a Config object from the config generated by newconf
1145 newconf
.load_from_file(args
.filename
)
1150 # Create a Config object from the running config
1154 running
.load_from_file(args
.input)
1156 running
.load_from_show_running()
1158 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1159 lines_to_configure
= []
1162 print("\nLines To Delete")
1163 print("===============")
1165 for (ctx_keys
, line
) in lines_to_del
:
1170 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1171 lines_to_configure
.append(cmd
)
1175 print("\nLines To Add")
1176 print("============")
1178 for (ctx_keys
, line
) in lines_to_add
:
1183 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1184 lines_to_configure
.append(cmd
)
1189 # We will not be able to do anything, go ahead and exit(1)
1190 if not vtysh_config_available():
1193 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1195 # This looks a little odd but we have to do this twice...here is why
1196 # If the user had this running bgp config:
1199 # neighbor 1.1.1.1 remote-as 50
1200 # neighbor 1.1.1.1 route-map FOO out
1202 # and this config in the newconf config file
1205 # neighbor 1.1.1.1 remote-as 999
1206 # neighbor 1.1.1.1 route-map FOO out
1209 # Then the script will do
1210 # - no neighbor 1.1.1.1 remote-as 50
1211 # - neighbor 1.1.1.1 remote-as 999
1213 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1214 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1215 # configs again to put this line back.
1217 # There are many keywords in FRR that can only appear one time under
1218 # a context, take "bgp router-id" for example. If the config that we are
1219 # reloading against has the following:
1222 # bgp router-id 1.1.1.1
1223 # bgp router-id 2.2.2.2
1225 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1226 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1227 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1228 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1229 # second pass to include all of the "adds" from the first pass.
1230 lines_to_add_first_pass
= []
1234 running
.load_from_show_running()
1235 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1237 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1240 lines_to_add_first_pass
= lines_to_add
1242 lines_to_add
.extend(lines_to_add_first_pass
)
1244 # Only do deletes on the first pass. The reason being if we
1245 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1246 # will automatically add:
1249 # ipv6 nd ra-interval 10
1250 # no ipv6 nd suppress-ra
1253 # but those lines aren't in the config we are reloading against so
1254 # on the 2nd pass they will show up in lines_to_del. This could
1255 # apply to other scenarios as well where configuring FOO adds BAR
1257 if lines_to_del
and x
== 0:
1258 for (ctx_keys
, line
) in lines_to_del
:
1263 # 'no' commands are tricky, we can't just put them in a file and
1264 # vtysh -f that file. See the next comment for an explanation
1266 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1269 # Some commands in frr are picky about taking a "no" of the entire line.
1270 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1271 # only the beginning. If we hit one of these command an exception will be
1272 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1275 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1276 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1277 # % Unknown command.
1278 # frr(config-if)# no ip ospf authentication message-digest
1279 # % Unknown command.
1280 # frr(config-if)# no ip ospf authentication
1285 _
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
)
1287 except subprocess
.CalledProcessError
:
1289 # - Pull the last entry from cmd (this would be
1290 # 'no ip ospf authentication message-digest 1.1.1.1' in
1292 # - Split that last entry by whitespace and drop the last word
1293 log
.info('Failed to execute %s', ' '.join(cmd
))
1294 last_arg
= cmd
[-1].split(' ')
1296 if len(last_arg
) <= 2:
1297 log
.error('"%s" we failed to remove this command', original_cmd
)
1300 new_last_arg
= last_arg
[0:-1]
1301 cmd
[-1] = ' '.join(new_last_arg
)
1303 log
.info('Executed "%s"', ' '.join(cmd
))
1307 lines_to_configure
= []
1309 for (ctx_keys
, line
) in lines_to_add
:
1314 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1315 lines_to_configure
.append(cmd
)
1317 if lines_to_configure
:
1318 random_string
= ''.join(random
.SystemRandom().choice(
1319 string
.ascii_uppercase
+
1320 string
.digits
) for _
in range(6))
1322 filename
= "/var/run/frr/reload-%s.txt" % random_string
1323 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1325 with
open(filename
, 'w') as fh
:
1326 for line
in lines_to_configure
:
1327 fh
.write(line
+ '\n')
1330 subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
], stderr
=subprocess
.STDOUT
)
1331 except subprocess
.CalledProcessError
as e
:
1332 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1336 # Make these changes persistent
1337 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1338 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])