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
40 from collections
import OrderedDict
41 from ipaddr
import IPv6Address
, IPNetwork
42 from pprint
import pformat
45 log
= logging
.getLogger(__name__
)
48 class VtyshMarkException(Exception):
52 class Context(object):
55 A Context object represents a section of frr configuration such as:
58 description swp3 -> r8's swp1
63 or a single line context object such as this:
69 def __init__(self
, keys
, lines
):
73 # Keep a dictionary of the lines, this is to make it easy to tell if a
74 # line exists in this Context
75 self
.dlines
= OrderedDict()
78 self
.dlines
[ligne
] = True
80 def add_lines(self
, lines
):
82 Add lines to specified context
85 self
.lines
.extend(lines
)
88 self
.dlines
[ligne
] = True
94 A frr configuration is stored in a Config object. A Config object
95 contains a dictionary of Context objects where the Context keys
96 ('router ospf' for example) are our dictionary key.
101 self
.contexts
= OrderedDict()
103 def load_from_file(self
, filename
):
105 Read configuration from specified file and slurp it into internal memory
106 The internal representation has been marked appropriately by passing it
107 through vtysh with the -m parameter
109 log
.info('Loading Config object from file %s', filename
)
112 file_output
= subprocess
.check_output(['/usr/bin/vtysh', '-m', '-f', filename
],
113 stderr
=subprocess
.STDOUT
)
114 except subprocess
.CalledProcessError
as e
:
115 ve
= VtyshMarkException(e
)
119 for line
in file_output
.split('\n'):
122 qv6_line
= get_normalized_ipv6_line(line
)
123 self
.lines
.append(qv6_line
)
125 self
.lines
.append(line
)
129 def load_from_show_running(self
):
131 Read running configuration and slurp it into internal memory
132 The internal representation has been marked appropriately by passing it
133 through vtysh with the -m parameter
135 log
.info('Loading Config object from vtysh show running')
138 config_text
= subprocess
.check_output(
139 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
140 shell
=True, stderr
=subprocess
.STDOUT
)
141 except subprocess
.CalledProcessError
as e
:
142 ve
= VtyshMarkException(e
)
146 for line
in config_text
.split('\n'):
149 if (line
== 'Building configuration...' or
150 line
== 'Current configuration:' or
154 self
.lines
.append(line
)
160 Return the lines read in from the configuration
163 return '\n'.join(self
.lines
)
165 def get_contexts(self
):
167 Return the parsed context as strings for display, log etc.
170 for (_
, ctx
) in sorted(self
.contexts
.iteritems()):
171 print str(ctx
) + '\n'
173 def save_contexts(self
, key
, lines
):
175 Save the provided key and lines as a context
182 IP addresses specified in "network" statements, "ip prefix-lists"
183 etc. can differ in the host part of the specification the user
184 provides and what the running config displays. For example, user
185 can specify 11.1.1.1/24, and the running config displays this as
186 11.1.1.0/24. Ensure we don't do a needless operation for such
187 lines. IS-IS & OSPFv3 have no "network" support.
189 re_key_rt
= re
.match(r
'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key
[0])
191 addr
= re_key_rt
.group(2)
194 newaddr
= IPNetwork(addr
)
195 key
[0] = '%s route %s/%s%s' % (re_key_rt
.group(1),
202 re_key_rt
= re
.match(
203 r
'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
207 addr
= re_key_rt
.group(4)
210 newaddr
= '%s/%s' % (IPNetwork(addr
).network
,
211 IPNetwork(addr
).prefixlen
)
217 legestr
= re_key_rt
.group(5)
218 re_lege
= re
.search(r
'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr
)
220 legestr
= '%sge %s le %s%s' % (re_lege
.group(1),
224 re_lege
= re
.search(r
'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr
)
226 if (re_lege
and ((re_key_rt
.group(1) == "ip" and
227 re_lege
.group(3) == "32") or
228 (re_key_rt
.group(1) == "ipv6" and
229 re_lege
.group(3) == "128"))):
230 legestr
= '%sge %s%s' % (re_lege
.group(1),
234 key
[0] = '%s prefix-list%s%s %s%s' % (re_key_rt
.group(1),
240 if lines
and key
[0].startswith('router bgp'):
243 re_net
= re
.match(r
'network\s+([A-Fa-f:.0-9/]+)(.*)$', line
)
245 addr
= re_net
.group(1)
246 if '/' not in addr
and key
[0].startswith('router bgp'):
247 # This is most likely an error because with no
248 # prefixlen, BGP treats the prefixlen as 8
252 newaddr
= IPNetwork(addr
)
253 line
= 'network %s/%s %s' % (newaddr
.network
,
256 newlines
.append(line
)
258 # Really this should be an error. Whats a network
259 # without an IP Address following it ?
260 newlines
.append(line
)
262 newlines
.append(line
)
266 More fixups in user specification and what running config shows.
267 "null0" in routes must be replaced by Null0, and "blackhole" must
268 be replaced by Null0 as well.
270 if (key
[0].startswith('ip route') or key
[0].startswith('ipv6 route') and
271 'null0' in key
[0] or 'blackhole' in key
[0]):
272 key
[0] = re
.sub(r
'\s+null0(\s*$)', ' Null0', key
[0])
273 key
[0] = re
.sub(r
'\s+blackhole(\s*$)', ' Null0', key
[0])
276 if tuple(key
) not in self
.contexts
:
277 ctx
= Context(tuple(key
), lines
)
278 self
.contexts
[tuple(key
)] = ctx
280 ctx
= self
.contexts
[tuple(key
)]
284 if tuple(key
) not in self
.contexts
:
285 ctx
= Context(tuple(key
), [])
286 self
.contexts
[tuple(key
)] = ctx
288 def load_contexts(self
):
290 Parse the configuration and create contexts for each appropriate block
293 current_context_lines
= []
297 The end of a context is flagged via the 'end' keyword:
306 bgp router-id 10.0.0.1
307 bgp log-neighbor-changes
308 no bgp default ipv4-unicast
309 neighbor EBGP peer-group
310 neighbor EBGP advertisement-interval 1
311 neighbor EBGP timers connect 10
312 neighbor 2001:40:1:4::6 remote-as 40
313 neighbor 2001:40:1:8::a remote-as 40
317 neighbor IBGPv6 activate
318 neighbor 2001:10::2 peer-group IBGPv6
319 neighbor 2001:10::3 peer-group IBGPv6
324 neighbor LEAF activate
328 route-target import 10.1.1.1:10100
329 route-target export 10.1.1.1:10100
335 ospf router-id 10.0.0.1
336 log-adjacency-changes detail
337 timers throttle spf 0 50 5000
342 # The code assumes that its working on the output from the "vtysh -m"
343 # command. That provides the appropriate markers to signify end of
344 # a context. This routine uses that to build the contexts for the
347 # There are single line contexts such as "log file /media/node/zebra.log"
348 # and multi-line contexts such as "router ospf" and subcontexts
349 # within a context such as "address-family" within "router bgp"
350 # In each of these cases, the first line of the context becomes the
351 # key of the context. So "router bgp 10" is the key for the non-address
352 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
353 # the key for the subcontext and so on.
358 # the keywords that we know are single line contexts. bgp in this case
359 # is not the main router bgp block, but enabling multi-instance
360 oneline_ctx_keywords
= ("access-list ",
381 for line
in self
.lines
:
386 if line
.startswith('!') or line
.startswith('#'):
390 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
391 self
.save_contexts(ctx_keys
, current_context_lines
)
393 # Start a new context
396 current_context_lines
= []
398 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
399 self
.save_contexts(ctx_keys
, current_context_lines
)
403 self
.save_contexts(ctx_keys
, current_context_lines
)
404 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
406 # Start a new context
410 current_context_lines
= []
412 elif line
== "exit-address-family" or line
== "exit" or line
== "exit-vni":
413 # if this exit is for address-family ipv4 unicast, ignore the pop
415 self
.save_contexts(ctx_keys
, current_context_lines
)
417 # Start a new context
418 ctx_keys
= copy
.deepcopy(main_ctx_key
)
419 current_context_lines
= []
420 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
422 elif new_ctx
is True:
426 ctx_keys
= copy
.deepcopy(main_ctx_key
)
429 current_context_lines
= []
431 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
436 # Save old context first
437 self
.save_contexts(ctx_keys
, current_context_lines
)
438 current_context_lines
= []
439 main_ctx_key
= copy
.deepcopy(ctx_keys
)
440 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
442 ctx_keys
.append(line
)
444 elif "address-family " in line
:
447 # Save old context first
448 self
.save_contexts(ctx_keys
, current_context_lines
)
449 current_context_lines
= []
450 main_ctx_key
= copy
.deepcopy(ctx_keys
)
451 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
453 if line
== "address-family ipv6":
454 ctx_keys
.append("address-family ipv6 unicast")
455 elif line
== "address-family ipv4":
456 ctx_keys
.append("address-family ipv4 unicast")
457 elif line
== "address-family evpn":
458 ctx_keys
.append("address-family l2vpn evpn")
460 ctx_keys
.append(line
)
463 # Continuing in an existing context, add non-commented lines to it
464 current_context_lines
.append(line
)
465 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
467 # Save the context of the last one
468 self
.save_contexts(ctx_keys
, current_context_lines
)
471 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
473 Return the vtysh command for the specified context line
482 for ctx_key
in ctx_keys
:
491 if line
.startswith('no '):
492 cmd
.append('%s' % line
[3:])
494 cmd
.append('no %s' % line
)
500 # If line is None then we are typically deleting an entire
501 # context ('no router ospf' for example)
506 # Only put the 'no' on the last sub-context
507 for ctx_key
in ctx_keys
:
510 if ctx_key
== ctx_keys
[-1]:
511 cmd
.append('no %s' % ctx_key
)
513 cmd
.append('%s' % ctx_key
)
515 for ctx_key
in ctx_keys
:
522 def line_for_vtysh_file(ctx_keys
, line
, delete
):
524 Return the command as it would appear in frr.conf
529 for (i
, ctx_key
) in enumerate(ctx_keys
):
530 cmd
.append(' ' * i
+ ctx_key
)
533 indent
= len(ctx_keys
) * ' '
536 if line
.startswith('no '):
537 cmd
.append('%s%s' % (indent
, line
[3:]))
539 cmd
.append('%sno %s' % (indent
, line
))
542 cmd
.append(indent
+ line
)
544 # If line is None then we are typically deleting an entire
545 # context ('no router ospf' for example)
549 # Only put the 'no' on the last sub-context
550 for ctx_key
in ctx_keys
:
552 if ctx_key
== ctx_keys
[-1]:
553 cmd
.append('no %s' % ctx_key
)
555 cmd
.append('%s' % ctx_key
)
557 for ctx_key
in ctx_keys
:
560 cmd
= '\n' + '\n'.join(cmd
)
562 # There are some commands that are on by default so their "no" form will be
563 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
564 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
565 # not by doing a "no no bgp default ipv4-unicast"
566 cmd
= cmd
.replace('no no ', '')
571 def get_normalized_ipv6_line(line
):
573 Return a normalized IPv6 line as produced by frr,
574 with all letters in lower case and trailing and leading
575 zeros removed, and only the network portion present if
576 the IPv6 word is a network
579 words
= line
.split(' ')
585 v6word
= IPNetwork(word
)
586 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
591 norm_word
= '%s' % IPv6Address(word
)
596 norm_line
= norm_line
+ " " + norm_word
598 return norm_line
.strip()
601 def line_exist(lines
, target_ctx_keys
, target_line
):
602 for (ctx_keys
, line
) in lines
:
603 if ctx_keys
== target_ctx_keys
and line
== target_line
:
608 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
610 # Quite possibly the most confusing (while accurate) variable names in history
611 lines_to_add_to_del
= []
612 lines_to_del_to_del
= []
614 for (ctx_keys
, line
) in lines_to_del
:
617 if ctx_keys
[0].startswith('router bgp') and line
and line
.startswith('neighbor '):
619 BGP changed how it displays swpX peers that are part of peer-group. Older
620 versions of frr would display these on separate lines:
621 neighbor swp1 interface
622 neighbor swp1 peer-group FOO
624 but today we display via a single line
625 neighbor swp1 interface peer-group FOO
627 This change confuses frr-reload.py so check to see if we are deleting
628 neighbor swp1 interface peer-group FOO
631 neighbor swp1 interface
632 neighbor swp1 peer-group FOO
634 If so then chop the del line and the corresponding add lines
637 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
638 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
640 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
641 swpx_interface
= None
642 swpx_peergroup
= None
644 if re_swpx_int_peergroup
:
645 swpx
= re_swpx_int_peergroup
.group(1)
646 peergroup
= re_swpx_int_peergroup
.group(2)
647 swpx_interface
= "neighbor %s interface" % swpx
648 elif re_swpx_int_v6only_peergroup
:
649 swpx
= re_swpx_int_v6only_peergroup
.group(1)
650 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
651 swpx_interface
= "neighbor %s interface v6only" % swpx
653 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
654 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
655 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
656 tmp_ctx_keys
= tuple(list(ctx_keys
))
658 if not found_add_swpx_peergroup
:
659 tmp_ctx_keys
= list(ctx_keys
)
660 tmp_ctx_keys
.append('address-family ipv4 unicast')
661 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
662 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
664 if not found_add_swpx_peergroup
:
665 tmp_ctx_keys
= list(ctx_keys
)
666 tmp_ctx_keys
.append('address-family ipv6 unicast')
667 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
668 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
670 if found_add_swpx_interface
and found_add_swpx_peergroup
:
672 lines_to_del_to_del
.append((ctx_keys
, line
))
673 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
674 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
677 In 3.0.1 we changed how we display neighbor interface command. Older
678 versions of frr would display the following:
679 neighbor swp1 interface
680 neighbor swp1 remote-as external
681 neighbor swp1 capability extended-nexthop
683 but today we display via a single line
684 neighbor swp1 interface remote-as external
686 and capability extended-nexthop is no longer needed because we
687 automatically enable it when the neighbor is of type interface.
689 This change confuses frr-reload.py so check to see if we are deleting
690 neighbor swp1 interface remote-as (external|internal|ASNUM)
693 neighbor swp1 interface
694 neighbor swp1 remote-as (external|internal|ASNUM)
695 neighbor swp1 capability extended-nexthop
697 If so then chop the del line and the corresponding add lines
699 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
700 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
702 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
703 swpx_interface
= None
706 if re_swpx_int_remoteas
:
707 swpx
= re_swpx_int_remoteas
.group(1)
708 remoteas
= re_swpx_int_remoteas
.group(2)
709 swpx_interface
= "neighbor %s interface" % swpx
710 elif re_swpx_int_v6only_remoteas
:
711 swpx
= re_swpx_int_v6only_remoteas
.group(1)
712 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
713 swpx_interface
= "neighbor %s interface v6only" % swpx
715 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
716 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
717 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
718 tmp_ctx_keys
= tuple(list(ctx_keys
))
720 if found_add_swpx_interface
and found_add_swpx_remoteas
:
722 lines_to_del_to_del
.append((ctx_keys
, line
))
723 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
724 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
727 In 3.0, we made bgp bestpath multipath as-relax command
728 automatically assume no-as-set since the lack of this option caused
729 weird routing problems and this problem was peculiar to this
730 implementation. When the running config is shown in relases after
731 3.0, the no-as-set is not shown as its the default. This causes
732 reload to unnecessarily unapply this option to only apply it back
733 again, causing unnecessary session resets. Handle this.
735 if ctx_keys
[0].startswith('router bgp') and line
and 'multipath-relax' in line
:
736 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
737 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
738 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
740 if re_asrelax_new
and found_asrelax_old
:
742 lines_to_del_to_del
.append((ctx_keys
, line
))
743 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
746 More old-to-new config handling. ip import-table no longer accepts
747 distance, but we honor the old syntax. But 'show running' shows only
748 the new syntax. This causes an unnecessary 'no import-table' followed
749 by the same old 'ip import-table' which causes perturbations in
750 announced routes leading to traffic blackholes. Fix this issue.
752 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
754 table_num
= re_importtbl
.group(1)
755 for ctx
in lines_to_add
:
756 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
757 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
758 lines_to_add_to_del
.append((ctx
[0], None))
761 ip/ipv6 prefix-list can be specified without a seq number. However,
762 the running config always adds 'seq x', where x is a number incremented
763 by 5 for every element, to the prefix list. So, ignore such lines as
764 well. Sample prefix-list lines:
765 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
766 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
767 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
769 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
772 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
773 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
774 re_ip_pfxlst
.group(6))
775 for ctx
in lines_to_add
:
776 if ctx
[0][0] == tmpline
:
777 lines_to_del_to_del
.append((ctx_keys
, None))
778 lines_to_add_to_del
.append(((tmpline
,), None))
780 if (len(ctx_keys
) == 3 and
781 ctx_keys
[0].startswith('router bgp') and
782 ctx_keys
[1] == 'address-family l2vpn evpn' and
783 ctx_keys
[2].startswith('vni')):
785 re_route_target
= re
.search('^route-target import (.*)$', line
) if line
is not None else False
788 rt
= re_route_target
.group(1).strip()
789 route_target_import_line
= line
790 route_target_export_line
= "route-target export %s" % rt
791 route_target_both_line
= "route-target both %s" % rt
793 found_route_target_export_line
= line_exist(lines_to_del
, ctx_keys
, route_target_export_line
)
794 found_route_target_both_line
= line_exist(lines_to_add
, ctx_keys
, route_target_both_line
)
797 If the running configs has
798 route-target import 1:1
799 route-target export 1:1
801 and the config we are reloading against has
802 route-target both 1:1
804 then we can ignore deleting the import/export and ignore adding the 'both'
806 if found_route_target_export_line
and found_route_target_both_line
:
807 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
808 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
809 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
812 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
815 lines_to_del_to_del
.append((ctx_keys
, line
))
816 lines_to_add_to_del
.append((ctx_keys
, line
))
819 We have commands that used to be displayed in the global part
820 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
824 neighbor ISL advertisement-interval 0
830 address-family ipv4 unicast
831 neighbor ISL advertisement-interval 0
833 Look to see if we are deleting it in one format just to add it back in the other
835 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
836 tmp_ctx_keys
= list(ctx_keys
)[:-1]
837 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
839 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
842 lines_to_del_to_del
.append((ctx_keys
, line
))
843 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
845 for (ctx_keys
, line
) in lines_to_del_to_del
:
846 lines_to_del
.remove((ctx_keys
, line
))
848 for (ctx_keys
, line
) in lines_to_add_to_del
:
849 lines_to_add
.remove((ctx_keys
, line
))
851 return (lines_to_add
, lines_to_del
)
854 def compare_context_objects(newconf
, running
):
856 Create a context diff for the two specified contexts
859 # Compare the two Config objects to find the lines that we need to add/del
864 # Find contexts that are in newconf but not in running
865 # Find contexts that are in running but not in newconf
866 for (running_ctx_keys
, running_ctx
) in running
.contexts
.iteritems():
868 if running_ctx_keys
not in newconf
.contexts
:
870 # We check that the len is 1 here so that we only look at ('router bgp 10')
871 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
872 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
873 # running but not in newconf.
874 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
876 lines_to_del
.append((running_ctx_keys
, None))
878 # We cannot do 'no interface' in FRR, and so deal with it
879 elif running_ctx_keys
[0].startswith('interface'):
880 for line
in running_ctx
.lines
:
881 lines_to_del
.append((running_ctx_keys
, line
))
883 # If this is an address-family under 'router bgp' and we are already deleting the
884 # entire 'router bgp' context then ignore this sub-context
885 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
888 # Delete an entire vni sub-context under "address-family l2vpn evpn"
889 elif ("router bgp" in running_ctx_keys
[0] and
890 len(running_ctx_keys
) > 2 and
891 running_ctx_keys
[1].startswith('address-family l2vpn evpn') and
892 running_ctx_keys
[2].startswith('vni ')):
893 lines_to_del
.append((running_ctx_keys
, None))
895 elif ("router bgp" in running_ctx_keys
[0] and
896 len(running_ctx_keys
) > 1 and
897 running_ctx_keys
[1].startswith('address-family')):
898 # There's no 'no address-family' support and so we have to
899 # delete each line individually again
900 for line
in running_ctx
.lines
:
901 lines_to_del
.append((running_ctx_keys
, line
))
904 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
905 lines_to_del
.append((running_ctx_keys
, None))
907 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
908 lines_to_del
.append((running_ctx_keys
, None))
912 for line
in running_ctx
.lines
:
913 lines_to_del
.append((running_ctx_keys
, line
))
915 # Find the lines within each context to add
916 # Find the lines within each context to del
917 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
919 if newconf_ctx_keys
in running
.contexts
:
920 running_ctx
= running
.contexts
[newconf_ctx_keys
]
922 for line
in newconf_ctx
.lines
:
923 if line
not in running_ctx
.dlines
:
924 lines_to_add
.append((newconf_ctx_keys
, line
))
926 for line
in running_ctx
.lines
:
927 if line
not in newconf_ctx
.dlines
:
928 lines_to_del
.append((newconf_ctx_keys
, line
))
930 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
932 if newconf_ctx_keys
not in running
.contexts
:
933 lines_to_add
.append((newconf_ctx_keys
, None))
935 for line
in newconf_ctx
.lines
:
936 lines_to_add
.append((newconf_ctx_keys
, line
))
938 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
940 return (lines_to_add
, lines_to_del
)
943 if __name__
== '__main__':
944 # Command line options
945 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
946 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
947 group
= parser
.add_mutually_exclusive_group(required
=True)
948 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
949 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
950 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
951 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
952 parser
.add_argument('filename', help='Location of new frr config file')
953 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
954 args
= parser
.parse_args()
957 # For --test log to stdout
958 # For --reload log to /var/log/frr/frr-reload.log
959 if args
.test
or args
.stdout
:
960 logging
.basicConfig(level
=logging
.INFO
,
961 format
='%(asctime)s %(levelname)5s: %(message)s')
963 # Color the errors and warnings in red
964 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
965 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
968 if not os
.path
.isdir('/var/log/frr/'):
969 os
.makedirs('/var/log/frr/')
971 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
973 format
='%(asctime)s %(levelname)5s: %(message)s')
975 # argparse should prevent this from happening but just to be safe...
977 raise Exception('Must specify --reload or --test')
978 log
= logging
.getLogger(__name__
)
980 # Verify the new config file is valid
981 if not os
.path
.isfile(args
.filename
):
982 msg
= "Filename %s does not exist" % args
.filename
987 if not os
.path
.getsize(args
.filename
):
988 msg
= "Filename %s is an empty file" % args
.filename
993 # Verify that 'service integrated-vtysh-config' is configured
994 vtysh_filename
= '/etc/frr/vtysh.conf'
995 service_integrated_vtysh_config
= True
997 if os
.path
.isfile(vtysh_filename
):
998 with
open(vtysh_filename
, 'r') as fh
:
999 for line
in fh
.readlines():
1002 if line
== 'no service integrated-vtysh-config':
1003 service_integrated_vtysh_config
= False
1006 if not service_integrated_vtysh_config
:
1007 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1013 log
.setLevel(logging
.DEBUG
)
1015 log
.info('Called via "%s"', str(args
))
1017 # Create a Config object from the config generated by newconf
1019 newconf
.load_from_file(args
.filename
)
1024 # Create a Config object from the running config
1028 running
.load_from_file(args
.input)
1030 running
.load_from_show_running()
1032 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1033 lines_to_configure
= []
1036 print "\nLines To Delete"
1037 print "==============="
1039 for (ctx_keys
, line
) in lines_to_del
:
1044 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
1045 lines_to_configure
.append(cmd
)
1049 print "\nLines To Add"
1050 print "============"
1052 for (ctx_keys
, line
) in lines_to_add
:
1057 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1058 lines_to_configure
.append(cmd
)
1063 log
.debug('New Frr Config\n%s', newconf
.get_lines())
1065 # This looks a little odd but we have to do this twice...here is why
1066 # If the user had this running bgp config:
1069 # neighbor 1.1.1.1 remote-as 50
1070 # neighbor 1.1.1.1 route-map FOO out
1072 # and this config in the newconf config file
1075 # neighbor 1.1.1.1 remote-as 999
1076 # neighbor 1.1.1.1 route-map FOO out
1079 # Then the script will do
1080 # - no neighbor 1.1.1.1 remote-as 50
1081 # - neighbor 1.1.1.1 remote-as 999
1083 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1084 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1085 # configs again to put this line back.
1087 # There are many keywords in FRR that can only appear one time under
1088 # a context, take "bgp router-id" for example. If the config that we are
1089 # reloading against has the following:
1092 # bgp router-id 1.1.1.1
1093 # bgp router-id 2.2.2.2
1095 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1096 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1097 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1098 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1099 # second pass to include all of the "adds" from the first pass.
1100 lines_to_add_first_pass
= []
1104 running
.load_from_show_running()
1105 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1107 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1110 lines_to_add_first_pass
= lines_to_add
1112 lines_to_add
.extend(lines_to_add_first_pass
)
1114 # Only do deletes on the first pass. The reason being if we
1115 # configure a bgp neighbor via "neighbor swp1 interface" FRR
1116 # will automatically add:
1119 # ipv6 nd ra-interval 10
1120 # no ipv6 nd suppress-ra
1123 # but those lines aren't in the config we are reloading against so
1124 # on the 2nd pass they will show up in lines_to_del. This could
1125 # apply to other scenarios as well where configuring FOO adds BAR
1127 if lines_to_del
and x
== 0:
1128 for (ctx_keys
, line
) in lines_to_del
:
1133 # 'no' commands are tricky, we can't just put them in a file and
1134 # vtysh -f that file. See the next comment for an explanation
1136 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1139 # Some commands in frr are picky about taking a "no" of the entire line.
1140 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1141 # only the beginning. If we hit one of these command an exception will be
1142 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1145 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1146 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1147 # % Unknown command.
1148 # frr(config-if)# no ip ospf authentication message-digest
1149 # % Unknown command.
1150 # frr(config-if)# no ip ospf authentication
1155 _
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
)
1157 except subprocess
.CalledProcessError
:
1159 # - Pull the last entry from cmd (this would be
1160 # 'no ip ospf authentication message-digest 1.1.1.1' in
1162 # - Split that last entry by whitespace and drop the last word
1163 log
.info('Failed to execute %s', ' '.join(cmd
))
1164 last_arg
= cmd
[-1].split(' ')
1166 if len(last_arg
) <= 2:
1167 log
.error('"%s" we failed to remove this command', original_cmd
)
1170 new_last_arg
= last_arg
[0:-1]
1171 cmd
[-1] = ' '.join(new_last_arg
)
1173 log
.info('Executed "%s"', ' '.join(cmd
))
1177 lines_to_configure
= []
1179 for (ctx_keys
, line
) in lines_to_add
:
1184 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1185 lines_to_configure
.append(cmd
)
1187 if lines_to_configure
:
1188 random_string
= ''.join(random
.SystemRandom().choice(
1189 string
.ascii_uppercase
+
1190 string
.digits
) for _
in range(6))
1192 filename
= "/var/run/frr/reload-%s.txt" % random_string
1193 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1195 with
open(filename
, 'w') as fh
:
1196 for line
in lines_to_configure
:
1197 fh
.write(line
+ '\n')
1200 subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
], stderr
=subprocess
.STDOUT
)
1201 except subprocess
.CalledProcessError
as e
:
1202 log
.warning("frr-reload.py failed due to\n%s" % e
.output
)
1206 # Make these changes persistent
1207 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1208 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])