text file
"""
+from __future__ import print_function, unicode_literals
import argparse
import copy
import logging
import subprocess
import sys
from collections import OrderedDict
-from ipaddr import IPv6Address, IPNetwork
+try:
+ from ipaddress import IPv6Address, ip_network
+except ImportError:
+ from ipaddr import IPv6Address, IPNetwork
from pprint import pformat
+try:
+ dict.iteritems
+except AttributeError:
+ # Python 3
+ def iteritems(d):
+ return iter(d.items())
+else:
+ # Python 2
+ def iteritems(d):
+ return d.iteritems()
log = logging.getLogger(__name__)
ve.output = e.output
raise ve
- for line in file_output.split('\n'):
+ for line in file_output.decode('utf-8').split('\n'):
line = line.strip()
+
+ # Compress duplicate whitespaces
+ line = ' '.join(line.split())
+
if ":" in line:
qv6_line = get_normalized_ipv6_line(line)
self.lines.append(qv6_line)
ve.output = e.output
raise ve
- for line in config_text.split('\n'):
+ for line in config_text.decode('utf-8').split('\n'):
line = line.strip()
if (line == 'Building configuration...' or
Return the parsed context as strings for display, log etc.
"""
- for (_, ctx) in sorted(self.contexts.iteritems()):
- print str(ctx) + '\n'
+ for (_, ctx) in sorted(iteritems(self.contexts)):
+ print(str(ctx) + '\n')
def save_contexts(self, key, lines):
"""
addr = re_key_rt.group(2)
if '/' in addr:
try:
- newaddr = IPNetwork(addr)
- key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
- newaddr.network,
- newaddr.prefixlen,
- re_key_rt.group(3))
+ if 'ipaddress' not in sys.modules:
+ newaddr = IPNetwork(addr)
+ key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
+ newaddr.network,
+ newaddr.prefixlen,
+ re_key_rt.group(3))
+ else:
+ newaddr = ip_network(addr, strict=False)
+ key[0] = '%s route %s/%s%s' % (re_key_rt.group(1),
+ str(newaddr.network_address),
+ newaddr.prefixlen,
+ re_key_rt.group(3))
except ValueError:
pass
addr = re_key_rt.group(4)
if '/' in addr:
try:
- newaddr = '%s/%s' % (IPNetwork(addr).network,
- IPNetwork(addr).prefixlen)
+ if 'ipaddress' not in sys.modules:
+ newaddr = '%s/%s' % (IPNetwork(addr).network,
+ IPNetwork(addr).prefixlen)
+ else:
+ network_addr = ip_network(addr, strict=False)
+ newaddr = '%s/%s' % (str(network_addr.network_address),
+ network_addr.prefixlen)
except ValueError:
newaddr = addr
else:
addr = addr + '/8'
try:
- newaddr = IPNetwork(addr)
- line = 'network %s/%s %s' % (newaddr.network,
- newaddr.prefixlen,
- re_net.group(2))
+ if 'ipaddress' not in sys.modules:
+ newaddr = IPNetwork(addr)
+ line = 'network %s/%s %s' % (newaddr.network,
+ newaddr.prefixlen,
+ re_net.group(2))
+ else:
+ network_addr = ip_network(addr, strict=False)
+ line = 'network %s/%s %s' % (str(network_addr.network_address),
+ network_addr.prefixlen,
+ re_net.group(2))
newlines.append(line)
except ValueError:
# Really this should be an error. Whats a network
self.save_contexts(ctx_keys, current_context_lines)
new_ctx = True
- elif line == "end":
+ elif line in ["end", "exit-vrf"]:
self.save_contexts(ctx_keys, current_context_lines)
log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
ctx_keys = []
current_context_lines = []
- elif line == "exit-address-family" or line == "exit" or line == "exit-vni":
+ elif line in ["exit-address-family", "exit", "exit-vnc"]:
# if this exit is for address-family ipv4 unicast, ignore the pop
if main_ctx_key:
self.save_contexts(ctx_keys, current_context_lines)
current_context_lines = []
log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys)
+ elif line == "exit-vni":
+ if sub_main_ctx_key:
+ self.save_contexts(ctx_keys, current_context_lines)
+
+ # Start a new context
+ ctx_keys = copy.deepcopy(sub_main_ctx_key)
+ current_context_lines = []
+ log.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line, ctx_keys)
+
elif new_ctx is True:
if not main_ctx_key:
ctx_keys = [line, ]
current_context_lines = []
new_ctx = False
log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
-
- elif "vni " in line:
- main_ctx_key = []
-
- # Save old context first
- self.save_contexts(ctx_keys, current_context_lines)
- current_context_lines = []
- main_ctx_key = copy.deepcopy(ctx_keys)
- log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line)
-
- ctx_keys.append(line)
-
- elif "address-family " in line:
+ elif (line.startswith("address-family ") or
+ line.startswith("vnc defaults") or
+ line.startswith("vnc l2-group") or
+ line.startswith("vnc nve-group")):
main_ctx_key = []
# Save old context first
else:
ctx_keys.append(line)
+ elif ((line.startswith("vni ") and
+ len(ctx_keys) == 2 and
+ ctx_keys[0].startswith('router bgp') and
+ ctx_keys[1] == 'address-family l2vpn evpn')):
+
+ # Save old context first
+ self.save_contexts(ctx_keys, current_context_lines)
+ current_context_lines = []
+ sub_main_ctx_key = copy.deepcopy(ctx_keys)
+ log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line)
+ ctx_keys.append(line)
+
else:
# Continuing in an existing context, add non-commented lines to it
current_context_lines.append(line)
norm_word = None
if "/" in word:
try:
- v6word = IPNetwork(word)
- norm_word = '%s/%s' % (v6word.network, v6word.prefixlen)
+ if 'ipaddress' not in sys.modules:
+ v6word = IPNetwork(word)
+ norm_word = '%s/%s' % (v6word.network, v6word.prefixlen)
+ else:
+ v6word = ip_network(word, strict=False)
+ norm_word = '%s/%s' % (str(v6word.network_address), v6word.prefixlen)
except ValueError:
pass
if not norm_word:
return norm_line.strip()
-def line_exist(lines, target_ctx_keys, target_line):
+def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
for (ctx_keys, line) in lines:
- if ctx_keys == target_ctx_keys and line == target_line:
- return True
+ if ctx_keys == target_ctx_keys:
+ if exact_match:
+ if line == target_line:
+ return True
+ else:
+ if line.startswith(target_line):
+ return True
return False
for (ctx_keys, line) in lines_to_del:
deleted = False
- if ctx_keys[0].startswith('router bgp') and line and line.startswith('neighbor '):
- """
- BGP changed how it displays swpX peers that are part of peer-group. Older
- versions of frr would display these on separate lines:
- neighbor swp1 interface
- neighbor swp1 peer-group FOO
-
- but today we display via a single line
- neighbor swp1 interface peer-group FOO
-
- This change confuses frr-reload.py so check to see if we are deleting
- neighbor swp1 interface peer-group FOO
-
- and adding
- neighbor swp1 interface
- neighbor swp1 peer-group FOO
-
- If so then chop the del line and the corresponding add lines
- """
-
- re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
- re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
-
- if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
- swpx_interface = None
- swpx_peergroup = None
-
- if re_swpx_int_peergroup:
- swpx = re_swpx_int_peergroup.group(1)
- peergroup = re_swpx_int_peergroup.group(2)
- swpx_interface = "neighbor %s interface" % swpx
- elif re_swpx_int_v6only_peergroup:
- swpx = re_swpx_int_v6only_peergroup.group(1)
- peergroup = re_swpx_int_v6only_peergroup.group(2)
- swpx_interface = "neighbor %s interface v6only" % swpx
-
- swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
- found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
- found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
- tmp_ctx_keys = tuple(list(ctx_keys))
-
- if not found_add_swpx_peergroup:
- tmp_ctx_keys = list(ctx_keys)
- tmp_ctx_keys.append('address-family ipv4 unicast')
- tmp_ctx_keys = tuple(tmp_ctx_keys)
- found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
+ if ctx_keys[0].startswith('router bgp') and line:
+
+ if line.startswith('neighbor '):
+ '''
+ BGP changed how it displays swpX peers that are part of peer-group. Older
+ versions of frr would display these on separate lines:
+ neighbor swp1 interface
+ neighbor swp1 peer-group FOO
+
+ but today we display via a single line
+ neighbor swp1 interface peer-group FOO
+
+ This change confuses frr-reload.py so check to see if we are deleting
+ neighbor swp1 interface peer-group FOO
+
+ and adding
+ neighbor swp1 interface
+ neighbor swp1 peer-group FOO
+
+ If so then chop the del line and the corresponding add lines
+ '''
+
+ re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line)
+ re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line)
+
+ if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup:
+ swpx_interface = None
+ swpx_peergroup = None
+
+ if re_swpx_int_peergroup:
+ swpx = re_swpx_int_peergroup.group(1)
+ peergroup = re_swpx_int_peergroup.group(2)
+ swpx_interface = "neighbor %s interface" % swpx
+ elif re_swpx_int_v6only_peergroup:
+ swpx = re_swpx_int_v6only_peergroup.group(1)
+ peergroup = re_swpx_int_v6only_peergroup.group(2)
+ swpx_interface = "neighbor %s interface v6only" % swpx
+
+ swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup)
+ found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
+ found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup)
+ tmp_ctx_keys = tuple(list(ctx_keys))
if not found_add_swpx_peergroup:
tmp_ctx_keys = list(ctx_keys)
- tmp_ctx_keys.append('address-family ipv6 unicast')
+ tmp_ctx_keys.append('address-family ipv4 unicast')
tmp_ctx_keys = tuple(tmp_ctx_keys)
found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
- if found_add_swpx_interface and found_add_swpx_peergroup:
+ if not found_add_swpx_peergroup:
+ tmp_ctx_keys = list(ctx_keys)
+ tmp_ctx_keys.append('address-family ipv6 unicast')
+ tmp_ctx_keys = tuple(tmp_ctx_keys)
+ found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup)
+
+ if found_add_swpx_interface and found_add_swpx_peergroup:
+ deleted = True
+ lines_to_del_to_del.append((ctx_keys, line))
+ lines_to_add_to_del.append((ctx_keys, swpx_interface))
+ lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
+
+ '''
+ We changed how we display the neighbor interface command. Older
+ versions of frr would display the following:
+ neighbor swp1 interface
+ neighbor swp1 remote-as external
+ neighbor swp1 capability extended-nexthop
+
+ but today we display via a single line
+ neighbor swp1 interface remote-as external
+
+ and capability extended-nexthop is no longer needed because we
+ automatically enable it when the neighbor is of type interface.
+
+ This change confuses frr-reload.py so check to see if we are deleting
+ neighbor swp1 interface remote-as (external|internal|ASNUM)
+
+ and adding
+ neighbor swp1 interface
+ neighbor swp1 remote-as (external|internal|ASNUM)
+ neighbor swp1 capability extended-nexthop
+
+ If so then chop the del line and the corresponding add lines
+ '''
+ re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
+ re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
+
+ if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
+ swpx_interface = None
+ swpx_remoteas = None
+
+ if re_swpx_int_remoteas:
+ swpx = re_swpx_int_remoteas.group(1)
+ remoteas = re_swpx_int_remoteas.group(2)
+ swpx_interface = "neighbor %s interface" % swpx
+ elif re_swpx_int_v6only_remoteas:
+ swpx = re_swpx_int_v6only_remoteas.group(1)
+ remoteas = re_swpx_int_v6only_remoteas.group(2)
+ swpx_interface = "neighbor %s interface v6only" % swpx
+
+ swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
+ found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
+ found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
+ tmp_ctx_keys = tuple(list(ctx_keys))
+
+ if found_add_swpx_interface and found_add_swpx_remoteas:
+ deleted = True
+ lines_to_del_to_del.append((ctx_keys, line))
+ lines_to_add_to_del.append((ctx_keys, swpx_interface))
+ lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
+
+ '''
+ We made the 'bgp bestpath as-path multipath-relax' command
+ automatically assume 'no-as-set' since the lack of this option caused
+ weird routing problems. When the running config is shown in
+ releases with this change, the no-as-set keyword is not shown as it
+ is the default. This causes frr-reload to unnecessarily unapply
+ this option only to apply it back again, causing unnecessary session
+ resets.
+ '''
+ if 'multipath-relax' in line:
+ re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line)
+ old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set'
+ found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
+
+ if re_asrelax_new and found_asrelax_old:
deleted = True
lines_to_del_to_del.append((ctx_keys, line))
- lines_to_add_to_del.append((ctx_keys, swpx_interface))
- lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup))
-
- """
- In 3.0.1 we changed how we display neighbor interface command. Older
- versions of frr would display the following:
- neighbor swp1 interface
- neighbor swp1 remote-as external
- neighbor swp1 capability extended-nexthop
-
- but today we display via a single line
- neighbor swp1 interface remote-as external
-
- and capability extended-nexthop is no longer needed because we
- automatically enable it when the neighbor is of type interface.
-
- This change confuses frr-reload.py so check to see if we are deleting
- neighbor swp1 interface remote-as (external|internal|ASNUM)
-
- and adding
- neighbor swp1 interface
- neighbor swp1 remote-as (external|internal|ASNUM)
- neighbor swp1 capability extended-nexthop
-
- If so then chop the del line and the corresponding add lines
- """
- re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line)
- re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line)
-
- if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas:
- swpx_interface = None
- swpx_remoteas = None
-
- if re_swpx_int_remoteas:
- swpx = re_swpx_int_remoteas.group(1)
- remoteas = re_swpx_int_remoteas.group(2)
- swpx_interface = "neighbor %s interface" % swpx
- elif re_swpx_int_v6only_remoteas:
- swpx = re_swpx_int_v6only_remoteas.group(1)
- remoteas = re_swpx_int_v6only_remoteas.group(2)
- swpx_interface = "neighbor %s interface v6only" % swpx
-
- swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas)
- found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface)
- found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas)
- tmp_ctx_keys = tuple(list(ctx_keys))
-
- if found_add_swpx_interface and found_add_swpx_remoteas:
- deleted = True
+ lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
+
+ '''
+ If we are modifying the BGP table-map we need to avoid a del/add and
+ instead modify the table-map in place via an add. This is needed to
+ avoid installing all routes in the RIB the second the 'no table-map'
+ is issued.
+ '''
+ if line.startswith('table-map'):
+ found_table_map = line_exist(lines_to_add, ctx_keys, 'table-map', False)
+
+ if found_table_map:
lines_to_del_to_del.append((ctx_keys, line))
- lines_to_add_to_del.append((ctx_keys, swpx_interface))
- lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas))
-
- '''
- In 3.0, we made bgp bestpath multipath as-relax command
- automatically assume no-as-set since the lack of this option caused
- weird routing problems and this problem was peculiar to this
- implementation. When the running config is shown in relases after
- 3.0, the no-as-set is not shown as its the default. This causes
- reload to unnecessarily unapply this option to only apply it back
- again, causing unnecessary session resets. Handle this.
- '''
- if ctx_keys[0].startswith('router bgp') and line and 'multipath-relax' in line:
- re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line)
- old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set'
- found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd)
-
- if re_asrelax_new and found_asrelax_old:
- deleted = True
- lines_to_del_to_del.append((ctx_keys, line))
- lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd))
'''
- More old-to-new config handling. ip import-table no longer accepts
- distance, but we honor the old syntax. But 'show running' shows only
- the new syntax. This causes an unnecessary 'no import-table' followed
- by the same old 'ip import-table' which causes perturbations in
- announced routes leading to traffic blackholes. Fix this issue.
+ More old-to-new config handling. ip import-table no longer accepts
+ distance, but we honor the old syntax. But 'show running' shows only
+ the new syntax. This causes an unnecessary 'no import-table' followed
+ by the same old 'ip import-table' which causes perturbations in
+ announced routes leading to traffic blackholes. Fix this issue.
'''
re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0])
if re_importtbl:
return (lines_to_add, lines_to_del)
+def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
+ """
+ There are certain commands that cannot be removed. Remove
+ those commands from lines_to_del.
+ """
+ lines_to_del_to_del = []
+
+ for (ctx_keys, line) in lines_to_del:
+
+ if (ctx_keys[0].startswith('frr version') or
+ ctx_keys[0].startswith('frr defaults') or
+ ctx_keys[0].startswith('password') or
+ ctx_keys[0].startswith('line vty') or
+
+ # This is technically "no"able but if we did so frr-reload would
+ # stop working so do not let the user shoot themselves in the foot
+ # by removing this.
+ ctx_keys[0].startswith('service integrated-vtysh-config')):
+
+ log.info("(%s, %s) cannot be removed" % (pformat(ctx_keys), line))
+ lines_to_del_to_del.append((ctx_keys, line))
+
+ for (ctx_keys, line) in lines_to_del_to_del:
+ lines_to_del.remove((ctx_keys, line))
+
+ return (lines_to_add, lines_to_del)
+
+
def compare_context_objects(newconf, running):
"""
Create a context diff for the two specified contexts
# Find contexts that are in newconf but not in running
# Find contexts that are in running but not in newconf
- for (running_ctx_keys, running_ctx) in running.contexts.iteritems():
+ for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
if running_ctx_keys not in newconf.contexts:
# Find the lines within each context to add
# Find the lines within each context to del
- for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
+ for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
if newconf_ctx_keys in running.contexts:
running_ctx = running.contexts[newconf_ctx_keys]
if line not in newconf_ctx.dlines:
lines_to_del.append((newconf_ctx_keys, line))
- for (newconf_ctx_keys, newconf_ctx) in newconf.contexts.iteritems():
+ for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
if newconf_ctx_keys not in running.contexts:
lines_to_add.append((newconf_ctx_keys, None))
lines_to_add.append((newconf_ctx_keys, line))
(lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del)
+ (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del)
return (lines_to_add, lines_to_del)
cmd = ['/usr/bin/vtysh', '-c', 'conf t']
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).strip()
- if 'VTY configuration is locked by other VTY' in output:
- print output
+ if 'VTY configuration is locked by other VTY' in output.decode('utf-8'):
+ print(output)
log.error("'%s' returned\n%s\n" % (' '.join(cmd), output))
return False
except subprocess.CalledProcessError as e:
msg = "vtysh could not connect with any frr daemons"
- print msg
+ print(msg)
log.error(msg)
return False
# Verify the new config file is valid
if not os.path.isfile(args.filename):
msg = "Filename %s does not exist" % args.filename
- print msg
+ print(msg)
log.error(msg)
sys.exit(1)
if not os.path.getsize(args.filename):
msg = "Filename %s is an empty file" % args.filename
- print msg
+ print(msg)
log.error(msg)
sys.exit(1)
if not service_integrated_vtysh_config:
msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
- print msg
+ print(msg)
log.error(msg)
sys.exit(1)
lines_to_configure = []
if lines_to_del:
- print "\nLines To Delete"
- print "==============="
+ print("\nLines To Delete")
+ print("===============")
for (ctx_keys, line) in lines_to_del:
cmd = line_for_vtysh_file(ctx_keys, line, True)
lines_to_configure.append(cmd)
- print cmd
+ print(cmd)
if lines_to_add:
- print "\nLines To Add"
- print "============"
+ print("\nLines To Add")
+ print("============")
for (ctx_keys, line) in lines_to_add:
cmd = line_for_vtysh_file(ctx_keys, line, False)
lines_to_configure.append(cmd)
- print cmd
+ print(cmd)
elif args.reload: