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 ",
396 "allow-external-route-update",
416 "vrrp autoconfigure")
418 for line
in self
.lines
:
423 if line
.startswith('!') or line
.startswith('#'):
427 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
428 self
.save_contexts(ctx_keys
, current_context_lines
)
430 # Start a new context
433 current_context_lines
= []
435 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
436 self
.save_contexts(ctx_keys
, current_context_lines
)
440 self
.save_contexts(ctx_keys
, current_context_lines
)
441 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
443 # Start a new context
447 current_context_lines
= []
449 elif line
== "exit-vrf":
450 self
.save_contexts(ctx_keys
, current_context_lines
)
451 current_context_lines
.append(line
)
452 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
458 current_context_lines
= []
460 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
461 # if this exit is for address-family ipv4 unicast, ignore the pop
463 self
.save_contexts(ctx_keys
, current_context_lines
)
465 # Start a new context
466 ctx_keys
= copy
.deepcopy(main_ctx_key
)
467 current_context_lines
= []
468 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
470 elif line
== "exit-vni":
472 self
.save_contexts(ctx_keys
, current_context_lines
)
474 # Start a new context
475 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
476 current_context_lines
= []
477 log
.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line
, ctx_keys
)
479 elif new_ctx
is True:
483 ctx_keys
= copy
.deepcopy(main_ctx_key
)
486 current_context_lines
= []
488 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
489 elif (line
.startswith("address-family ") or
490 line
.startswith("vnc defaults") or
491 line
.startswith("vnc l2-group") or
492 line
.startswith("vnc nve-group")):
495 # Save old context first
496 self
.save_contexts(ctx_keys
, current_context_lines
)
497 current_context_lines
= []
498 main_ctx_key
= copy
.deepcopy(ctx_keys
)
499 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
501 if line
== "address-family ipv6":
502 ctx_keys
.append("address-family ipv6 unicast")
503 elif line
== "address-family ipv4":
504 ctx_keys
.append("address-family ipv4 unicast")
505 elif line
== "address-family evpn":
506 ctx_keys
.append("address-family l2vpn evpn")
508 ctx_keys
.append(line
)
510 elif ((line
.startswith("vni ") and
511 len(ctx_keys
) == 2 and
512 ctx_keys
[0].startswith('router bgp') and
513 ctx_keys
[1] == 'address-family l2vpn evpn')):
515 # Save old context first
516 self
.save_contexts(ctx_keys
, current_context_lines
)
517 current_context_lines
= []
518 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
519 log
.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line
)
520 ctx_keys
.append(line
)
523 # Continuing in an existing context, add non-commented lines to it
524 current_context_lines
.append(line
)
525 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
527 # Save the context of the last one
528 self
.save_contexts(ctx_keys
, current_context_lines
)
531 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
533 Return the vtysh command for the specified context line
542 for ctx_key
in ctx_keys
:
551 if line
.startswith('no '):
552 cmd
.append('%s' % line
[3:])
554 cmd
.append('no %s' % line
)
560 # If line is None then we are typically deleting an entire
561 # context ('no router ospf' for example)
566 # Only put the 'no' on the last sub-context
567 for ctx_key
in ctx_keys
:
570 if ctx_key
== ctx_keys
[-1]:
571 cmd
.append('no %s' % ctx_key
)
573 cmd
.append('%s' % ctx_key
)
575 for ctx_key
in ctx_keys
:
582 def line_for_vtysh_file(ctx_keys
, line
, delete
):
584 Return the command as it would appear in frr.conf
589 for (i
, ctx_key
) in enumerate(ctx_keys
):
590 cmd
.append(' ' * i
+ ctx_key
)
593 indent
= len(ctx_keys
) * ' '
596 if line
.startswith('no '):
597 cmd
.append('%s%s' % (indent
, line
[3:]))
599 cmd
.append('%sno %s' % (indent
, line
))
602 cmd
.append(indent
+ line
)
604 # If line is None then we are typically deleting an entire
605 # context ('no router ospf' for example)
609 # Only put the 'no' on the last sub-context
610 for ctx_key
in ctx_keys
:
612 if ctx_key
== ctx_keys
[-1]:
613 cmd
.append('no %s' % ctx_key
)
615 cmd
.append('%s' % ctx_key
)
617 for ctx_key
in ctx_keys
:
620 cmd
= '\n' + '\n'.join(cmd
)
622 # There are some commands that are on by default so their "no" form will be
623 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
624 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
625 # not by doing a "no no bgp default ipv4-unicast"
626 cmd
= cmd
.replace('no no ', '')
631 def get_normalized_ipv6_line(line
):
633 Return a normalized IPv6 line as produced by frr,
634 with all letters in lower case and trailing and leading
635 zeros removed, and only the network portion present if
636 the IPv6 word is a network
639 words
= line
.split(' ')
645 if 'ipaddress' not in sys
.modules
:
646 v6word
= IPNetwork(word
)
647 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
649 v6word
= ip_network(word
, strict
=False)
650 norm_word
= '%s/%s' % (str(v6word
.network_address
), v6word
.prefixlen
)
655 norm_word
= '%s' % IPv6Address(word
)
660 norm_line
= norm_line
+ " " + norm_word
662 return norm_line
.strip()
665 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
666 for (ctx_keys
, line
) in lines
:
667 if ctx_keys
== target_ctx_keys
:
669 if line
== target_line
:
672 if line
.startswith(target_line
):
677 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
679 # Quite possibly the most confusing (while accurate) variable names in history
680 lines_to_add_to_del
= []
681 lines_to_del_to_del
= []
683 for (ctx_keys
, line
) in lines_to_del
:
686 if ctx_keys
[0].startswith('router bgp') and line
:
688 if line
.startswith('neighbor '):
690 BGP changed how it displays swpX peers that are part of peer-group. Older
691 versions of frr would display these on separate lines:
692 neighbor swp1 interface
693 neighbor swp1 peer-group FOO
695 but today we display via a single line
696 neighbor swp1 interface peer-group FOO
698 This change confuses frr-reload.py so check to see if we are deleting
699 neighbor swp1 interface peer-group FOO
702 neighbor swp1 interface
703 neighbor swp1 peer-group FOO
705 If so then chop the del line and the corresponding add lines
708 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
709 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
711 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
712 swpx_interface
= None
713 swpx_peergroup
= None
715 if re_swpx_int_peergroup
:
716 swpx
= re_swpx_int_peergroup
.group(1)
717 peergroup
= re_swpx_int_peergroup
.group(2)
718 swpx_interface
= "neighbor %s interface" % swpx
719 elif re_swpx_int_v6only_peergroup
:
720 swpx
= re_swpx_int_v6only_peergroup
.group(1)
721 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
722 swpx_interface
= "neighbor %s interface v6only" % swpx
724 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
725 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
726 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
727 tmp_ctx_keys
= tuple(list(ctx_keys
))
729 if not found_add_swpx_peergroup
:
730 tmp_ctx_keys
= list(ctx_keys
)
731 tmp_ctx_keys
.append('address-family ipv4 unicast')
732 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
733 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
735 if not found_add_swpx_peergroup
:
736 tmp_ctx_keys
= list(ctx_keys
)
737 tmp_ctx_keys
.append('address-family ipv6 unicast')
738 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
739 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
741 if found_add_swpx_interface
and found_add_swpx_peergroup
:
743 lines_to_del_to_del
.append((ctx_keys
, line
))
744 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
745 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
748 Changing the bfd timers on neighbors is allowed without doing
749 a delete/add process. Since doing a "no neighbor blah bfd ..."
750 will cause the peer to bounce unnecessarily, just skip the delete
753 re_nbr_bfd_timers
= re
.search(r
'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line
)
755 if re_nbr_bfd_timers
:
756 nbr
= re_nbr_bfd_timers
.group(1)
757 bfd_nbr
= "neighbor %s" % nbr
759 for (ctx_keys
, add_line
) in lines_to_add
:
760 re_add_nbr_bfd_timers
= re
.search(r
'neighbor (\S+) bfd (\S+) (\S+) (\S+)', add_line
)
762 if re_add_nbr_bfd_timers
:
763 found_add_bfd_nbr
= line_exist(lines_to_add
, ctx_keys
, bfd_nbr
, False)
765 if found_add_bfd_nbr
:
766 lines_to_del_to_del
.append((ctx_keys
, line
))
769 We changed how we display the neighbor interface command. Older
770 versions of frr would display the following:
771 neighbor swp1 interface
772 neighbor swp1 remote-as external
773 neighbor swp1 capability extended-nexthop
775 but today we display via a single line
776 neighbor swp1 interface remote-as external
778 and capability extended-nexthop is no longer needed because we
779 automatically enable it when the neighbor is of type interface.
781 This change confuses frr-reload.py so check to see if we are deleting
782 neighbor swp1 interface remote-as (external|internal|ASNUM)
785 neighbor swp1 interface
786 neighbor swp1 remote-as (external|internal|ASNUM)
787 neighbor swp1 capability extended-nexthop
789 If so then chop the del line and the corresponding add lines
791 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
792 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
794 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
795 swpx_interface
= None
798 if re_swpx_int_remoteas
:
799 swpx
= re_swpx_int_remoteas
.group(1)
800 remoteas
= re_swpx_int_remoteas
.group(2)
801 swpx_interface
= "neighbor %s interface" % swpx
802 elif re_swpx_int_v6only_remoteas
:
803 swpx
= re_swpx_int_v6only_remoteas
.group(1)
804 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
805 swpx_interface
= "neighbor %s interface v6only" % swpx
807 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
808 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
809 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
810 tmp_ctx_keys
= tuple(list(ctx_keys
))
812 if found_add_swpx_interface
and found_add_swpx_remoteas
:
814 lines_to_del_to_del
.append((ctx_keys
, line
))
815 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
816 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
819 We made the 'bgp bestpath as-path multipath-relax' command
820 automatically assume 'no-as-set' since the lack of this option caused
821 weird routing problems. When the running config is shown in
822 releases with this change, the no-as-set keyword is not shown as it
823 is the default. This causes frr-reload to unnecessarily unapply
824 this option only to apply it back again, causing unnecessary session
827 if 'multipath-relax' in line
:
828 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
829 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
830 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
832 if re_asrelax_new
and found_asrelax_old
:
834 lines_to_del_to_del
.append((ctx_keys
, line
))
835 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
838 If we are modifying the BGP table-map we need to avoid a del/add and
839 instead modify the table-map in place via an add. This is needed to
840 avoid installing all routes in the RIB the second the 'no table-map'
843 if line
.startswith('table-map'):
844 found_table_map
= line_exist(lines_to_add
, ctx_keys
, 'table-map', False)
847 lines_to_del_to_del
.append((ctx_keys
, line
))
850 More old-to-new config handling. ip import-table no longer accepts
851 distance, but we honor the old syntax. But 'show running' shows only
852 the new syntax. This causes an unnecessary 'no import-table' followed
853 by the same old 'ip import-table' which causes perturbations in
854 announced routes leading to traffic blackholes. Fix this issue.
856 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
858 table_num
= re_importtbl
.group(1)
859 for ctx
in lines_to_add
:
860 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
861 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
862 lines_to_add_to_del
.append((ctx
[0], None))
865 ip/ipv6 prefix-list can be specified without a seq number. However,
866 the running config always adds 'seq x', where x is a number incremented
867 by 5 for every element, to the prefix list. So, ignore such lines as
868 well. Sample prefix-list lines:
869 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
870 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
871 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
873 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
876 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
877 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
878 re_ip_pfxlst
.group(6))
879 for ctx
in lines_to_add
:
880 if ctx
[0][0] == tmpline
:
881 lines_to_del_to_del
.append((ctx_keys
, None))
882 lines_to_add_to_del
.append(((tmpline
,), None))
884 if (len(ctx_keys
) == 3 and
885 ctx_keys
[0].startswith('router bgp') and
886 ctx_keys
[1] == 'address-family l2vpn evpn' and
887 ctx_keys
[2].startswith('vni')):
889 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
892 rt
= re_route_target
.group(1).strip()
893 route_target_import_line
= line
894 route_target_export_line
= "route-target export %s" % rt
895 route_target_both_line
= "route-target both %s" % rt
897 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
898 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
901 If the running configs has
902 route-target import 1:1
903 route-target export 1:1
905 and the config we are reloading against has
906 route-target both 1:1
908 then we can ignore deleting the import/export and ignore adding the 'both'
910 if found_route_target_export_line
and found_route_target_both_line
:
911 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
912 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
913 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
916 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
919 lines_to_del_to_del
.append((ctx_keys
, line
))
920 lines_to_add_to_del
.append((ctx_keys
, line
))
923 We have commands that used to be displayed in the global part
924 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
928 neighbor ISL advertisement-interval 0
934 address-family ipv4 unicast
935 neighbor ISL advertisement-interval 0
937 Look to see if we are deleting it in one format just to add it back in the other
939 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
940 tmp_ctx_keys
= list(ctx_keys
)[:-1]
941 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
943 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
946 lines_to_del_to_del
.append((ctx_keys
, line
))
947 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
949 for (ctx_keys
, line
) in lines_to_del_to_del
:
950 lines_to_del
.remove((ctx_keys
, line
))
952 for (ctx_keys
, line
) in lines_to_add_to_del
:
953 lines_to_add
.remove((ctx_keys
, line
))
955 return (lines_to_add
, lines_to_del
)
958 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
960 There are certain commands that cannot be removed. Remove
961 those commands from lines_to_del.
963 lines_to_del_to_del
= []
965 for (ctx_keys
, line
) in lines_to_del
:
967 if (ctx_keys
[0].startswith('frr version') or
968 ctx_keys
[0].startswith('frr defaults') or
969 ctx_keys
[0].startswith('password') or
970 ctx_keys
[0].startswith('line vty') or
972 # This is technically "no"able but if we did so frr-reload would
973 # stop working so do not let the user shoot themselves in the foot
975 ctx_keys
[0].startswith('service integrated-vtysh-config')):
977 log
.info("(%s, %s) cannot be removed" % (pformat(ctx_keys
), line
))
978 lines_to_del_to_del
.append((ctx_keys
, line
))
980 for (ctx_keys
, line
) in lines_to_del_to_del
:
981 lines_to_del
.remove((ctx_keys
, line
))
983 return (lines_to_add
, lines_to_del
)
986 def compare_context_objects(newconf
, running
):
988 Create a context diff for the two specified contexts
991 # Compare the two Config objects to find the lines that we need to add/del
996 # Find contexts that are in newconf but not in running
997 # Find contexts that are in running but not in newconf
998 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1000 if running_ctx_keys
not in newconf
.contexts
:
1002 # We check that the len is 1 here so that we only look at ('router bgp 10')
1003 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1004 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1005 # running but not in newconf.
1006 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1008 lines_to_del
.append((running_ctx_keys
, None))
1010 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1011 elif running_ctx_keys
[0].startswith('interface') or running_ctx_keys
[0].startswith('vrf'):
1012 for line
in running_ctx
.lines
:
1013 lines_to_del
.append((running_ctx_keys
, line
))
1015 # If this is an address-family under 'router bgp' and we are already deleting the
1016 # entire 'router bgp' context then ignore this sub-context
1017 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
1020 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1021 elif ("router bgp" in running_ctx_keys
[0] and
1022 len(running_ctx_keys
) > 2 and
1023 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
1024 running_ctx_keys
[2].startswith('vni ')):
1025 lines_to_del
.append((running_ctx_keys
, None))
1027 elif ("router bgp" in running_ctx_keys
[0] and
1028 len(running_ctx_keys
) > 1 and
1029 running_ctx_keys
[1].startswith('address-family')):
1030 # There's no 'no address-family' support and so we have to
1031 # delete each line individually again
1032 for line
in running_ctx
.lines
:
1033 lines_to_del
.append((running_ctx_keys
, line
))
1035 # Non-global context
1036 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
1037 lines_to_del
.append((running_ctx_keys
, None))
1039 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1040 lines_to_del
.append((running_ctx_keys
, None))
1044 for line
in running_ctx
.lines
:
1045 lines_to_del
.append((running_ctx_keys
, line
))
1047 # Find the lines within each context to add
1048 # Find the lines within each context to del
1049 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1051 if newconf_ctx_keys
in running
.contexts
:
1052 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1054 for line
in newconf_ctx
.lines
:
1055 if line
not in running_ctx
.dlines
:
1056 lines_to_add
.append((newconf_ctx_keys
, line
))
1058 for line
in running_ctx
.lines
:
1059 if line
not in newconf_ctx
.dlines
:
1060 lines_to_del
.append((newconf_ctx_keys
, line
))
1062 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1064 if newconf_ctx_keys
not in running
.contexts
:
1065 lines_to_add
.append((newconf_ctx_keys
, None))
1067 for line
in newconf_ctx
.lines
:
1068 lines_to_add
.append((newconf_ctx_keys
, line
))
1070 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
1071 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(lines_to_add
, lines_to_del
)
1073 return (lines_to_add
, lines_to_del
)
1077 def vtysh_config_available():
1079 Return False if no frr daemon is running or some other vtysh session is
1080 in 'configuration terminal' mode which will prevent us from making any
1081 configuration changes.
1085 cmd
= ['/usr/bin/vtysh', '-c', 'conf t']
1086 output
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
).strip()
1088 if 'VTY configuration is locked by other VTY' in output
.decode('utf-8'):
1090 log
.error("'%s' returned\n%s\n" % (' '.join(cmd
), output
))
1093 except subprocess
.CalledProcessError
as e
:
1094 msg
= "vtysh could not connect with any frr daemons"
1102 if __name__
== '__main__':
1103 # Command line options
1104 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
1105 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
1106 group
= parser
.add_mutually_exclusive_group(required
=True)
1107 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
1108 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
1109 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
1110 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
1111 parser
.add_argument('filename', help='Location of new frr config file')
1112 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
1113 args
= parser
.parse_args()
1116 # For --test log to stdout
1117 # For --reload log to /var/log/frr/frr-reload.log
1118 if args
.test
or args
.stdout
:
1119 logging
.basicConfig(level
=logging
.INFO
,
1120 format
='%(asctime)s %(levelname)5s: %(message)s')
1122 # Color the errors and warnings in red
1123 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
1124 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
1127 if not os
.path
.isdir('/var/log/frr/'):
1128 os
.makedirs('/var/log/frr/')
1130 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
1132 format
='%(asctime)s %(levelname)5s: %(message)s')
1134 # argparse should prevent this from happening but just to be safe...
1136 raise Exception('Must specify --reload or --test')
1137 log
= logging
.getLogger(__name__
)
1139 # Verify the new config file is valid
1140 if not os
.path
.isfile(args
.filename
):
1141 msg
= "Filename %s does not exist" % args
.filename
1146 if not os
.path
.getsize(args
.filename
):
1147 msg
= "Filename %s is an empty file" % args
.filename
1152 # Verify that 'service integrated-vtysh-config' is configured
1153 vtysh_filename
= '/etc/frr/vtysh.conf'
1154 service_integrated_vtysh_config
= True
1156 if os
.path
.isfile(vtysh_filename
):
1157 with
open(vtysh_filename
, 'r') as fh
:
1158 for line
in fh
.readlines():
1161 if line
== 'no service integrated-vtysh-config':
1162 service_integrated_vtysh_config
= False
1165 if not service_integrated_vtysh_config
:
1166 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1172 log
.setLevel(logging
.DEBUG
)
1174 log
.info('Called via "%s"', str(args
))
1176 # Create a Config object from the config generated by newconf
1178 newconf
.load_from_file(args
.filename
)
1183 # Create a Config object from the running config
1187 running
.load_from_file(args
.input)
1189 running
.load_from_show_running()
1191 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1192 lines_to_configure
= []
1195 print("\nLines To Delete")
1196 print("===============")
1198 for (ctx_keys
, line
) in lines_to_del
:
1203 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1204 lines_to_configure
.append(cmd
)
1208 print("\nLines To Add")
1209 print("============")
1211 for (ctx_keys
, line
) in lines_to_add
:
1216 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1217 lines_to_configure
.append(cmd
)
1222 # We will not be able to do anything, go ahead and exit(1)
1223 if not vtysh_config_available():
1226 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1228 # This looks a little odd but we have to do this twice...here is why
1229 # If the user had this running bgp config:
1232 # neighbor 1.1.1.1 remote-as 50
1233 # neighbor 1.1.1.1 route-map FOO out
1235 # and this config in the newconf config file
1238 # neighbor 1.1.1.1 remote-as 999
1239 # neighbor 1.1.1.1 route-map FOO out
1242 # Then the script will do
1243 # - no neighbor 1.1.1.1 remote-as 50
1244 # - neighbor 1.1.1.1 remote-as 999
1246 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1247 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1248 # configs again to put this line back.
1250 # There are many keywords in FRR that can only appear one time under
1251 # a context, take "bgp router-id" for example. If the config that we are
1252 # reloading against has the following:
1255 # bgp router-id 1.1.1.1
1256 # bgp router-id 2.2.2.2
1258 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1259 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1260 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1261 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1262 # second pass to include all of the "adds" from the first pass.
1263 lines_to_add_first_pass
= []
1267 running
.load_from_show_running()
1268 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1270 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1273 lines_to_add_first_pass
= lines_to_add
1275 lines_to_add
.extend(lines_to_add_first_pass
)
1277 # Only do deletes on the first pass. The reason being if we
1278 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1279 # will automatically add:
1282 # ipv6 nd ra-interval 10
1283 # no ipv6 nd suppress-ra
1286 # but those lines aren't in the config we are reloading against so
1287 # on the 2nd pass they will show up in lines_to_del. This could
1288 # apply to other scenarios as well where configuring FOO adds BAR
1290 if lines_to_del
and x
== 0:
1291 for (ctx_keys
, line
) in lines_to_del
:
1296 # 'no' commands are tricky, we can't just put them in a file and
1297 # vtysh -f that file. See the next comment for an explanation
1299 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1302 # Some commands in frr are picky about taking a "no" of the entire line.
1303 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1304 # only the beginning. If we hit one of these command an exception will be
1305 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1308 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1309 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1310 # % Unknown command.
1311 # frr(config-if)# no ip ospf authentication message-digest
1312 # % Unknown command.
1313 # frr(config-if)# no ip ospf authentication
1318 _
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
)
1320 except subprocess
.CalledProcessError
:
1322 # - Pull the last entry from cmd (this would be
1323 # 'no ip ospf authentication message-digest 1.1.1.1' in
1325 # - Split that last entry by whitespace and drop the last word
1326 log
.info('Failed to execute %s', ' '.join(cmd
))
1327 last_arg
= cmd
[-1].split(' ')
1329 if len(last_arg
) <= 2:
1330 log
.error('"%s" we failed to remove this command', original_cmd
)
1333 new_last_arg
= last_arg
[0:-1]
1334 cmd
[-1] = ' '.join(new_last_arg
)
1336 log
.info('Executed "%s"', ' '.join(cmd
))
1340 lines_to_configure
= []
1342 for (ctx_keys
, line
) in lines_to_add
:
1347 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1348 lines_to_configure
.append(cmd
)
1350 if lines_to_configure
:
1351 random_string
= ''.join(random
.SystemRandom().choice(
1352 string
.ascii_uppercase
+
1353 string
.digits
) for _
in range(6))
1355 filename
= "/var/run/frr/reload-%s.txt" % random_string
1356 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1358 with
open(filename
, 'w') as fh
:
1359 for line
in lines_to_configure
:
1360 fh
.write(line
+ '\n')
1363 subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
], stderr
=subprocess
.STDOUT
)
1364 except subprocess
.CalledProcessError
as e
:
1365 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1369 # Make these changes persistent
1370 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1371 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])