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 ospf router-id 10.0.0.1
325 log-adjacency-changes detail
326 timers throttle spf 0 50 5000
331 # The code assumes that its working on the output from the "vtysh -m"
332 # command. That provides the appropriate markers to signify end of
333 # a context. This routine uses that to build the contexts for the
336 # There are single line contexts such as "log file /media/node/zebra.log"
337 # and multi-line contexts such as "router ospf" and subcontexts
338 # within a context such as "address-family" within "router bgp"
339 # In each of these cases, the first line of the context becomes the
340 # key of the context. So "router bgp 10" is the key for the non-address
341 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
342 # the key for the subcontext and so on.
347 # the keywords that we know are single line contexts. bgp in this case
348 # is not the main router bgp block, but enabling multi-instance
349 oneline_ctx_keywords
= ("access-list ",
370 for line
in self
.lines
:
375 if line
.startswith('!') or line
.startswith('#'):
379 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
380 self
.save_contexts(ctx_keys
, current_context_lines
)
382 # Start a new context
385 current_context_lines
= []
387 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
388 self
.save_contexts(ctx_keys
, current_context_lines
)
392 self
.save_contexts(ctx_keys
, current_context_lines
)
393 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
395 # Start a new context
399 current_context_lines
= []
401 elif line
== "exit-address-family" or line
== "exit":
402 # if this exit is for address-family ipv4 unicast, ignore the pop
404 self
.save_contexts(ctx_keys
, current_context_lines
)
406 # Start a new context
407 ctx_keys
= copy
.deepcopy(main_ctx_key
)
408 current_context_lines
= []
409 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
411 elif new_ctx
is True:
415 ctx_keys
= copy
.deepcopy(main_ctx_key
)
418 current_context_lines
= []
420 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
422 elif "address-family " in line
:
425 # Save old context first
426 self
.save_contexts(ctx_keys
, current_context_lines
)
427 current_context_lines
= []
428 main_ctx_key
= copy
.deepcopy(ctx_keys
)
429 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
431 if line
== "address-family ipv6":
432 ctx_keys
.append("address-family ipv6 unicast")
433 elif line
== "address-family ipv4":
434 ctx_keys
.append("address-family ipv4 unicast")
436 ctx_keys
.append(line
)
439 # Continuing in an existing context, add non-commented lines to it
440 current_context_lines
.append(line
)
441 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
443 # Save the context of the last one
444 self
.save_contexts(ctx_keys
, current_context_lines
)
447 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
449 Return the vtysh command for the specified context line
458 for ctx_key
in ctx_keys
:
467 if line
.startswith('no '):
468 cmd
.append('%s' % line
[3:])
470 cmd
.append('no %s' % line
)
476 # If line is None then we are typically deleting an entire
477 # context ('no router ospf' for example)
482 # Only put the 'no' on the last sub-context
483 for ctx_key
in ctx_keys
:
486 if ctx_key
== ctx_keys
[-1]:
487 cmd
.append('no %s' % ctx_key
)
489 cmd
.append('%s' % ctx_key
)
491 for ctx_key
in ctx_keys
:
498 def line_for_vtysh_file(ctx_keys
, line
, delete
):
500 Return the command as it would appear in frr.conf
505 for (i
, ctx_key
) in enumerate(ctx_keys
):
506 cmd
.append(' ' * i
+ ctx_key
)
509 indent
= len(ctx_keys
) * ' '
512 if line
.startswith('no '):
513 cmd
.append('%s%s' % (indent
, line
[3:]))
515 cmd
.append('%sno %s' % (indent
, line
))
518 cmd
.append(indent
+ line
)
520 # If line is None then we are typically deleting an entire
521 # context ('no router ospf' for example)
525 # Only put the 'no' on the last sub-context
526 for ctx_key
in ctx_keys
:
528 if ctx_key
== ctx_keys
[-1]:
529 cmd
.append('no %s' % ctx_key
)
531 cmd
.append('%s' % ctx_key
)
533 for ctx_key
in ctx_keys
:
536 return '\n' + '\n'.join(cmd
)
539 def get_normalized_ipv6_line(line
):
541 Return a normalized IPv6 line as produced by frr,
542 with all letters in lower case and trailing and leading
543 zeros removed, and only the network portion present if
544 the IPv6 word is a network
547 words
= line
.split(' ')
553 v6word
= IPNetwork(word
)
554 norm_word
= '%s/%s' % (v6word
.network
, v6word
.prefixlen
)
559 norm_word
= '%s' % IPv6Address(word
)
564 norm_line
= norm_line
+ " " + norm_word
566 return norm_line
.strip()
569 def line_exist(lines
, target_ctx_keys
, target_line
):
570 for (ctx_keys
, line
) in lines
:
571 if ctx_keys
== target_ctx_keys
and line
== target_line
:
576 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
578 # Quite possibly the most confusing (while accurate) variable names in history
579 lines_to_add_to_del
= []
580 lines_to_del_to_del
= []
582 for (ctx_keys
, line
) in lines_to_del
:
585 if ctx_keys
[0].startswith('router bgp') and line
and line
.startswith('neighbor '):
587 BGP changed how it displays swpX peers that are part of peer-group. Older
588 versions of frr would display these on separate lines:
589 neighbor swp1 interface
590 neighbor swp1 peer-group FOO
592 but today we display via a single line
593 neighbor swp1 interface peer-group FOO
595 This change confuses frr-reload.py so check to see if we are deleting
596 neighbor swp1 interface peer-group FOO
599 neighbor swp1 interface
600 neighbor swp1 peer-group FOO
602 If so then chop the del line and the corresponding add lines
605 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
606 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
608 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
609 swpx_interface
= None
610 swpx_peergroup
= None
612 if re_swpx_int_peergroup
:
613 swpx
= re_swpx_int_peergroup
.group(1)
614 peergroup
= re_swpx_int_peergroup
.group(2)
615 swpx_interface
= "neighbor %s interface" % swpx
616 elif re_swpx_int_v6only_peergroup
:
617 swpx
= re_swpx_int_v6only_peergroup
.group(1)
618 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
619 swpx_interface
= "neighbor %s interface v6only" % swpx
621 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
622 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
623 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
624 tmp_ctx_keys
= tuple(list(ctx_keys
))
626 if not found_add_swpx_peergroup
:
627 tmp_ctx_keys
= list(ctx_keys
)
628 tmp_ctx_keys
.append('address-family ipv4 unicast')
629 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
630 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
632 if not found_add_swpx_peergroup
:
633 tmp_ctx_keys
= list(ctx_keys
)
634 tmp_ctx_keys
.append('address-family ipv6 unicast')
635 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
636 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
638 if found_add_swpx_interface
and found_add_swpx_peergroup
:
640 lines_to_del_to_del
.append((ctx_keys
, line
))
641 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
642 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
645 In 3.0.1 we changed how we display neighbor interface command. Older
646 versions of frr would display the following:
647 neighbor swp1 interface
648 neighbor swp1 remote-as external
649 neighbor swp1 capability extended-nexthop
651 but today we display via a single line
652 neighbor swp1 interface remote-as external
654 and capability extended-nexthop is no longer needed because we
655 automatically enable it when the neighbor is of type interface.
657 This change confuses frr-reload.py so check to see if we are deleting
658 neighbor swp1 interface remote-as (external|internal|ASNUM)
661 neighbor swp1 interface
662 neighbor swp1 remote-as (external|internal|ASNUM)
663 neighbor swp1 capability extended-nexthop
665 If so then chop the del line and the corresponding add lines
667 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
668 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
670 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
671 swpx_interface
= None
674 if re_swpx_int_remoteas
:
675 swpx
= re_swpx_int_remoteas
.group(1)
676 remoteas
= re_swpx_int_remoteas
.group(2)
677 swpx_interface
= "neighbor %s interface" % swpx
678 elif re_swpx_int_v6only_remoteas
:
679 swpx
= re_swpx_int_v6only_remoteas
.group(1)
680 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
681 swpx_interface
= "neighbor %s interface v6only" % swpx
683 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
684 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
685 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
686 tmp_ctx_keys
= tuple(list(ctx_keys
))
688 if found_add_swpx_interface
and found_add_swpx_remoteas
:
690 lines_to_del_to_del
.append((ctx_keys
, line
))
691 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
692 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
695 In 3.0, we made bgp bestpath multipath as-relax command
696 automatically assume no-as-set since the lack of this option caused
697 weird routing problems and this problem was peculiar to this
698 implementation. When the running config is shown in relases after
699 3.0, the no-as-set is not shown as its the default. This causes
700 reload to unnecessarily unapply this option to only apply it back
701 again, causing unnecessary session resets. Handle this.
703 if ctx_keys
[0].startswith('router bgp') and line
and 'multipath-relax' in line
:
704 re_asrelax_new
= re
.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line
)
705 old_asrelax_cmd
= 'bgp bestpath as-path multipath-relax no-as-set'
706 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
708 if re_asrelax_new
and found_asrelax_old
:
710 lines_to_del_to_del
.append((ctx_keys
, line
))
711 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
714 More old-to-new config handling. ip import-table no longer accepts
715 distance, but we honor the old syntax. But 'show running' shows only
716 the new syntax. This causes an unnecessary 'no import-table' followed
717 by the same old 'ip import-table' which causes perturbations in
718 announced routes leading to traffic blackholes. Fix this issue.
720 re_importtbl
= re
.search('^ip\s+import-table\s+(\d+)$', ctx_keys
[0])
722 table_num
= re_importtbl
.group(1)
723 for ctx
in lines_to_add
:
724 if ctx
[0][0].startswith('ip import-table %s distance' % table_num
):
725 lines_to_del_to_del
.append((('ip import-table %s' % table_num
,), None))
726 lines_to_add_to_del
.append((ctx
[0], None))
729 ip/ipv6 prefix-list can be specified without a seq number. However,
730 the running config always adds 'seq x', where x is a number incremented
731 by 5 for every element, to the prefix list. So, ignore such lines as
732 well. Sample prefix-list lines:
733 ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
734 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
735 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
737 re_ip_pfxlst
= re
.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
740 tmpline
= (re_ip_pfxlst
.group(1) + re_ip_pfxlst
.group(2) +
741 re_ip_pfxlst
.group(3) + re_ip_pfxlst
.group(5) +
742 re_ip_pfxlst
.group(6))
743 for ctx
in lines_to_add
:
744 if ctx
[0][0] == tmpline
:
745 lines_to_del_to_del
.append((ctx_keys
, None))
746 lines_to_add_to_del
.append(((tmpline
,), None))
749 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
752 lines_to_del_to_del
.append((ctx_keys
, line
))
753 lines_to_add_to_del
.append((ctx_keys
, line
))
756 We have commands that used to be displayed in the global part
757 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
761 neighbor ISL advertisement-interval 0
767 address-family ipv4 unicast
768 neighbor ISL advertisement-interval 0
770 Look to see if we are deleting it in one format just to add it back in the other
772 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
773 tmp_ctx_keys
= list(ctx_keys
)[:-1]
774 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
776 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
779 lines_to_del_to_del
.append((ctx_keys
, line
))
780 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
782 for (ctx_keys
, line
) in lines_to_del_to_del
:
783 lines_to_del
.remove((ctx_keys
, line
))
785 for (ctx_keys
, line
) in lines_to_add_to_del
:
786 lines_to_add
.remove((ctx_keys
, line
))
788 return (lines_to_add
, lines_to_del
)
791 def compare_context_objects(newconf
, running
):
793 Create a context diff for the two specified contexts
796 # Compare the two Config objects to find the lines that we need to add/del
801 # Find contexts that are in newconf but not in running
802 # Find contexts that are in running but not in newconf
803 for (running_ctx_keys
, running_ctx
) in running
.contexts
.iteritems():
805 if running_ctx_keys
not in newconf
.contexts
:
807 # We check that the len is 1 here so that we only look at ('router bgp 10')
808 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
809 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
810 # running but not in newconf.
811 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
813 lines_to_del
.append((running_ctx_keys
, None))
815 # We cannot do 'no interface' in quagga, and so deal with it
816 elif running_ctx_keys
[0].startswith('interface'):
817 for line
in running_ctx
.lines
:
818 lines_to_del
.append((running_ctx_keys
, line
))
820 # If this is an address-family under 'router bgp' and we are already deleting the
821 # entire 'router bgp' context then ignore this sub-context
822 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
825 elif ("router bgp" in running_ctx_keys
[0] and
826 len(running_ctx_keys
) > 1 and
827 running_ctx_keys
[1].startswith('address-family')):
828 # There's no 'no address-family' support and so we have to
829 # delete each line individually again
830 for line
in running_ctx
.lines
:
831 lines_to_del
.append((running_ctx_keys
, line
))
834 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
835 lines_to_del
.append((running_ctx_keys
, None))
839 for line
in running_ctx
.lines
:
840 lines_to_del
.append((running_ctx_keys
, line
))
842 # Find the lines within each context to add
843 # Find the lines within each context to del
844 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
846 if newconf_ctx_keys
in running
.contexts
:
847 running_ctx
= running
.contexts
[newconf_ctx_keys
]
849 for line
in newconf_ctx
.lines
:
850 if line
not in running_ctx
.dlines
:
851 lines_to_add
.append((newconf_ctx_keys
, line
))
853 for line
in running_ctx
.lines
:
854 if line
not in newconf_ctx
.dlines
:
855 lines_to_del
.append((newconf_ctx_keys
, line
))
857 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
859 if newconf_ctx_keys
not in running
.contexts
:
860 lines_to_add
.append((newconf_ctx_keys
, None))
862 for line
in newconf_ctx
.lines
:
863 lines_to_add
.append((newconf_ctx_keys
, line
))
865 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
867 return (lines_to_add
, lines_to_del
)
869 if __name__
== '__main__':
870 # Command line options
871 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
872 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
873 group
= parser
.add_mutually_exclusive_group(required
=True)
874 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
875 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
876 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
877 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
878 parser
.add_argument('filename', help='Location of new frr config file')
879 parser
.add_argument('--overwrite', action
='store_true', help='Overwrite frr.conf with running config output', default
=False)
880 args
= parser
.parse_args()
883 # For --test log to stdout
884 # For --reload log to /var/log/frr/frr-reload.log
885 if args
.test
or args
.stdout
:
886 logging
.basicConfig(level
=logging
.INFO
,
887 format
='%(asctime)s %(levelname)5s: %(message)s')
889 # Color the errors and warnings in red
890 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
891 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
894 if not os
.path
.isdir('/var/log/frr/'):
895 os
.makedirs('/var/log/frr/')
897 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
899 format
='%(asctime)s %(levelname)5s: %(message)s')
901 # argparse should prevent this from happening but just to be safe...
903 raise Exception('Must specify --reload or --test')
904 log
= logging
.getLogger(__name__
)
906 # Verify the new config file is valid
907 if not os
.path
.isfile(args
.filename
):
908 msg
= "Filename %s does not exist" % args
.filename
913 if not os
.path
.getsize(args
.filename
):
914 msg
= "Filename %s is an empty file" % args
.filename
919 # Verify that 'service integrated-vtysh-config' is configured
920 vtysh_filename
= '/etc/frr/vtysh.conf'
921 service_integrated_vtysh_config
= True
923 if os
.path
.isfile(vtysh_filename
):
924 with
open(vtysh_filename
, 'r') as fh
:
925 for line
in fh
.readlines():
928 if line
== 'no service integrated-vtysh-config':
929 service_integrated_vtysh_config
= False
932 if not service_integrated_vtysh_config
:
933 msg
= "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
939 log
.setLevel(logging
.DEBUG
)
941 log
.info('Called via "%s"', str(args
))
943 # Create a Config object from the config generated by newconf
945 newconf
.load_from_file(args
.filename
)
950 # Create a Config object from the running config
954 running
.load_from_file(args
.input)
956 running
.load_from_show_running()
958 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
959 lines_to_configure
= []
962 print "\nLines To Delete"
963 print "==============="
965 for (ctx_keys
, line
) in lines_to_del
:
970 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
971 lines_to_configure
.append(cmd
)
975 print "\nLines To Add"
978 for (ctx_keys
, line
) in lines_to_add
:
983 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
984 lines_to_configure
.append(cmd
)
989 log
.debug('New Frr Config\n%s', newconf
.get_lines())
991 # This looks a little odd but we have to do this twice...here is why
992 # If the user had this running bgp config:
995 # neighbor 1.1.1.1 remote-as 50
996 # neighbor 1.1.1.1 route-map FOO out
998 # and this config in the newconf config file
1001 # neighbor 1.1.1.1 remote-as 999
1002 # neighbor 1.1.1.1 route-map FOO out
1005 # Then the script will do
1006 # - no neighbor 1.1.1.1 remote-as 50
1007 # - neighbor 1.1.1.1 remote-as 999
1009 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
1010 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
1011 # configs again to put this line back.
1013 # There are many keywords in quagga that can only appear one time under
1014 # a context, take "bgp router-id" for example. If the config that we are
1015 # reloading against has the following:
1018 # bgp router-id 1.1.1.1
1019 # bgp router-id 2.2.2.2
1021 # The final config needs to contain "bgp router-id 2.2.2.2". On the
1022 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
1023 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
1024 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
1025 # second pass to include all of the "adds" from the first pass.
1026 lines_to_add_first_pass
= []
1030 running
.load_from_show_running()
1031 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
1033 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1036 lines_to_add_first_pass
= lines_to_add
1038 lines_to_add
.extend(lines_to_add_first_pass
)
1040 # Only do deletes on the first pass. The reason being if we
1041 # configure a bgp neighbor via "neighbor swp1 interface" quagga
1042 # will automatically add:
1045 # ipv6 nd ra-interval 10
1046 # no ipv6 nd suppress-ra
1049 # but those lines aren't in the config we are reloading against so
1050 # on the 2nd pass they will show up in lines_to_del. This could
1051 # apply to other scenarios as well where configuring FOO adds BAR
1053 if lines_to_del
and x
== 0:
1054 for (ctx_keys
, line
) in lines_to_del
:
1059 # 'no' commands are tricky, we can't just put them in a file and
1060 # vtysh -f that file. See the next comment for an explanation
1062 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
1065 # Some commands in frr are picky about taking a "no" of the entire line.
1066 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
1067 # only the beginning. If we hit one of these command an exception will be
1068 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
1071 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
1072 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
1073 # % Unknown command.
1074 # frr(config-if)# no ip ospf authentication message-digest
1075 # % Unknown command.
1076 # frr(config-if)# no ip ospf authentication
1081 _
= subprocess
.check_output(cmd
)
1083 except subprocess
.CalledProcessError
:
1085 # - Pull the last entry from cmd (this would be
1086 # 'no ip ospf authentication message-digest 1.1.1.1' in
1088 # - Split that last entry by whitespace and drop the last word
1089 log
.info('Failed to execute %s', ' '.join(cmd
))
1090 last_arg
= cmd
[-1].split(' ')
1092 if len(last_arg
) <= 2:
1093 log
.error('"%s" we failed to remove this command', original_cmd
)
1096 new_last_arg
= last_arg
[0:-1]
1097 cmd
[-1] = ' '.join(new_last_arg
)
1099 log
.info('Executed "%s"', ' '.join(cmd
))
1103 lines_to_configure
= []
1105 for (ctx_keys
, line
) in lines_to_add
:
1110 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
1111 lines_to_configure
.append(cmd
)
1113 if lines_to_configure
:
1114 random_string
= ''.join(random
.SystemRandom().choice(
1115 string
.ascii_uppercase
+
1116 string
.digits
) for _
in range(6))
1118 filename
= "/var/run/frr/reload-%s.txt" % random_string
1119 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
1121 with
open(filename
, 'w') as fh
:
1122 for line
in lines_to_configure
:
1123 fh
.write(line
+ '\n')
1125 output
= subprocess
.check_output(['/usr/bin/vtysh', '-f', filename
])
1127 # exit non-zero if we see these errors
1128 for x
in ('BGP instance name and AS number mismatch',
1129 'BGP instance is already running',
1130 '% not a local address'):
1131 for line
in output
.splitlines():
1133 msg
= "ERROR: %s" % x
1140 # Make these changes persistent
1141 if args
.overwrite
or args
.filename
!= '/etc/frr/frr.conf':
1142 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])