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
, bindir
, confdir
):
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([str(bindir
+ '/vtysh'), '-m', '-f', filename
, '--config_dir', confdir
],
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
, bindir
, confdir
, daemon
):
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 bindir
+ "/vtysh --config_dir " + confdir
+ " -c 'show run " + daemon
+ "' | /usr/bin/tail -n +4 | " + bindir
+ "/vtysh --config_dir " + confdir
+ " -m -f -",
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",
417 "vrrp autoconfigure")
419 for line
in self
.lines
:
424 if line
.startswith('!') or line
.startswith('#'):
428 # there is one exception though: ldpd accepts a 'router-id' clause
429 # as part of its 'mpls ldp' config context. If we are processing
430 # ldp configuration and encounter a router-id we should NOT switch
432 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
) and not (
433 ctx_keys
and ctx_keys
[0].startswith("mpls ldp") and line
.startswith("router-id ")):
434 self
.save_contexts(ctx_keys
, current_context_lines
)
436 # Start a new context
439 current_context_lines
= []
441 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
442 self
.save_contexts(ctx_keys
, current_context_lines
)
446 self
.save_contexts(ctx_keys
, current_context_lines
)
447 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
449 # Start a new context
453 current_context_lines
= []
455 elif line
== "exit-vrf":
456 self
.save_contexts(ctx_keys
, current_context_lines
)
457 current_context_lines
.append(line
)
458 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
464 current_context_lines
= []
466 elif line
in ["exit-address-family", "exit", "exit-vnc"]:
467 # if this exit is for address-family ipv4 unicast, ignore the pop
469 self
.save_contexts(ctx_keys
, current_context_lines
)
471 # Start a new context
472 ctx_keys
= copy
.deepcopy(main_ctx_key
)
473 current_context_lines
= []
474 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
476 elif line
in ["exit-vni", "exit-ldp-if"]:
478 self
.save_contexts(ctx_keys
, current_context_lines
)
480 # Start a new context
481 ctx_keys
= copy
.deepcopy(sub_main_ctx_key
)
482 current_context_lines
= []
483 log
.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line
, ctx_keys
)
485 elif new_ctx
is True:
489 ctx_keys
= copy
.deepcopy(main_ctx_key
)
492 current_context_lines
= []
494 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
495 elif (line
.startswith("address-family ") or
496 line
.startswith("vnc defaults") or
497 line
.startswith("vnc l2-group") or
498 line
.startswith("vnc nve-group") or
499 line
.startswith("member pseudowire")):
502 # Save old context first
503 self
.save_contexts(ctx_keys
, current_context_lines
)
504 current_context_lines
= []
505 main_ctx_key
= copy
.deepcopy(ctx_keys
)
506 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
508 if line
== "address-family ipv6" and not ctx_keys
[0].startswith("mpls ldp"):
509 ctx_keys
.append("address-family ipv6 unicast")
510 elif line
== "address-family ipv4" and not ctx_keys
[0].startswith("mpls ldp"):
511 ctx_keys
.append("address-family ipv4 unicast")
512 elif line
== "address-family evpn":
513 ctx_keys
.append("address-family l2vpn evpn")
515 ctx_keys
.append(line
)
517 elif ((line
.startswith("vni ") and
518 len(ctx_keys
) == 2 and
519 ctx_keys
[0].startswith('router bgp') and
520 ctx_keys
[1] == 'address-family l2vpn evpn')):
522 # Save old context first
523 self
.save_contexts(ctx_keys
, current_context_lines
)
524 current_context_lines
= []
525 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
526 log
.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line
)
527 ctx_keys
.append(line
)
529 elif ((line
.startswith("interface ") and
530 len(ctx_keys
) == 2 and
531 ctx_keys
[0].startswith('mpls ldp') and
532 ctx_keys
[1].startswith('address-family'))):
534 # Save old context first
535 self
.save_contexts(ctx_keys
, current_context_lines
)
536 current_context_lines
= []
537 sub_main_ctx_key
= copy
.deepcopy(ctx_keys
)
538 log
.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line
)
539 ctx_keys
.append(line
)
542 # Continuing in an existing context, add non-commented lines to it
543 current_context_lines
.append(line
)
544 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
546 # Save the context of the last one
547 self
.save_contexts(ctx_keys
, current_context_lines
)
550 def line_to_vtysh_conft(ctx_keys
, line
, delete
, bindir
, confdir
):
552 Return the vtysh command for the specified context line
556 cmd
.append(str(bindir
+ '/vtysh'))
557 cmd
.append('--config_dir')
563 for ctx_key
in ctx_keys
:
572 if line
.startswith('no '):
573 cmd
.append('%s' % line
[3:])
575 cmd
.append('no %s' % line
)
581 # If line is None then we are typically deleting an entire
582 # context ('no router ospf' for example)
587 # Only put the 'no' on the last sub-context
588 for ctx_key
in ctx_keys
:
591 if ctx_key
== ctx_keys
[-1]:
592 cmd
.append('no %s' % ctx_key
)
594 cmd
.append('%s' % ctx_key
)
596 for ctx_key
in ctx_keys
:
603 def line_for_vtysh_file(ctx_keys
, line
, delete
):
605 Return the command as it would appear in frr.conf
610 for (i
, ctx_key
) in enumerate(ctx_keys
):
611 cmd
.append(' ' * i
+ ctx_key
)
614 indent
= len(ctx_keys
) * ' '
617 if line
.startswith('no '):
618 cmd
.append('%s%s' % (indent
, line
[3:]))
620 cmd
.append('%sno %s' % (indent
, line
))
623 cmd
.append(indent
+ line
)
625 # If line is None then we are typically deleting an entire
626 # context ('no router ospf' for example)
630 # Only put the 'no' on the last sub-context
631 for ctx_key
in ctx_keys
:
633 if ctx_key
== ctx_keys
[-1]:
634 cmd
.append('no %s' % ctx_key
)
636 cmd
.append('%s' % ctx_key
)
638 for ctx_key
in ctx_keys
:
641 cmd
= '\n' + '\n'.join(cmd
)
643 # There are some commands that are on by default so their "no" form will be
644 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
645 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
646 # not by doing a "no no bgp default ipv4-unicast"
647 cmd
= cmd
.replace('no no ', '')
652 def get_normalized_ipv6_line(line
):
654 Return a normalized IPv6 line as produced by frr,
655 with all letters in lower case and trailing and leading
656 zeros removed, and only the network portion present if
657 the IPv6 word is a network
660 words
= line
.split(' ')
666 if 'ipaddress' not in sys
.modules
:
667 v6word
= IPNetwork(word
)
668 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
670 v6word
= ip_network(word
, strict
=False)
671 norm_word
= '%s/%s' % (str(v6word
.network_address
), v6word
.prefixlen
)
676 norm_word
= '%s' % IPv6Address(word
)
681 norm_line
= norm_line
+ " " + norm_word
683 return norm_line
.strip()
686 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
687 for (ctx_keys
, line
) in lines
:
688 if ctx_keys
== target_ctx_keys
:
690 if line
== target_line
:
693 if line
.startswith(target_line
):
698 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
700 # Quite possibly the most confusing (while accurate) variable names in history
701 lines_to_add_to_del
= []
702 lines_to_del_to_del
= []
704 for (ctx_keys
, line
) in lines_to_del
:
707 if ctx_keys
[0].startswith('router bgp') and line
:
709 if line
.startswith('neighbor '):
711 BGP changed how it displays swpX peers that are part of peer-group. Older
712 versions of frr would display these on separate lines:
713 neighbor swp1 interface
714 neighbor swp1 peer-group FOO
716 but today we display via a single line
717 neighbor swp1 interface peer-group FOO
719 This change confuses frr-reload.py so check to see if we are deleting
720 neighbor swp1 interface peer-group FOO
723 neighbor swp1 interface
724 neighbor swp1 peer-group FOO
726 If so then chop the del line and the corresponding add lines
729 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
730 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
732 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
733 swpx_interface
= None
734 swpx_peergroup
= None
736 if re_swpx_int_peergroup
:
737 swpx
= re_swpx_int_peergroup
.group(1)
738 peergroup
= re_swpx_int_peergroup
.group(2)
739 swpx_interface
= "neighbor %s interface" % swpx
740 elif re_swpx_int_v6only_peergroup
:
741 swpx
= re_swpx_int_v6only_peergroup
.group(1)
742 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
743 swpx_interface
= "neighbor %s interface v6only" % swpx
745 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
746 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
747 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
748 tmp_ctx_keys
= tuple(list(ctx_keys
))
750 if not found_add_swpx_peergroup
:
751 tmp_ctx_keys
= list(ctx_keys
)
752 tmp_ctx_keys
.append('address-family ipv4 unicast')
753 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
754 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
756 if not found_add_swpx_peergroup
:
757 tmp_ctx_keys
= list(ctx_keys
)
758 tmp_ctx_keys
.append('address-family ipv6 unicast')
759 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
760 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
762 if found_add_swpx_interface
and found_add_swpx_peergroup
:
764 lines_to_del_to_del
.append((ctx_keys
, line
))
765 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
766 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
769 Changing the bfd timers on neighbors is allowed without doing
770 a delete/add process. Since doing a "no neighbor blah bfd ..."
771 will cause the peer to bounce unnecessarily, just skip the delete
774 re_nbr_bfd_timers
= re
.search(r
'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line
)
776 if re_nbr_bfd_timers
:
777 nbr
= re_nbr_bfd_timers
.group(1)
778 bfd_nbr
= "neighbor %s" % nbr
779 bfd_search_string
= bfd_nbr
+ r
' bfd (\S+) (\S+) (\S+)'
781 for (ctx_keys
, add_line
) in lines_to_add
:
782 re_add_nbr_bfd_timers
= re
.search(bfd_search_string
, add_line
)
784 if re_add_nbr_bfd_timers
:
785 found_add_bfd_nbr
= line_exist(lines_to_add
, ctx_keys
, bfd_nbr
, False)
787 if found_add_bfd_nbr
:
788 lines_to_del_to_del
.append((ctx_keys
, line
))
791 We changed how we display the neighbor interface command. Older
792 versions of frr would display the following:
793 neighbor swp1 interface
794 neighbor swp1 remote-as external
795 neighbor swp1 capability extended-nexthop
797 but today we display via a single line
798 neighbor swp1 interface remote-as external
800 and capability extended-nexthop is no longer needed because we
801 automatically enable it when the neighbor is of type interface.
803 This change confuses frr-reload.py so check to see if we are deleting
804 neighbor swp1 interface remote-as (external|internal|ASNUM)
807 neighbor swp1 interface
808 neighbor swp1 remote-as (external|internal|ASNUM)
809 neighbor swp1 capability extended-nexthop
811 If so then chop the del line and the corresponding add lines
813 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
814 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
816 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
817 swpx_interface
= None
820 if re_swpx_int_remoteas
:
821 swpx
= re_swpx_int_remoteas
.group(1)
822 remoteas
= re_swpx_int_remoteas
.group(2)
823 swpx_interface
= "neighbor %s interface" % swpx
824 elif re_swpx_int_v6only_remoteas
:
825 swpx
= re_swpx_int_v6only_remoteas
.group(1)
826 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
827 swpx_interface
= "neighbor %s interface v6only" % swpx
829 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
830 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
831 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
832 tmp_ctx_keys
= tuple(list(ctx_keys
))
834 if found_add_swpx_interface
and found_add_swpx_remoteas
:
836 lines_to_del_to_del
.append((ctx_keys
, line
))
837 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
838 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
841 We made the 'bgp bestpath as-path multipath-relax' command
842 automatically assume 'no-as-set' since the lack of this option caused
843 weird routing problems. When the running config is shown in
844 releases with this change, the no-as-set keyword is not shown as it
845 is the default. This causes frr-reload to unnecessarily unapply
846 this option only to apply it back again, causing unnecessary session
849 if 'multipath-relax' in line
:
850 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
851 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
852 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
854 if re_asrelax_new
and found_asrelax_old
:
856 lines_to_del_to_del
.append((ctx_keys
, line
))
857 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
860 If we are modifying the BGP table-map we need to avoid a del/add and
861 instead modify the table-map in place via an add. This is needed to
862 avoid installing all routes in the RIB the second the 'no table-map'
865 if line
.startswith('table-map'):
866 found_table_map
= line_exist(lines_to_add
, ctx_keys
, 'table-map', False)
869 lines_to_del_to_del
.append((ctx_keys
, line
))
872 More old-to-new config handling. ip import-table no longer accepts
873 distance, but we honor the old syntax. But 'show running' shows only
874 the new syntax. This causes an unnecessary 'no import-table' followed
875 by the same old 'ip import-table' which causes perturbations in
876 announced routes leading to traffic blackholes. Fix this issue.
878 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
880 table_num
= re_importtbl
.group(1)
881 for ctx
in lines_to_add
:
882 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
883 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
884 lines_to_add_to_del
.append((ctx
[0], None))
887 ip/ipv6 prefix-list can be specified without a seq number. However,
888 the running config always adds 'seq x', where x is a number incremented
889 by 5 for every element, to the prefix list. So, ignore such lines as
890 well. Sample prefix-list lines:
891 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
892 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
893 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
895 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
898 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
899 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
900 re_ip_pfxlst
.group(6))
901 for ctx
in lines_to_add
:
902 if ctx
[0][0] == tmpline
:
903 lines_to_del_to_del
.append((ctx_keys
, None))
904 lines_to_add_to_del
.append(((tmpline
,), None))
906 if (len(ctx_keys
) == 3 and
907 ctx_keys
[0].startswith('router bgp') and
908 ctx_keys
[1] == 'address-family l2vpn evpn' and
909 ctx_keys
[2].startswith('vni')):
911 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
914 rt
= re_route_target
.group(1).strip()
915 route_target_import_line
= line
916 route_target_export_line
= "route-target export %s" % rt
917 route_target_both_line
= "route-target both %s" % rt
919 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
920 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
923 If the running configs has
924 route-target import 1:1
925 route-target export 1:1
927 and the config we are reloading against has
928 route-target both 1:1
930 then we can ignore deleting the import/export and ignore adding the 'both'
932 if found_route_target_export_line
and found_route_target_both_line
:
933 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
934 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
935 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
937 # Deleting static routes under a vrf can lead to time-outs if each is sent
938 # as separate vtysh -c commands. Change them from being in lines_to_del and
939 # put the "no" form in lines_to_add
940 if ctx_keys
[0].startswith('vrf ') and line
:
941 if (line
.startswith('ip route') or
942 line
.startswith('ipv6 route')):
943 add_cmd
= ('no ' + line
)
944 lines_to_add
.append((ctx_keys
, add_cmd
))
945 lines_to_del_to_del
.append((ctx_keys
, line
))
948 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
951 lines_to_del_to_del
.append((ctx_keys
, line
))
952 lines_to_add_to_del
.append((ctx_keys
, line
))
955 We have commands that used to be displayed in the global part
956 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
960 neighbor ISL advertisement-interval 0
966 address-family ipv4 unicast
967 neighbor ISL advertisement-interval 0
969 Look to see if we are deleting it in one format just to add it back in the other
971 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
972 tmp_ctx_keys
= list(ctx_keys
)[:-1]
973 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
975 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
978 lines_to_del_to_del
.append((ctx_keys
, line
))
979 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
981 for (ctx_keys
, line
) in lines_to_del_to_del
:
982 lines_to_del
.remove((ctx_keys
, line
))
984 for (ctx_keys
, line
) in lines_to_add_to_del
:
985 lines_to_add
.remove((ctx_keys
, line
))
987 return (lines_to_add
, lines_to_del
)
990 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
992 There are certain commands that cannot be removed. Remove
993 those commands from lines_to_del.
995 lines_to_del_to_del
= []
997 for (ctx_keys
, line
) in lines_to_del
:
999 if (ctx_keys
[0].startswith('frr version') or
1000 ctx_keys
[0].startswith('frr defaults') or
1001 ctx_keys
[0].startswith('password') or
1002 ctx_keys
[0].startswith('line vty') or
1004 # This is technically "no"able but if we did so frr-reload would
1005 # stop working so do not let the user shoot themselves in the foot
1007 ctx_keys
[0].startswith('service integrated-vtysh-config')):
1009 log
.info("(%s, %s) cannot be removed" % (pformat(ctx_keys
), line
))
1010 lines_to_del_to_del
.append((ctx_keys
, line
))
1012 for (ctx_keys
, line
) in lines_to_del_to_del
:
1013 lines_to_del
.remove((ctx_keys
, line
))
1015 return (lines_to_add
, lines_to_del
)
1018 def compare_context_objects(newconf
, running
):
1020 Create a context diff for the two specified contexts
1023 # Compare the two Config objects to find the lines that we need to add/del
1028 # Find contexts that are in newconf but not in running
1029 # Find contexts that are in running but not in newconf
1030 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1032 if running_ctx_keys
not in newconf
.contexts
:
1034 # We check that the len is 1 here so that we only look at ('router bgp 10')
1035 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1036 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1037 # running but not in newconf.
1038 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1040 lines_to_del
.append((running_ctx_keys
, None))
1042 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1043 elif running_ctx_keys
[0].startswith('interface') or running_ctx_keys
[0].startswith('vrf'):
1044 for line
in running_ctx
.lines
:
1045 lines_to_del
.append((running_ctx_keys
, line
))
1047 # If this is an address-family under 'router bgp' and we are already deleting the
1048 # entire 'router bgp' context then ignore this sub-context
1049 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
1052 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1053 elif ("router bgp" in running_ctx_keys
[0] and
1054 len(running_ctx_keys
) > 2 and
1055 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
1056 running_ctx_keys
[2].startswith('vni ')):
1057 lines_to_del
.append((running_ctx_keys
, None))
1059 elif ("router bgp" in running_ctx_keys
[0] and
1060 len(running_ctx_keys
) > 1 and
1061 running_ctx_keys
[1].startswith('address-family')):
1062 # There's no 'no address-family' support and so we have to
1063 # delete each line individually again
1064 for line
in running_ctx
.lines
:
1065 lines_to_del
.append((running_ctx_keys
, line
))
1067 # Some commands can happen at higher counts that make
1068 # doing vtysh -c inefficient (and can time out.) For
1069 # these commands, instead of adding them to lines_to_del,
1070 # add the "no " version to lines_to_add.
1071 elif (running_ctx_keys
[0].startswith('ip route') or
1072 running_ctx_keys
[0].startswith('ipv6 route') or
1073 running_ctx_keys
[0].startswith('access-list') or
1074 running_ctx_keys
[0].startswith('ipv6 access-list') or
1075 running_ctx_keys
[0].startswith('ip prefix-list') or
1076 running_ctx_keys
[0].startswith('ipv6 prefix-list')):
1077 add_cmd
= ('no ' + running_ctx_keys
[0],)
1078 lines_to_add
.append((add_cmd
, None))
1080 # Non-global context
1081 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
1082 lines_to_del
.append((running_ctx_keys
, None))
1084 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1085 lines_to_del
.append((running_ctx_keys
, None))
1089 for line
in running_ctx
.lines
:
1090 lines_to_del
.append((running_ctx_keys
, line
))
1092 # Find the lines within each context to add
1093 # Find the lines within each context to del
1094 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1096 if newconf_ctx_keys
in running
.contexts
:
1097 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1099 for line
in newconf_ctx
.lines
:
1100 if line
not in running_ctx
.dlines
:
1101 lines_to_add
.append((newconf_ctx_keys
, line
))
1103 for line
in running_ctx
.lines
:
1104 if line
not in newconf_ctx
.dlines
:
1105 lines_to_del
.append((newconf_ctx_keys
, line
))
1107 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1109 if newconf_ctx_keys
not in running
.contexts
:
1110 lines_to_add
.append((newconf_ctx_keys
, None))
1112 for line
in newconf_ctx
.lines
:
1113 lines_to_add
.append((newconf_ctx_keys
, line
))
1115 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
1116 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(lines_to_add
, lines_to_del
)
1118 return (lines_to_add
, lines_to_del
)
1122 def vtysh_config_available(bindir
, confdir
):
1124 Return False if no frr daemon is running or some other vtysh session is
1125 in 'configuration terminal' mode which will prevent us from making any
1126 configuration changes.
1130 cmd
= [str(bindir
+ '/vtysh'), '--config_dir', confdir
, '-c', 'conf t']
1131 output
= subprocess
.check_output(cmd
).strip()
1133 if 'VTY configuration is locked by other VTY' in output
.decode('utf-8'):
1135 log
.error("'%s' returned\n%s\n" % (' '.join(cmd
), output
))
1138 except subprocess
.CalledProcessError
as e
:
1139 msg
= "vtysh could not connect with any frr daemons"
1147 if __name__
== '__main__':
1148 # Command line options
1149 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
1150 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
1151 group
= parser
.add_mutually_exclusive_group(required
=True)
1152 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
1153 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
1154 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
1155 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
1156 parser
.add_argument('filename', help='Location of new frr config file')
1157 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
1158 parser
.add_argument('--bindir', help='path to the vtysh executable', default
='/usr/bin')
1159 parser
.add_argument('--confdir', help='path to the daemon config files', default
='/etc/frr')
1160 parser
.add_argument('--rundir', help='path for the temp config file', default
='/var/run/frr')
1161 parser
.add_argument('--daemon', help='daemon for which want to replace the config', default
='')
1163 args
= parser
.parse_args()
1166 # For --test log to stdout
1167 # For --reload log to /var/log/frr/frr-reload.log
1168 if args
.test
or args
.stdout
:
1169 logging
.basicConfig(level
=logging
.INFO
,
1170 format
='%(asctime)s %(levelname)5s: %(message)s')
1172 # Color the errors and warnings in red
1173 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
1174 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
1177 if not os
.path
.isdir('/var/log/frr/'):
1178 os
.makedirs('/var/log/frr/')
1180 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
1182 format
='%(asctime)s %(levelname)5s: %(message)s')
1184 # argparse should prevent this from happening but just to be safe...
1186 raise Exception('Must specify --reload or --test')
1187 log
= logging
.getLogger(__name__
)
1189 # Verify the new config file is valid
1190 if not os
.path
.isfile(args
.filename
):
1191 msg
= "Filename %s does not exist" % args
.filename
1196 if not os
.path
.getsize(args
.filename
):
1197 msg
= "Filename %s is an empty file" % args
.filename
1202 # Verify that confdir is correct
1203 if not os
.path
.isdir(args
.confdir
):
1204 msg
= "Confdir %s is not a valid path" % args
.confdir
1209 # Verify that bindir is correct
1210 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ '/vtysh'):
1211 msg
= "Bindir %s is not a valid path to vtysh" % args
.bindir
1216 # verify that the daemon, if specified, is valid
1217 if args
.daemon
and args
.daemon
not in ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']:
1218 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1223 # Verify that 'service integrated-vtysh-config' is configured
1224 vtysh_filename
= args
.confdir
+ '/vtysh.conf'
1225 service_integrated_vtysh_config
= True
1227 if os
.path
.isfile(vtysh_filename
):
1228 with
open(vtysh_filename
, 'r') as fh
:
1229 for line
in fh
.readlines():
1232 if line
== 'no service integrated-vtysh-config':
1233 service_integrated_vtysh_config
= False
1236 if not service_integrated_vtysh_config
and not args
.daemon
:
1237 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1243 log
.setLevel(logging
.DEBUG
)
1245 log
.info('Called via "%s"', str(args
))
1247 # Create a Config object from the config generated by newconf
1249 newconf
.load_from_file(args
.filename
, args
.bindir
, args
.confdir
)
1254 # Create a Config object from the running config
1258 running
.load_from_file(args
.input, args
.bindir
, args
.confdir
)
1260 running
.load_from_show_running(args
.bindir
, args
.confdir
, args
.daemon
)
1262 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1263 lines_to_configure
= []
1266 print("\nLines To Delete")
1267 print("===============")
1269 for (ctx_keys
, line
) in lines_to_del
:
1274 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1275 lines_to_configure
.append(cmd
)
1279 print("\nLines To Add")
1280 print("============")
1282 for (ctx_keys
, line
) in lines_to_add
:
1287 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1288 lines_to_configure
.append(cmd
)
1293 # We will not be able to do anything, go ahead and exit(1)
1294 if not vtysh_config_available(args
.bindir
, args
.confdir
):
1297 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1299 # This looks a little odd but we have to do this twice...here is why
1300 # If the user had this running bgp config:
1303 # neighbor 1.1.1.1 remote-as 50
1304 # neighbor 1.1.1.1 route-map FOO out
1306 # and this config in the newconf config file
1309 # neighbor 1.1.1.1 remote-as 999
1310 # neighbor 1.1.1.1 route-map FOO out
1313 # Then the script will do
1314 # - no neighbor 1.1.1.1 remote-as 50
1315 # - neighbor 1.1.1.1 remote-as 999
1317 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1318 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1319 # configs again to put this line back.
1321 # There are many keywords in FRR that can only appear one time under
1322 # a context, take "bgp router-id" for example. If the config that we are
1323 # reloading against has the following:
1326 # bgp router-id 1.1.1.1
1327 # bgp router-id 2.2.2.2
1329 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1330 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1331 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1332 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1333 # second pass to include all of the "adds" from the first pass.
1334 lines_to_add_first_pass
= []
1338 running
.load_from_show_running(args
.bindir
, args
.confdir
, args
.daemon
)
1339 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1341 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1344 lines_to_add_first_pass
= lines_to_add
1346 lines_to_add
.extend(lines_to_add_first_pass
)
1348 # Only do deletes on the first pass. The reason being if we
1349 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1350 # will automatically add:
1353 # ipv6 nd ra-interval 10
1354 # no ipv6 nd suppress-ra
1357 # but those lines aren't in the config we are reloading against so
1358 # on the 2nd pass they will show up in lines_to_del. This could
1359 # apply to other scenarios as well where configuring FOO adds BAR
1361 if lines_to_del
and x
== 0:
1362 for (ctx_keys
, line
) in lines_to_del
:
1367 # 'no' commands are tricky, we can't just put them in a file and
1368 # vtysh -f that file. See the next comment for an explanation
1370 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True, args
.bindir
, args
.confdir
)
1373 # Some commands in frr are picky about taking a "no" of the entire line.
1374 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1375 # only the beginning. If we hit one of these command an exception will be
1376 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1379 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1380 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1381 # % Unknown command.
1382 # frr(config-if)# no ip ospf authentication message-digest
1383 # % Unknown command.
1384 # frr(config-if)# no ip ospf authentication
1389 _
= subprocess
.check_output(cmd
)
1391 except subprocess
.CalledProcessError
:
1393 # - Pull the last entry from cmd (this would be
1394 # 'no ip ospf authentication message-digest 1.1.1.1' in
1396 # - Split that last entry by whitespace and drop the last word
1397 log
.info('Failed to execute %s', ' '.join(cmd
))
1398 last_arg
= cmd
[-1].split(' ')
1400 if len(last_arg
) <= 2:
1401 log
.error('"%s" we failed to remove this command', original_cmd
)
1404 new_last_arg
= last_arg
[0:-1]
1405 cmd
[-1] = ' '.join(new_last_arg
)
1407 log
.info('Executed "%s"', ' '.join(cmd
))
1411 lines_to_configure
= []
1413 for (ctx_keys
, line
) in lines_to_add
:
1418 # Don't run "no" commands twice since they can error
1419 # out the second time due to first deletion
1420 if x
== 1 and ctx_keys
[0].startswith('no '):
1423 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1424 lines_to_configure
.append(cmd
)
1426 if lines_to_configure
:
1427 random_string
= ''.join(random
.SystemRandom().choice(
1428 string
.ascii_uppercase
+
1429 string
.digits
) for _
in range(6))
1431 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
1432 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1434 with
open(filename
, 'w') as fh
:
1435 for line
in lines_to_configure
:
1436 fh
.write(line
+ '\n')
1439 subprocess
.check_output([str(args
.bindir
+ '/vtysh'), '--config_dir', args
.confdir
, '-f', filename
])
1440 except subprocess
.CalledProcessError
as e
:
1441 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1445 # Make these changes persistent
1446 target
= str(args
.confdir
+ '/frr.conf')
1447 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):
1448 subprocess
.call([str(args
.bindir
+ '/vtysh'), '--config_dir', args
.confdir
, '-c', 'write'])