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 ",
414 "vrrp autoconfigure")
416 for line
in self
.lines
:
421 if line
.startswith('!') or line
.startswith('#'):
425 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
426 self
.save_contexts(ctx_keys
, current_context_lines
)
428 # Start a new context
431 current_context_lines
= []
433 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
434 self
.save_contexts(ctx_keys
, current_context_lines
)
438 self
.save_contexts(ctx_keys
, current_context_lines
)
439 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
441 # Start a new context
445 current_context_lines
= []
447 elif line
== "exit-vrf":
448 self
.save_contexts(ctx_keys
, current_context_lines
)
449 current_context_lines
.append(line
)
450 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
456 current_context_lines
= []
458 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
459 # if this exit is for address-family ipv4 unicast, ignore the pop
461 self
.save_contexts(ctx_keys
, current_context_lines
)
463 # Start a new context
464 ctx_keys
= copy
.deepcopy(main_ctx_key
)
465 current_context_lines
= []
466 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
468 elif line
== "exit-vni":
470 self
.save_contexts(ctx_keys
, current_context_lines
)
472 # Start a new context
473 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
474 current_context_lines
= []
475 log
.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line
, ctx_keys
)
477 elif new_ctx
is True:
481 ctx_keys
= copy
.deepcopy(main_ctx_key
)
484 current_context_lines
= []
486 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
487 elif (line
.startswith("address-family ") or
488 line
.startswith("vnc defaults") or
489 line
.startswith("vnc l2-group") or
490 line
.startswith("vnc nve-group")):
493 # Save old context first
494 self
.save_contexts(ctx_keys
, current_context_lines
)
495 current_context_lines
= []
496 main_ctx_key
= copy
.deepcopy(ctx_keys
)
497 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
499 if line
== "address-family ipv6":
500 ctx_keys
.append("address-family ipv6 unicast")
501 elif line
== "address-family ipv4":
502 ctx_keys
.append("address-family ipv4 unicast")
503 elif line
== "address-family evpn":
504 ctx_keys
.append("address-family l2vpn evpn")
506 ctx_keys
.append(line
)
508 elif ((line
.startswith("vni ") and
509 len(ctx_keys
) == 2 and
510 ctx_keys
[0].startswith('router bgp') and
511 ctx_keys
[1] == 'address-family l2vpn evpn')):
513 # Save old context first
514 self
.save_contexts(ctx_keys
, current_context_lines
)
515 current_context_lines
= []
516 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
517 log
.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line
)
518 ctx_keys
.append(line
)
521 # Continuing in an existing context, add non-commented lines to it
522 current_context_lines
.append(line
)
523 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
525 # Save the context of the last one
526 self
.save_contexts(ctx_keys
, current_context_lines
)
529 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
531 Return the vtysh command for the specified context line
540 for ctx_key
in ctx_keys
:
549 if line
.startswith('no '):
550 cmd
.append('%s' % line
[3:])
552 cmd
.append('no %s' % line
)
558 # If line is None then we are typically deleting an entire
559 # context ('no router ospf' for example)
564 # Only put the 'no' on the last sub-context
565 for ctx_key
in ctx_keys
:
568 if ctx_key
== ctx_keys
[-1]:
569 cmd
.append('no %s' % ctx_key
)
571 cmd
.append('%s' % ctx_key
)
573 for ctx_key
in ctx_keys
:
580 def line_for_vtysh_file(ctx_keys
, line
, delete
):
582 Return the command as it would appear in frr.conf
587 for (i
, ctx_key
) in enumerate(ctx_keys
):
588 cmd
.append(' ' * i
+ ctx_key
)
591 indent
= len(ctx_keys
) * ' '
594 if line
.startswith('no '):
595 cmd
.append('%s%s' % (indent
, line
[3:]))
597 cmd
.append('%sno %s' % (indent
, line
))
600 cmd
.append(indent
+ line
)
602 # If line is None then we are typically deleting an entire
603 # context ('no router ospf' for example)
607 # Only put the 'no' on the last sub-context
608 for ctx_key
in ctx_keys
:
610 if ctx_key
== ctx_keys
[-1]:
611 cmd
.append('no %s' % ctx_key
)
613 cmd
.append('%s' % ctx_key
)
615 for ctx_key
in ctx_keys
:
618 cmd
= '\n' + '\n'.join(cmd
)
620 # There are some commands that are on by default so their "no" form will be
621 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
622 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
623 # not by doing a "no no bgp default ipv4-unicast"
624 cmd
= cmd
.replace('no no ', '')
629 def get_normalized_ipv6_line(line
):
631 Return a normalized IPv6 line as produced by frr,
632 with all letters in lower case and trailing and leading
633 zeros removed, and only the network portion present if
634 the IPv6 word is a network
637 words
= line
.split(' ')
643 if 'ipaddress' not in sys
.modules
:
644 v6word
= IPNetwork(word
)
645 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
647 v6word
= ip_network(word
, strict
=False)
648 norm_word
= '%s/%s' % (str(v6word
.network_address
), v6word
.prefixlen
)
653 norm_word
= '%s' % IPv6Address(word
)
658 norm_line
= norm_line
+ " " + norm_word
660 return norm_line
.strip()
663 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
664 for (ctx_keys
, line
) in lines
:
665 if ctx_keys
== target_ctx_keys
:
667 if line
== target_line
:
670 if line
.startswith(target_line
):
675 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
677 # Quite possibly the most confusing (while accurate) variable names in history
678 lines_to_add_to_del
= []
679 lines_to_del_to_del
= []
681 for (ctx_keys
, line
) in lines_to_del
:
684 if ctx_keys
[0].startswith('router bgp') and line
:
686 if line
.startswith('neighbor '):
688 BGP changed how it displays swpX peers that are part of peer-group. Older
689 versions of frr would display these on separate lines:
690 neighbor swp1 interface
691 neighbor swp1 peer-group FOO
693 but today we display via a single line
694 neighbor swp1 interface peer-group FOO
696 This change confuses frr-reload.py so check to see if we are deleting
697 neighbor swp1 interface peer-group FOO
700 neighbor swp1 interface
701 neighbor swp1 peer-group FOO
703 If so then chop the del line and the corresponding add lines
706 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
707 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
709 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
710 swpx_interface
= None
711 swpx_peergroup
= None
713 if re_swpx_int_peergroup
:
714 swpx
= re_swpx_int_peergroup
.group(1)
715 peergroup
= re_swpx_int_peergroup
.group(2)
716 swpx_interface
= "neighbor %s interface" % swpx
717 elif re_swpx_int_v6only_peergroup
:
718 swpx
= re_swpx_int_v6only_peergroup
.group(1)
719 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
720 swpx_interface
= "neighbor %s interface v6only" % swpx
722 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
723 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
724 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
725 tmp_ctx_keys
= tuple(list(ctx_keys
))
727 if not found_add_swpx_peergroup
:
728 tmp_ctx_keys
= list(ctx_keys
)
729 tmp_ctx_keys
.append('address-family ipv4 unicast')
730 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
731 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
733 if not found_add_swpx_peergroup
:
734 tmp_ctx_keys
= list(ctx_keys
)
735 tmp_ctx_keys
.append('address-family ipv6 unicast')
736 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
737 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
739 if found_add_swpx_interface
and found_add_swpx_peergroup
:
741 lines_to_del_to_del
.append((ctx_keys
, line
))
742 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
743 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
746 Changing the bfd timers on neighbors is allowed without doing
747 a delete/add process. Since doing a "no neighbor blah bfd ..."
748 will cause the peer to bounce unnecessarily, just skip the delete
751 re_nbr_bfd_timers
= re
.search(r
'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line
)
753 if re_nbr_bfd_timers
:
754 nbr
= re_nbr_bfd_timers
.group(1)
755 bfd_nbr
= "neighbor %s" % nbr
757 for (ctx_keys
, add_line
) in lines_to_add
:
758 re_add_nbr_bfd_timers
= re
.search(r
'neighbor (\S+) bfd (\S+) (\S+) (\S+)', add_line
)
760 if re_add_nbr_bfd_timers
:
761 found_add_bfd_nbr
= line_exist(lines_to_add
, ctx_keys
, bfd_nbr
, False)
763 if found_add_bfd_nbr
:
764 lines_to_del_to_del
.append((ctx_keys
, line
))
767 We changed how we display the neighbor interface command. Older
768 versions of frr would display the following:
769 neighbor swp1 interface
770 neighbor swp1 remote-as external
771 neighbor swp1 capability extended-nexthop
773 but today we display via a single line
774 neighbor swp1 interface remote-as external
776 and capability extended-nexthop is no longer needed because we
777 automatically enable it when the neighbor is of type interface.
779 This change confuses frr-reload.py so check to see if we are deleting
780 neighbor swp1 interface remote-as (external|internal|ASNUM)
783 neighbor swp1 interface
784 neighbor swp1 remote-as (external|internal|ASNUM)
785 neighbor swp1 capability extended-nexthop
787 If so then chop the del line and the corresponding add lines
789 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
790 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
792 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
793 swpx_interface
= None
796 if re_swpx_int_remoteas
:
797 swpx
= re_swpx_int_remoteas
.group(1)
798 remoteas
= re_swpx_int_remoteas
.group(2)
799 swpx_interface
= "neighbor %s interface" % swpx
800 elif re_swpx_int_v6only_remoteas
:
801 swpx
= re_swpx_int_v6only_remoteas
.group(1)
802 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
803 swpx_interface
= "neighbor %s interface v6only" % swpx
805 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
806 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
807 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
808 tmp_ctx_keys
= tuple(list(ctx_keys
))
810 if found_add_swpx_interface
and found_add_swpx_remoteas
:
812 lines_to_del_to_del
.append((ctx_keys
, line
))
813 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
814 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
817 We made the 'bgp bestpath as-path multipath-relax' command
818 automatically assume 'no-as-set' since the lack of this option caused
819 weird routing problems. When the running config is shown in
820 releases with this change, the no-as-set keyword is not shown as it
821 is the default. This causes frr-reload to unnecessarily unapply
822 this option only to apply it back again, causing unnecessary session
825 if 'multipath-relax' in line
:
826 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
827 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
828 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
830 if re_asrelax_new
and found_asrelax_old
:
832 lines_to_del_to_del
.append((ctx_keys
, line
))
833 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
836 If we are modifying the BGP table-map we need to avoid a del/add and
837 instead modify the table-map in place via an add. This is needed to
838 avoid installing all routes in the RIB the second the 'no table-map'
841 if line
.startswith('table-map'):
842 found_table_map
= line_exist(lines_to_add
, ctx_keys
, 'table-map', False)
845 lines_to_del_to_del
.append((ctx_keys
, line
))
848 More old-to-new config handling. ip import-table no longer accepts
849 distance, but we honor the old syntax. But 'show running' shows only
850 the new syntax. This causes an unnecessary 'no import-table' followed
851 by the same old 'ip import-table' which causes perturbations in
852 announced routes leading to traffic blackholes. Fix this issue.
854 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
856 table_num
= re_importtbl
.group(1)
857 for ctx
in lines_to_add
:
858 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
859 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
860 lines_to_add_to_del
.append((ctx
[0], None))
863 ip/ipv6 prefix-list can be specified without a seq number. However,
864 the running config always adds 'seq x', where x is a number incremented
865 by 5 for every element, to the prefix list. So, ignore such lines as
866 well. Sample prefix-list lines:
867 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
868 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
869 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
871 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
874 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
875 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
876 re_ip_pfxlst
.group(6))
877 for ctx
in lines_to_add
:
878 if ctx
[0][0] == tmpline
:
879 lines_to_del_to_del
.append((ctx_keys
, None))
880 lines_to_add_to_del
.append(((tmpline
,), None))
882 if (len(ctx_keys
) == 3 and
883 ctx_keys
[0].startswith('router bgp') and
884 ctx_keys
[1] == 'address-family l2vpn evpn' and
885 ctx_keys
[2].startswith('vni')):
887 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
890 rt
= re_route_target
.group(1).strip()
891 route_target_import_line
= line
892 route_target_export_line
= "route-target export %s" % rt
893 route_target_both_line
= "route-target both %s" % rt
895 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
896 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
899 If the running configs has
900 route-target import 1:1
901 route-target export 1:1
903 and the config we are reloading against has
904 route-target both 1:1
906 then we can ignore deleting the import/export and ignore adding the 'both'
908 if found_route_target_export_line
and found_route_target_both_line
:
909 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
910 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
911 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
914 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
917 lines_to_del_to_del
.append((ctx_keys
, line
))
918 lines_to_add_to_del
.append((ctx_keys
, line
))
921 We have commands that used to be displayed in the global part
922 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
926 neighbor ISL advertisement-interval 0
932 address-family ipv4 unicast
933 neighbor ISL advertisement-interval 0
935 Look to see if we are deleting it in one format just to add it back in the other
937 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
938 tmp_ctx_keys
= list(ctx_keys
)[:-1]
939 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
941 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
944 lines_to_del_to_del
.append((ctx_keys
, line
))
945 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
947 for (ctx_keys
, line
) in lines_to_del_to_del
:
948 lines_to_del
.remove((ctx_keys
, line
))
950 for (ctx_keys
, line
) in lines_to_add_to_del
:
951 lines_to_add
.remove((ctx_keys
, line
))
953 return (lines_to_add
, lines_to_del
)
956 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
958 There are certain commands that cannot be removed. Remove
959 those commands from lines_to_del.
961 lines_to_del_to_del
= []
963 for (ctx_keys
, line
) in lines_to_del
:
965 if (ctx_keys
[0].startswith('frr version') or
966 ctx_keys
[0].startswith('frr defaults') or
967 ctx_keys
[0].startswith('password') or
968 ctx_keys
[0].startswith('line vty') or
970 # This is technically "no"able but if we did so frr-reload would
971 # stop working so do not let the user shoot themselves in the foot
973 ctx_keys
[0].startswith('service integrated-vtysh-config')):
975 log
.info("(%s, %s) cannot be removed" % (pformat(ctx_keys
), line
))
976 lines_to_del_to_del
.append((ctx_keys
, line
))
978 for (ctx_keys
, line
) in lines_to_del_to_del
:
979 lines_to_del
.remove((ctx_keys
, line
))
981 return (lines_to_add
, lines_to_del
)
984 def compare_context_objects(newconf
, running
):
986 Create a context diff for the two specified contexts
989 # Compare the two Config objects to find the lines that we need to add/del
994 # Find contexts that are in newconf but not in running
995 # Find contexts that are in running but not in newconf
996 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
998 if running_ctx_keys
not in newconf
.contexts
:
1000 # We check that the len is 1 here so that we only look at ('router bgp 10')
1001 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1002 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1003 # running but not in newconf.
1004 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1006 lines_to_del
.append((running_ctx_keys
, None))
1008 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1009 elif running_ctx_keys
[0].startswith('interface') or running_ctx_keys
[0].startswith('vrf'):
1010 for line
in running_ctx
.lines
:
1011 lines_to_del
.append((running_ctx_keys
, line
))
1013 # If this is an address-family under 'router bgp' and we are already deleting the
1014 # entire 'router bgp' context then ignore this sub-context
1015 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
1018 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1019 elif ("router bgp" in running_ctx_keys
[0] and
1020 len(running_ctx_keys
) > 2 and
1021 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
1022 running_ctx_keys
[2].startswith('vni ')):
1023 lines_to_del
.append((running_ctx_keys
, None))
1025 elif ("router bgp" in running_ctx_keys
[0] and
1026 len(running_ctx_keys
) > 1 and
1027 running_ctx_keys
[1].startswith('address-family')):
1028 # There's no 'no address-family' support and so we have to
1029 # delete each line individually again
1030 for line
in running_ctx
.lines
:
1031 lines_to_del
.append((running_ctx_keys
, line
))
1033 # Non-global context
1034 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
1035 lines_to_del
.append((running_ctx_keys
, None))
1037 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1038 lines_to_del
.append((running_ctx_keys
, None))
1042 for line
in running_ctx
.lines
:
1043 lines_to_del
.append((running_ctx_keys
, line
))
1045 # Find the lines within each context to add
1046 # Find the lines within each context to del
1047 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1049 if newconf_ctx_keys
in running
.contexts
:
1050 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1052 for line
in newconf_ctx
.lines
:
1053 if line
not in running_ctx
.dlines
:
1054 lines_to_add
.append((newconf_ctx_keys
, line
))
1056 for line
in running_ctx
.lines
:
1057 if line
not in newconf_ctx
.dlines
:
1058 lines_to_del
.append((newconf_ctx_keys
, line
))
1060 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1062 if newconf_ctx_keys
not in running
.contexts
:
1063 lines_to_add
.append((newconf_ctx_keys
, None))
1065 for line
in newconf_ctx
.lines
:
1066 lines_to_add
.append((newconf_ctx_keys
, line
))
1068 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
1069 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(lines_to_add
, lines_to_del
)
1071 return (lines_to_add
, lines_to_del
)
1075 def vtysh_config_available():
1077 Return False if no frr daemon is running or some other vtysh session is
1078 in 'configuration terminal' mode which will prevent us from making any
1079 configuration changes.
1083 cmd
= ['/usr/bin/vtysh', '-c', 'conf t']
1084 output
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
).strip()
1086 if 'VTY configuration is locked by other VTY' in output
.decode('utf-8'):
1088 log
.error("'%s' returned\n%s\n" % (' '.join(cmd
), output
))
1091 except subprocess
.CalledProcessError
as e
:
1092 msg
= "vtysh could not connect with any frr daemons"
1100 if __name__
== '__main__':
1101 # Command line options
1102 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
1103 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
1104 group
= parser
.add_mutually_exclusive_group(required
=True)
1105 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
1106 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
1107 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
1108 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
1109 parser
.add_argument('filename', help='Location of new frr config file')
1110 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
1111 args
= parser
.parse_args()
1114 # For --test log to stdout
1115 # For --reload log to /var/log/frr/frr-reload.log
1116 if args
.test
or args
.stdout
:
1117 logging
.basicConfig(level
=logging
.INFO
,
1118 format
='%(asctime)s %(levelname)5s: %(message)s')
1120 # Color the errors and warnings in red
1121 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
1122 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
1125 if not os
.path
.isdir('/var/log/frr/'):
1126 os
.makedirs('/var/log/frr/')
1128 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
1130 format
='%(asctime)s %(levelname)5s: %(message)s')
1132 # argparse should prevent this from happening but just to be safe...
1134 raise Exception('Must specify --reload or --test')
1135 log
= logging
.getLogger(__name__
)
1137 # Verify the new config file is valid
1138 if not os
.path
.isfile(args
.filename
):
1139 msg
= "Filename %s does not exist" % args
.filename
1144 if not os
.path
.getsize(args
.filename
):
1145 msg
= "Filename %s is an empty file" % args
.filename
1150 # Verify that 'service integrated-vtysh-config' is configured
1151 vtysh_filename
= '/etc/frr/vtysh.conf'
1152 service_integrated_vtysh_config
= True
1154 if os
.path
.isfile(vtysh_filename
):
1155 with
open(vtysh_filename
, 'r') as fh
:
1156 for line
in fh
.readlines():
1159 if line
== 'no service integrated-vtysh-config':
1160 service_integrated_vtysh_config
= False
1163 if not service_integrated_vtysh_config
:
1164 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1170 log
.setLevel(logging
.DEBUG
)
1172 log
.info('Called via "%s"', str(args
))
1174 # Create a Config object from the config generated by newconf
1176 newconf
.load_from_file(args
.filename
)
1181 # Create a Config object from the running config
1185 running
.load_from_file(args
.input)
1187 running
.load_from_show_running()
1189 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1190 lines_to_configure
= []
1193 print("\nLines To Delete")
1194 print("===============")
1196 for (ctx_keys
, line
) in lines_to_del
:
1201 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1202 lines_to_configure
.append(cmd
)
1206 print("\nLines To Add")
1207 print("============")
1209 for (ctx_keys
, line
) in lines_to_add
:
1214 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1215 lines_to_configure
.append(cmd
)
1220 # We will not be able to do anything, go ahead and exit(1)
1221 if not vtysh_config_available():
1224 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1226 # This looks a little odd but we have to do this twice...here is why
1227 # If the user had this running bgp config:
1230 # neighbor 1.1.1.1 remote-as 50
1231 # neighbor 1.1.1.1 route-map FOO out
1233 # and this config in the newconf config file
1236 # neighbor 1.1.1.1 remote-as 999
1237 # neighbor 1.1.1.1 route-map FOO out
1240 # Then the script will do
1241 # - no neighbor 1.1.1.1 remote-as 50
1242 # - neighbor 1.1.1.1 remote-as 999
1244 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1245 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1246 # configs again to put this line back.
1248 # There are many keywords in FRR that can only appear one time under
1249 # a context, take "bgp router-id" for example. If the config that we are
1250 # reloading against has the following:
1253 # bgp router-id 1.1.1.1
1254 # bgp router-id 2.2.2.2
1256 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1257 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1258 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1259 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1260 # second pass to include all of the "adds" from the first pass.
1261 lines_to_add_first_pass
= []
1265 running
.load_from_show_running()
1266 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1268 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1271 lines_to_add_first_pass
= lines_to_add
1273 lines_to_add
.extend(lines_to_add_first_pass
)
1275 # Only do deletes on the first pass. The reason being if we
1276 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1277 # will automatically add:
1280 # ipv6 nd ra-interval 10
1281 # no ipv6 nd suppress-ra
1284 # but those lines aren't in the config we are reloading against so
1285 # on the 2nd pass they will show up in lines_to_del. This could
1286 # apply to other scenarios as well where configuring FOO adds BAR
1288 if lines_to_del
and x
== 0:
1289 for (ctx_keys
, line
) in lines_to_del
:
1294 # 'no' commands are tricky, we can't just put them in a file and
1295 # vtysh -f that file. See the next comment for an explanation
1297 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1300 # Some commands in frr are picky about taking a "no" of the entire line.
1301 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1302 # only the beginning. If we hit one of these command an exception will be
1303 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1306 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1307 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1308 # % Unknown command.
1309 # frr(config-if)# no ip ospf authentication message-digest
1310 # % Unknown command.
1311 # frr(config-if)# no ip ospf authentication
1316 _
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
)
1318 except subprocess
.CalledProcessError
:
1320 # - Pull the last entry from cmd (this would be
1321 # 'no ip ospf authentication message-digest 1.1.1.1' in
1323 # - Split that last entry by whitespace and drop the last word
1324 log
.info('Failed to execute %s', ' '.join(cmd
))
1325 last_arg
= cmd
[-1].split(' ')
1327 if len(last_arg
) <= 2:
1328 log
.error('"%s" we failed to remove this command', original_cmd
)
1331 new_last_arg
= last_arg
[0:-1]
1332 cmd
[-1] = ' '.join(new_last_arg
)
1334 log
.info('Executed "%s"', ' '.join(cmd
))
1338 lines_to_configure
= []
1340 for (ctx_keys
, line
) in lines_to_add
:
1345 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1346 lines_to_configure
.append(cmd
)
1348 if lines_to_configure
:
1349 random_string
= ''.join(random
.SystemRandom().choice(
1350 string
.ascii_uppercase
+
1351 string
.digits
) for _
in range(6))
1353 filename
= "/var/run/frr/reload-%s.txt" % random_string
1354 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1356 with
open(filename
, 'w') as fh
:
1357 for line
in lines_to_configure
:
1358 fh
.write(line
+ '\n')
1361 subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
], stderr
=subprocess
.STDOUT
)
1362 except subprocess
.CalledProcessError
as e
:
1363 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1367 # Make these changes persistent
1368 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1369 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])