mirror of
				https://git.proxmox.com/git/mirror_frr
				synced 2025-10-31 15:11:11 +00:00 
			
		
		
		
	 55c8666a92
			
		
	
	
		55c8666a92
		
	
	
	
	
		
			
			Add `allow-external-route-update` and `domainname` to the one line context list, otherwise reload will fail when those commands show up in the running configuration. Signed-off-by: Rafael Zalamena <rzalamena@opensourcerouting.org>
		
			
				
	
	
		
			1375 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1375 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/python
 | |
| # Frr Reloader
 | |
| # Copyright (C) 2014 Cumulus Networks, Inc.
 | |
| #
 | |
| # This file is part of Frr.
 | |
| #
 | |
| # Frr is free software; you can redistribute it and/or modify it
 | |
| # under the terms of the GNU General Public License as published by the
 | |
| # Free Software Foundation; either version 2, or (at your option) any
 | |
| # later version.
 | |
| #
 | |
| # Frr is distributed in the hope that it will be useful, but
 | |
| # WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | |
| # General Public License for more details.
 | |
| #
 | |
| # You should have received a copy of the GNU General Public License
 | |
| # along with Frr; see the file COPYING.  If not, write to the Free
 | |
| # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 | |
| #  02111-1307, USA.
 | |
| #
 | |
| """
 | |
| This program
 | |
| - reads a frr configuration text file
 | |
| - reads frr's current running configuration via "vtysh -c 'show running'"
 | |
| - compares the two configs and determines what commands to execute to
 | |
|   synchronize frr's running configuration with the configuation in the
 | |
|   text file
 | |
| """
 | |
| 
 | |
| from __future__ import print_function, unicode_literals
 | |
| import argparse
 | |
| import copy
 | |
| import logging
 | |
| import os
 | |
| import random
 | |
| import re
 | |
| import string
 | |
| import subprocess
 | |
| import sys
 | |
| from collections import OrderedDict
 | |
| 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__)
 | |
| 
 | |
| 
 | |
| class VtyshMarkException(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class Context(object):
 | |
| 
 | |
|     """
 | |
|     A Context object represents a section of frr configuration such as:
 | |
| !
 | |
| interface swp3
 | |
|  description swp3 -> r8's swp1
 | |
|  ipv6 nd suppress-ra
 | |
|  link-detect
 | |
| !
 | |
| 
 | |
| or a single line context object such as this:
 | |
| 
 | |
| ip forwarding
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __init__(self, keys, lines):
 | |
|         self.keys = keys
 | |
|         self.lines = lines
 | |
| 
 | |
|         # Keep a dictionary of the lines, this is to make it easy to tell if a
 | |
|         # line exists in this Context
 | |
|         self.dlines = OrderedDict()
 | |
| 
 | |
|         for ligne in lines:
 | |
|             self.dlines[ligne] = True
 | |
| 
 | |
|     def add_lines(self, lines):
 | |
|         """
 | |
|         Add lines to specified context
 | |
|         """
 | |
| 
 | |
|         self.lines.extend(lines)
 | |
| 
 | |
|         for ligne in lines:
 | |
|             self.dlines[ligne] = True
 | |
| 
 | |
| 
 | |
| class Config(object):
 | |
| 
 | |
|     """
 | |
|     A frr configuration is stored in a Config object. A Config object
 | |
|     contains a dictionary of Context objects where the Context keys
 | |
|     ('router ospf' for example) are our dictionary key.
 | |
|     """
 | |
| 
 | |
|     def __init__(self):
 | |
|         self.lines = []
 | |
|         self.contexts = OrderedDict()
 | |
| 
 | |
|     def load_from_file(self, filename):
 | |
|         """
 | |
|         Read configuration from specified file and slurp it into internal memory
 | |
|         The internal representation has been marked appropriately by passing it
 | |
|         through vtysh with the -m parameter
 | |
|         """
 | |
|         log.info('Loading Config object from file %s', filename)
 | |
| 
 | |
|         try:
 | |
|             file_output = subprocess.check_output(['/usr/bin/vtysh', '-m', '-f', filename],
 | |
|                                                   stderr=subprocess.STDOUT)
 | |
|         except subprocess.CalledProcessError as e:
 | |
|             ve = VtyshMarkException(e)
 | |
|             ve.output = e.output
 | |
|             raise ve
 | |
| 
 | |
|         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)
 | |
|             else:
 | |
|                 self.lines.append(line)
 | |
| 
 | |
|         self.load_contexts()
 | |
| 
 | |
|     def load_from_show_running(self):
 | |
|         """
 | |
|         Read running configuration and slurp it into internal memory
 | |
|         The internal representation has been marked appropriately by passing it
 | |
|         through vtysh with the -m parameter
 | |
|         """
 | |
|         log.info('Loading Config object from vtysh show running')
 | |
| 
 | |
|         try:
 | |
|             config_text = subprocess.check_output(
 | |
|                 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
 | |
|                 shell=True, stderr=subprocess.STDOUT)
 | |
|         except subprocess.CalledProcessError as e:
 | |
|             ve = VtyshMarkException(e)
 | |
|             ve.output = e.output
 | |
|             raise ve
 | |
| 
 | |
|         for line in config_text.decode('utf-8').split('\n'):
 | |
|             line = line.strip()
 | |
| 
 | |
|             if (line == 'Building configuration...' or
 | |
|                 line == 'Current configuration:' or
 | |
|                     not line):
 | |
|                 continue
 | |
| 
 | |
|             self.lines.append(line)
 | |
| 
 | |
|         self.load_contexts()
 | |
| 
 | |
|     def get_lines(self):
 | |
|         """
 | |
|         Return the lines read in from the configuration
 | |
|         """
 | |
| 
 | |
|         return '\n'.join(self.lines)
 | |
| 
 | |
|     def get_contexts(self):
 | |
|         """
 | |
|         Return the parsed context as strings for display, log etc.
 | |
|         """
 | |
| 
 | |
|         for (_, ctx) in sorted(iteritems(self.contexts)):
 | |
|             print(str(ctx) + '\n')
 | |
| 
 | |
|     def save_contexts(self, key, lines):
 | |
|         """
 | |
|         Save the provided key and lines as a context
 | |
|         """
 | |
| 
 | |
|         if not key:
 | |
|             return
 | |
| 
 | |
|         '''
 | |
|             IP addresses specified in "network" statements, "ip prefix-lists"
 | |
|             etc. can differ in the host part of the specification the user
 | |
|             provides and what the running config displays. For example, user
 | |
|             can specify 11.1.1.1/24, and the running config displays this as
 | |
|             11.1.1.0/24. Ensure we don't do a needless operation for such
 | |
|             lines. IS-IS & OSPFv3 have no "network" support.
 | |
|         '''
 | |
|         re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0])
 | |
|         if re_key_rt:
 | |
|             addr = re_key_rt.group(2)
 | |
|             if '/' in addr:
 | |
|                 try:
 | |
|                     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
 | |
| 
 | |
|         re_key_rt = re.match(
 | |
|             r'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$',
 | |
|             key[0]
 | |
|         )
 | |
|         if re_key_rt:
 | |
|             addr = re_key_rt.group(4)
 | |
|             if '/' in addr:
 | |
|                 try:
 | |
|                     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:
 | |
|                 newaddr = addr
 | |
| 
 | |
|             legestr = re_key_rt.group(5)
 | |
|             re_lege = re.search(r'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr)
 | |
|             if re_lege:
 | |
|                 legestr = '%sge %s le %s%s' % (re_lege.group(1),
 | |
|                                                re_lege.group(3),
 | |
|                                                re_lege.group(2),
 | |
|                                                re_lege.group(4))
 | |
|             re_lege = re.search(r'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr)
 | |
| 
 | |
|             if (re_lege and ((re_key_rt.group(1) == "ip" and
 | |
|                               re_lege.group(3) == "32") or
 | |
|                              (re_key_rt.group(1) == "ipv6" and
 | |
|                               re_lege.group(3) == "128"))):
 | |
|                 legestr = '%sge %s%s' % (re_lege.group(1),
 | |
|                                          re_lege.group(2),
 | |
|                                          re_lege.group(4))
 | |
| 
 | |
|             key[0] = '%s prefix-list%s%s %s%s' % (re_key_rt.group(1),
 | |
|                                                   re_key_rt.group(2),
 | |
|                                                   re_key_rt.group(3),
 | |
|                                                   newaddr,
 | |
|                                                   legestr)
 | |
| 
 | |
|         if lines and key[0].startswith('router bgp'):
 | |
|             newlines = []
 | |
|             for line in lines:
 | |
|                 re_net = re.match(r'network\s+([A-Fa-f:.0-9/]+)(.*)$', line)
 | |
|                 if re_net:
 | |
|                     addr = re_net.group(1)
 | |
|                     if '/' not in addr and key[0].startswith('router bgp'):
 | |
|                         # This is most likely an error because with no
 | |
|                         # prefixlen, BGP treats the prefixlen as 8
 | |
|                         addr = addr + '/8'
 | |
| 
 | |
|                     try:
 | |
|                         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
 | |
|                         # without an IP Address following it ?
 | |
|                         newlines.append(line)
 | |
|                 else:
 | |
|                     newlines.append(line)
 | |
|             lines = newlines
 | |
| 
 | |
|         '''
 | |
|           More fixups in user specification and what running config shows.
 | |
|           "null0" in routes must be replaced by Null0.
 | |
|         '''
 | |
|         if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and
 | |
|                 'null0' in key[0]):
 | |
|             key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0])
 | |
| 
 | |
|         if lines:
 | |
|             if tuple(key) not in self.contexts:
 | |
|                 ctx = Context(tuple(key), lines)
 | |
|                 self.contexts[tuple(key)] = ctx
 | |
|             else:
 | |
|                 ctx = self.contexts[tuple(key)]
 | |
|                 ctx.add_lines(lines)
 | |
| 
 | |
|         else:
 | |
|             if tuple(key) not in self.contexts:
 | |
|                 ctx = Context(tuple(key), [])
 | |
|                 self.contexts[tuple(key)] = ctx
 | |
| 
 | |
|     def load_contexts(self):
 | |
|         """
 | |
|         Parse the configuration and create contexts for each appropriate block
 | |
|         """
 | |
| 
 | |
|         current_context_lines = []
 | |
|         ctx_keys = []
 | |
| 
 | |
|         '''
 | |
|         The end of a context is flagged via the 'end' keyword:
 | |
| 
 | |
| !
 | |
| interface swp52
 | |
|  ipv6 nd suppress-ra
 | |
|  link-detect
 | |
| !
 | |
| end
 | |
| router bgp 10
 | |
|  bgp router-id 10.0.0.1
 | |
|  bgp log-neighbor-changes
 | |
|  no bgp default ipv4-unicast
 | |
|  neighbor EBGP peer-group
 | |
|  neighbor EBGP advertisement-interval 1
 | |
|  neighbor EBGP timers connect 10
 | |
|  neighbor 2001:40:1:4::6 remote-as 40
 | |
|  neighbor 2001:40:1:8::a remote-as 40
 | |
| !
 | |
| end
 | |
|  address-family ipv6
 | |
|  neighbor IBGPv6 activate
 | |
|  neighbor 2001:10::2 peer-group IBGPv6
 | |
|  neighbor 2001:10::3 peer-group IBGPv6
 | |
|  exit-address-family
 | |
| !
 | |
| end
 | |
|  address-family evpn
 | |
|   neighbor LEAF activate
 | |
|   advertise-all-vni
 | |
|   vni 10100
 | |
|    rd 65000:10100
 | |
|    route-target import 10.1.1.1:10100
 | |
|    route-target export 10.1.1.1:10100
 | |
|   exit-vni
 | |
|  exit-address-family
 | |
| !
 | |
| end
 | |
| router ospf
 | |
|  ospf router-id 10.0.0.1
 | |
|  log-adjacency-changes detail
 | |
|  timers throttle spf 0 50 5000
 | |
| !
 | |
| end
 | |
|         '''
 | |
| 
 | |
|         # The code assumes that its working on the output from the "vtysh -m"
 | |
|         # command. That provides the appropriate markers to signify end of
 | |
|         # a context. This routine uses that to build the contexts for the
 | |
|         # config.
 | |
|         #
 | |
|         # There are single line contexts such as "log file /media/node/zebra.log"
 | |
|         # and multi-line contexts such as "router ospf" and subcontexts
 | |
|         # within a context such as "address-family" within "router bgp"
 | |
|         # In each of these cases, the first line of the context becomes the
 | |
|         # key of the context. So "router bgp 10" is the key for the non-address
 | |
|         # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
 | |
|         # the key for the subcontext and so on.
 | |
|         ctx_keys = []
 | |
|         main_ctx_key = []
 | |
|         new_ctx = True
 | |
| 
 | |
|         # the keywords that we know are single line contexts. bgp in this case
 | |
|         # is not the main router bgp block, but enabling multi-instance
 | |
|         oneline_ctx_keywords = ("access-list ",
 | |
|                                 "agentx",
 | |
|                                 "allow-external-route-update",
 | |
|                                 "bgp ",
 | |
|                                 "debug ",
 | |
|                                 "domainname ",
 | |
|                                 "dump ",
 | |
|                                 "enable ",
 | |
|                                 "frr ",
 | |
|                                 "hostname ",
 | |
|                                 "ip ",
 | |
|                                 "ipv6 ",
 | |
|                                 "log ",
 | |
|                                 "mpls",
 | |
|                                 "no ",
 | |
|                                 "password ",
 | |
|                                 "ptm-enable",
 | |
|                                 "router-id ",
 | |
|                                 "service ",
 | |
|                                 "table ",
 | |
|                                 "username ",
 | |
|                                 "zebra ",
 | |
|                                 "vrrp autoconfigure")
 | |
| 
 | |
|         for line in self.lines:
 | |
| 
 | |
|             if not line:
 | |
|                 continue
 | |
| 
 | |
|             if line.startswith('!') or line.startswith('#'):
 | |
|                 continue
 | |
| 
 | |
|             # one line contexts
 | |
|             if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords):
 | |
|                 self.save_contexts(ctx_keys, current_context_lines)
 | |
| 
 | |
|                 # Start a new context
 | |
|                 main_ctx_key = []
 | |
|                 ctx_keys = [line, ]
 | |
|                 current_context_lines = []
 | |
| 
 | |
|                 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
 | |
|                 self.save_contexts(ctx_keys, current_context_lines)
 | |
|                 new_ctx = True
 | |
| 
 | |
|             elif line == "end":
 | |
|                 self.save_contexts(ctx_keys, current_context_lines)
 | |
|                 log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys)
 | |
| 
 | |
|                 # Start a new context
 | |
|                 new_ctx = True
 | |
|                 main_ctx_key = []
 | |
|                 ctx_keys = []
 | |
|                 current_context_lines = []
 | |
| 
 | |
|             elif line == "exit-vrf":
 | |
|                 self.save_contexts(ctx_keys, current_context_lines)
 | |
|                 current_context_lines.append(line)
 | |
|                 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
 | |
| 
 | |
|                 #Start a new context
 | |
|                 new_ctx = True
 | |
|                 main_ctx_key = []
 | |
|                 ctx_keys = []
 | |
|                 current_context_lines = []
 | |
| 
 | |
|             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)
 | |
| 
 | |
|                     # Start a new context
 | |
|                     ctx_keys = copy.deepcopy(main_ctx_key)
 | |
|                     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, ]
 | |
|                 else:
 | |
|                     ctx_keys = copy.deepcopy(main_ctx_key)
 | |
|                     main_ctx_key = []
 | |
| 
 | |
|                 current_context_lines = []
 | |
|                 new_ctx = False
 | |
|                 log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys)
 | |
|             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
 | |
|                 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)
 | |
| 
 | |
|                 if line == "address-family ipv6":
 | |
|                     ctx_keys.append("address-family ipv6 unicast")
 | |
|                 elif line == "address-family ipv4":
 | |
|                     ctx_keys.append("address-family ipv4 unicast")
 | |
|                 elif line == "address-family evpn":
 | |
|                     ctx_keys.append("address-family l2vpn evpn")
 | |
|                 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)
 | |
|                 log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys)
 | |
| 
 | |
|         # Save the context of the last one
 | |
|         self.save_contexts(ctx_keys, current_context_lines)
 | |
| 
 | |
| 
 | |
| def line_to_vtysh_conft(ctx_keys, line, delete):
 | |
|     """
 | |
|     Return the vtysh command for the specified context line
 | |
|     """
 | |
| 
 | |
|     cmd = []
 | |
|     cmd.append('vtysh')
 | |
|     cmd.append('-c')
 | |
|     cmd.append('conf t')
 | |
| 
 | |
|     if line:
 | |
|         for ctx_key in ctx_keys:
 | |
|             cmd.append('-c')
 | |
|             cmd.append(ctx_key)
 | |
| 
 | |
|         line = line.lstrip()
 | |
| 
 | |
|         if delete:
 | |
|             cmd.append('-c')
 | |
| 
 | |
|             if line.startswith('no '):
 | |
|                 cmd.append('%s' % line[3:])
 | |
|             else:
 | |
|                 cmd.append('no %s' % line)
 | |
| 
 | |
|         else:
 | |
|             cmd.append('-c')
 | |
|             cmd.append(line)
 | |
| 
 | |
|     # If line is None then we are typically deleting an entire
 | |
|     # context ('no router ospf' for example)
 | |
|     else:
 | |
| 
 | |
|         if delete:
 | |
| 
 | |
|             # Only put the 'no' on the last sub-context
 | |
|             for ctx_key in ctx_keys:
 | |
|                 cmd.append('-c')
 | |
| 
 | |
|                 if ctx_key == ctx_keys[-1]:
 | |
|                     cmd.append('no %s' % ctx_key)
 | |
|                 else:
 | |
|                     cmd.append('%s' % ctx_key)
 | |
|         else:
 | |
|             for ctx_key in ctx_keys:
 | |
|                 cmd.append('-c')
 | |
|                 cmd.append(ctx_key)
 | |
| 
 | |
|     return cmd
 | |
| 
 | |
| 
 | |
| def line_for_vtysh_file(ctx_keys, line, delete):
 | |
|     """
 | |
|     Return the command as it would appear in frr.conf
 | |
|     """
 | |
|     cmd = []
 | |
| 
 | |
|     if line:
 | |
|         for (i, ctx_key) in enumerate(ctx_keys):
 | |
|             cmd.append(' ' * i + ctx_key)
 | |
| 
 | |
|         line = line.lstrip()
 | |
|         indent = len(ctx_keys) * ' '
 | |
| 
 | |
|         if delete:
 | |
|             if line.startswith('no '):
 | |
|                 cmd.append('%s%s' % (indent, line[3:]))
 | |
|             else:
 | |
|                 cmd.append('%sno %s' % (indent, line))
 | |
| 
 | |
|         else:
 | |
|             cmd.append(indent + line)
 | |
| 
 | |
|     # If line is None then we are typically deleting an entire
 | |
|     # context ('no router ospf' for example)
 | |
|     else:
 | |
|         if delete:
 | |
| 
 | |
|             # Only put the 'no' on the last sub-context
 | |
|             for ctx_key in ctx_keys:
 | |
| 
 | |
|                 if ctx_key == ctx_keys[-1]:
 | |
|                     cmd.append('no %s' % ctx_key)
 | |
|                 else:
 | |
|                     cmd.append('%s' % ctx_key)
 | |
|         else:
 | |
|             for ctx_key in ctx_keys:
 | |
|                 cmd.append(ctx_key)
 | |
| 
 | |
|     cmd = '\n' + '\n'.join(cmd)
 | |
| 
 | |
|     # There are some commands that are on by default so their "no" form will be
 | |
|     # displayed in the config.  "no bgp default ipv4-unicast" is one of these.
 | |
|     # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
 | |
|     # not by doing a "no no bgp default ipv4-unicast"
 | |
|     cmd = cmd.replace('no no ', '')
 | |
| 
 | |
|     return cmd
 | |
| 
 | |
| 
 | |
| def get_normalized_ipv6_line(line):
 | |
|     """
 | |
|     Return a normalized IPv6 line as produced by frr,
 | |
|     with all letters in lower case and trailing and leading
 | |
|     zeros removed, and only the network portion present if
 | |
|     the IPv6 word is a network
 | |
|     """
 | |
|     norm_line = ""
 | |
|     words = line.split(' ')
 | |
|     for word in words:
 | |
|         if ":" in word:
 | |
|             norm_word = None
 | |
|             if "/" in word:
 | |
|                 try:
 | |
|                     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:
 | |
|                 try:
 | |
|                     norm_word = '%s' % IPv6Address(word)
 | |
|                 except ValueError:
 | |
|                     norm_word = word
 | |
|         else:
 | |
|             norm_word = word
 | |
|         norm_line = norm_line + " " + norm_word
 | |
| 
 | |
|     return norm_line.strip()
 | |
| 
 | |
| 
 | |
| def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
 | |
|     for (ctx_keys, line) in lines:
 | |
|         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
 | |
| 
 | |
| 
 | |
| def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
 | |
| 
 | |
|     # Quite possibly the most confusing (while accurate) variable names in history
 | |
|     lines_to_add_to_del = []
 | |
|     lines_to_del_to_del = []
 | |
| 
 | |
|     for (ctx_keys, line) in lines_to_del:
 | |
|         deleted = False
 | |
| 
 | |
|         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 ipv4 unicast')
 | |
|                         tmp_ctx_keys = tuple(tmp_ctx_keys)
 | |
|                         found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, 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))
 | |
| 
 | |
|                 '''
 | |
|                 Changing the bfd timers on neighbors is allowed without doing
 | |
|                 a delete/add process. Since doing a "no neighbor blah bfd ..."
 | |
|                 will cause the peer to bounce unnecessarily, just skip the delete
 | |
|                 and just do the add.
 | |
|                 '''
 | |
|                 re_nbr_bfd_timers = re.search(r'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line)
 | |
| 
 | |
|                 if re_nbr_bfd_timers:
 | |
|                     nbr = re_nbr_bfd_timers.group(1)
 | |
|                     bfd_nbr = "neighbor %s" % nbr
 | |
| 
 | |
|                     for (ctx_keys, add_line) in lines_to_add:
 | |
|                         re_add_nbr_bfd_timers = re.search(r'neighbor (\S+) bfd (\S+) (\S+) (\S+)', add_line)
 | |
| 
 | |
|                         if re_add_nbr_bfd_timers:
 | |
|                             found_add_bfd_nbr = line_exist(lines_to_add, ctx_keys, bfd_nbr, False)
 | |
| 
 | |
|                             if found_add_bfd_nbr:
 | |
|                                 lines_to_del_to_del.append((ctx_keys, line))
 | |
| 
 | |
|                 '''
 | |
|                 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, 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))
 | |
| 
 | |
|         '''
 | |
|         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:
 | |
|             table_num = re_importtbl.group(1)
 | |
|             for ctx in lines_to_add:
 | |
|                 if ctx[0][0].startswith('ip import-table %s distance' % table_num):
 | |
|                     lines_to_del_to_del.append((('ip import-table %s' % table_num,), None))
 | |
|                     lines_to_add_to_del.append((ctx[0], None))
 | |
| 
 | |
|         '''
 | |
|         ip/ipv6 prefix-list can be specified without a seq number. However,
 | |
|         the running config always adds 'seq x', where x is a number incremented
 | |
|         by 5 for every element, to the prefix list. So, ignore such lines as
 | |
|         well. Sample prefix-list lines:
 | |
|              ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
 | |
|              ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
 | |
|              ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
 | |
|         '''
 | |
|         re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$',
 | |
|                                  ctx_keys[0])
 | |
|         if re_ip_pfxlst:
 | |
|             tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) +
 | |
|                        re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) +
 | |
|                        re_ip_pfxlst.group(6))
 | |
|             for ctx in lines_to_add:
 | |
|                 if ctx[0][0] == tmpline:
 | |
|                     lines_to_del_to_del.append((ctx_keys, None))
 | |
|                     lines_to_add_to_del.append(((tmpline,), None))
 | |
| 
 | |
|         if (len(ctx_keys) == 3 and
 | |
|             ctx_keys[0].startswith('router bgp') and
 | |
|             ctx_keys[1] == 'address-family l2vpn evpn' and
 | |
|             ctx_keys[2].startswith('vni')):
 | |
| 
 | |
|             re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False
 | |
| 
 | |
|             if re_route_target:
 | |
|                 rt = re_route_target.group(1).strip()
 | |
|                 route_target_import_line = line
 | |
|                 route_target_export_line = "route-target export %s" % rt
 | |
|                 route_target_both_line = "route-target both %s" % rt
 | |
| 
 | |
|                 found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line)
 | |
|                 found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line)
 | |
| 
 | |
|                 '''
 | |
|                 If the running configs has
 | |
|                     route-target import 1:1
 | |
|                     route-target export 1:1
 | |
| 
 | |
|                 and the config we are reloading against has
 | |
|                     route-target both 1:1
 | |
| 
 | |
|                 then we can ignore deleting the import/export and ignore adding the 'both'
 | |
|                 '''
 | |
|                 if found_route_target_export_line and found_route_target_both_line:
 | |
|                     lines_to_del_to_del.append((ctx_keys, route_target_import_line))
 | |
|                     lines_to_del_to_del.append((ctx_keys, route_target_export_line))
 | |
|                     lines_to_add_to_del.append((ctx_keys, route_target_both_line))
 | |
| 
 | |
|         if not deleted:
 | |
|             found_add_line = line_exist(lines_to_add, ctx_keys, line)
 | |
| 
 | |
|             if found_add_line:
 | |
|                 lines_to_del_to_del.append((ctx_keys, line))
 | |
|                 lines_to_add_to_del.append((ctx_keys, line))
 | |
|             else:
 | |
|                 '''
 | |
|                 We have commands that used to be displayed in the global part
 | |
|                 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
 | |
| 
 | |
|                 # old way
 | |
|                 router bgp 64900
 | |
|                   neighbor ISL advertisement-interval 0
 | |
| 
 | |
|                 vs.
 | |
| 
 | |
|                 # new way
 | |
|                 router bgp 64900
 | |
|                   address-family ipv4 unicast
 | |
|                     neighbor ISL advertisement-interval 0
 | |
| 
 | |
|                 Look to see if we are deleting it in one format just to add it back in the other
 | |
|                 '''
 | |
|                 if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast':
 | |
|                     tmp_ctx_keys = list(ctx_keys)[:-1]
 | |
|                     tmp_ctx_keys = tuple(tmp_ctx_keys)
 | |
| 
 | |
|                     found_add_line = line_exist(lines_to_add, tmp_ctx_keys, line)
 | |
| 
 | |
|                     if found_add_line:
 | |
|                         lines_to_del_to_del.append((ctx_keys, line))
 | |
|                         lines_to_add_to_del.append((tmp_ctx_keys, line))
 | |
| 
 | |
|     for (ctx_keys, line) in lines_to_del_to_del:
 | |
|         lines_to_del.remove((ctx_keys, line))
 | |
| 
 | |
|     for (ctx_keys, line) in lines_to_add_to_del:
 | |
|         lines_to_add.remove((ctx_keys, line))
 | |
| 
 | |
|     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
 | |
|     """
 | |
| 
 | |
|     # Compare the two Config objects to find the lines that we need to add/del
 | |
|     lines_to_add = []
 | |
|     lines_to_del = []
 | |
|     delete_bgpd = False
 | |
| 
 | |
|     # 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 iteritems(running.contexts):
 | |
| 
 | |
|         if running_ctx_keys not in newconf.contexts:
 | |
| 
 | |
|             # We check that the len is 1 here so that we only look at ('router bgp 10')
 | |
|             # and not ('router bgp 10', 'address-family ipv4 unicast'). The
 | |
|             # latter could cause a false delete_bgpd positive if ipv4 unicast is in
 | |
|             # running but not in newconf.
 | |
|             if "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) == 1:
 | |
|                 delete_bgpd = True
 | |
|                 lines_to_del.append((running_ctx_keys, None))
 | |
| 
 | |
|             # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
 | |
|             elif running_ctx_keys[0].startswith('interface') or running_ctx_keys[0].startswith('vrf'):
 | |
|                 for line in running_ctx.lines:
 | |
|                     lines_to_del.append((running_ctx_keys, line))
 | |
| 
 | |
|             # If this is an address-family under 'router bgp' and we are already deleting the
 | |
|             # entire 'router bgp' context then ignore this sub-context
 | |
|             elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd:
 | |
|                 continue
 | |
| 
 | |
|             # Delete an entire vni sub-context under "address-family l2vpn evpn"
 | |
|             elif ("router bgp" in running_ctx_keys[0] and
 | |
|                   len(running_ctx_keys) > 2 and
 | |
|                   running_ctx_keys[1].startswith('address-family l2vpn evpn') and
 | |
|                   running_ctx_keys[2].startswith('vni ')):
 | |
|                 lines_to_del.append((running_ctx_keys, None))
 | |
| 
 | |
|             elif ("router bgp" in running_ctx_keys[0] and
 | |
|                   len(running_ctx_keys) > 1 and
 | |
|                   running_ctx_keys[1].startswith('address-family')):
 | |
|                 # There's no 'no address-family' support and so we have to
 | |
|                 # delete each line individually again
 | |
|                 for line in running_ctx.lines:
 | |
|                     lines_to_del.append((running_ctx_keys, line))
 | |
| 
 | |
|             # Non-global context
 | |
|             elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys):
 | |
|                 lines_to_del.append((running_ctx_keys, None))
 | |
| 
 | |
|             elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys):
 | |
|                 lines_to_del.append((running_ctx_keys, None))
 | |
| 
 | |
|             # Global context
 | |
|             else:
 | |
|                 for line in running_ctx.lines:
 | |
|                     lines_to_del.append((running_ctx_keys, line))
 | |
| 
 | |
|     # Find the lines within each context to add
 | |
|     # Find the lines within each context to del
 | |
|     for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
 | |
| 
 | |
|         if newconf_ctx_keys in running.contexts:
 | |
|             running_ctx = running.contexts[newconf_ctx_keys]
 | |
| 
 | |
|             for line in newconf_ctx.lines:
 | |
|                 if line not in running_ctx.dlines:
 | |
|                     lines_to_add.append((newconf_ctx_keys, line))
 | |
| 
 | |
|             for line in running_ctx.lines:
 | |
|                 if line not in newconf_ctx.dlines:
 | |
|                     lines_to_del.append((newconf_ctx_keys, line))
 | |
| 
 | |
|     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))
 | |
| 
 | |
|             for line in newconf_ctx.lines:
 | |
|                 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)
 | |
| 
 | |
| 
 | |
| 
 | |
| def vtysh_config_available():
 | |
|     """
 | |
|     Return False if no frr daemon is running or some other vtysh session is
 | |
|     in 'configuration terminal' mode which will prevent us from making any
 | |
|     configuration changes.
 | |
|     """
 | |
| 
 | |
|     try:
 | |
|         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.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)
 | |
|         log.error(msg)
 | |
|         return False
 | |
| 
 | |
|     return True
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     # Command line options
 | |
|     parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs')
 | |
|     parser.add_argument('--input', help='Read running config from file instead of "show running"')
 | |
|     group = parser.add_mutually_exclusive_group(required=True)
 | |
|     group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False)
 | |
|     group.add_argument('--test', action='store_true', help='Show the deltas', default=False)
 | |
|     parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False)
 | |
|     parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False)
 | |
|     parser.add_argument('filename', help='Location of new frr config file')
 | |
|     parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False)
 | |
|     args = parser.parse_args()
 | |
| 
 | |
|     # Logging
 | |
|     # For --test log to stdout
 | |
|     # For --reload log to /var/log/frr/frr-reload.log
 | |
|     if args.test or args.stdout:
 | |
|         logging.basicConfig(level=logging.INFO,
 | |
|                             format='%(asctime)s %(levelname)5s: %(message)s')
 | |
| 
 | |
|         # Color the errors and warnings in red
 | |
|         logging.addLevelName(logging.ERROR, "\033[91m  %s\033[0m" % logging.getLevelName(logging.ERROR))
 | |
|         logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
 | |
| 
 | |
|     elif args.reload:
 | |
|         if not os.path.isdir('/var/log/frr/'):
 | |
|             os.makedirs('/var/log/frr/')
 | |
| 
 | |
|         logging.basicConfig(filename='/var/log/frr/frr-reload.log',
 | |
|                             level=logging.INFO,
 | |
|                             format='%(asctime)s %(levelname)5s: %(message)s')
 | |
| 
 | |
|     # argparse should prevent this from happening but just to be safe...
 | |
|     else:
 | |
|         raise Exception('Must specify --reload or --test')
 | |
|     log = logging.getLogger(__name__)
 | |
| 
 | |
|     # Verify the new config file is valid
 | |
|     if not os.path.isfile(args.filename):
 | |
|         msg = "Filename %s does not exist" % args.filename
 | |
|         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)
 | |
|         log.error(msg)
 | |
|         sys.exit(1)
 | |
| 
 | |
|     # Verify that 'service integrated-vtysh-config' is configured
 | |
|     vtysh_filename = '/etc/frr/vtysh.conf'
 | |
|     service_integrated_vtysh_config = True
 | |
| 
 | |
|     if os.path.isfile(vtysh_filename):
 | |
|         with open(vtysh_filename, 'r') as fh:
 | |
|             for line in fh.readlines():
 | |
|                 line = line.strip()
 | |
| 
 | |
|                 if line == 'no service integrated-vtysh-config':
 | |
|                     service_integrated_vtysh_config = False
 | |
|                     break
 | |
| 
 | |
|     if not service_integrated_vtysh_config:
 | |
|         msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
 | |
|         print(msg)
 | |
|         log.error(msg)
 | |
|         sys.exit(1)
 | |
| 
 | |
|     if args.debug:
 | |
|         log.setLevel(logging.DEBUG)
 | |
| 
 | |
|     log.info('Called via "%s"', str(args))
 | |
| 
 | |
|     # Create a Config object from the config generated by newconf
 | |
|     newconf = Config()
 | |
|     newconf.load_from_file(args.filename)
 | |
|     reload_ok = True
 | |
| 
 | |
|     if args.test:
 | |
| 
 | |
|         # Create a Config object from the running config
 | |
|         running = Config()
 | |
| 
 | |
|         if args.input:
 | |
|             running.load_from_file(args.input)
 | |
|         else:
 | |
|             running.load_from_show_running()
 | |
| 
 | |
|         (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
 | |
|         lines_to_configure = []
 | |
| 
 | |
|         if lines_to_del:
 | |
|             print("\nLines To Delete")
 | |
|             print("===============")
 | |
| 
 | |
|             for (ctx_keys, line) in lines_to_del:
 | |
| 
 | |
|                 if line == '!':
 | |
|                     continue
 | |
| 
 | |
|                 cmd = line_for_vtysh_file(ctx_keys, line, True)
 | |
|                 lines_to_configure.append(cmd)
 | |
|                 print(cmd)
 | |
| 
 | |
|         if lines_to_add:
 | |
|             print("\nLines To Add")
 | |
|             print("============")
 | |
| 
 | |
|             for (ctx_keys, line) in lines_to_add:
 | |
| 
 | |
|                 if line == '!':
 | |
|                     continue
 | |
| 
 | |
|                 cmd = line_for_vtysh_file(ctx_keys, line, False)
 | |
|                 lines_to_configure.append(cmd)
 | |
|                 print(cmd)
 | |
| 
 | |
|     elif args.reload:
 | |
| 
 | |
|         # We will not be able to do anything, go ahead and exit(1)
 | |
|         if not vtysh_config_available():
 | |
|             sys.exit(1)
 | |
| 
 | |
|         log.debug('New Frr Config\n%s', newconf.get_lines())
 | |
| 
 | |
|         # This looks a little odd but we have to do this twice...here is why
 | |
|         # If the user had this running bgp config:
 | |
|         #
 | |
|         # router bgp 10
 | |
|         #  neighbor 1.1.1.1 remote-as 50
 | |
|         #  neighbor 1.1.1.1 route-map FOO out
 | |
|         #
 | |
|         # and this config in the newconf config file
 | |
|         #
 | |
|         # router bgp 10
 | |
|         #  neighbor 1.1.1.1 remote-as 999
 | |
|         #  neighbor 1.1.1.1 route-map FOO out
 | |
|         #
 | |
|         #
 | |
|         # Then the script will do
 | |
|         # - no neighbor 1.1.1.1 remote-as 50
 | |
|         # - neighbor 1.1.1.1 remote-as 999
 | |
|         #
 | |
|         # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
 | |
|         # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
 | |
|         # configs again to put this line back.
 | |
| 
 | |
|         # There are many keywords in FRR that can only appear one time under
 | |
|         # a context, take "bgp router-id" for example. If the config that we are
 | |
|         # reloading against has the following:
 | |
|         #
 | |
|         # router bgp 10
 | |
|         #   bgp router-id 1.1.1.1
 | |
|         #   bgp router-id 2.2.2.2
 | |
|         #
 | |
|         # The final config needs to contain "bgp router-id 2.2.2.2". On the
 | |
|         # first pass we will add "bgp router-id 2.2.2.2" but then on the second
 | |
|         # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
 | |
|         # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
 | |
|         # second pass to include all of the "adds" from the first pass.
 | |
|         lines_to_add_first_pass = []
 | |
| 
 | |
|         for x in range(2):
 | |
|             running = Config()
 | |
|             running.load_from_show_running()
 | |
|             log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
 | |
| 
 | |
|             (lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
 | |
| 
 | |
|             if x == 0:
 | |
|                 lines_to_add_first_pass = lines_to_add
 | |
|             else:
 | |
|                 lines_to_add.extend(lines_to_add_first_pass)
 | |
| 
 | |
|             # Only do deletes on the first pass. The reason being if we
 | |
|             # configure a bgp neighbor via "neighbor swp1 interface" FRR
 | |
|             # will automatically add:
 | |
|             #
 | |
|             # interface swp1
 | |
|             #  ipv6 nd ra-interval 10
 | |
|             #  no ipv6 nd suppress-ra
 | |
|             # !
 | |
|             #
 | |
|             # but those lines aren't in the config we are reloading against so
 | |
|             # on the 2nd pass they will show up in lines_to_del.  This could
 | |
|             # apply to other scenarios as well where configuring FOO adds BAR
 | |
|             # to the config.
 | |
|             if lines_to_del and x == 0:
 | |
|                 for (ctx_keys, line) in lines_to_del:
 | |
| 
 | |
|                     if line == '!':
 | |
|                         continue
 | |
| 
 | |
|                     # 'no' commands are tricky, we can't just put them in a file and
 | |
|                     # vtysh -f that file. See the next comment for an explanation
 | |
|                     # of their quirks
 | |
|                     cmd = line_to_vtysh_conft(ctx_keys, line, True)
 | |
|                     original_cmd = cmd
 | |
| 
 | |
|                     # Some commands in frr are picky about taking a "no" of the entire line.
 | |
|                     # OSPF is bad about this, you can't "no" the entire line, you have to "no"
 | |
|                     # only the beginning. If we hit one of these command an exception will be
 | |
|                     # thrown.  Catch it and remove the last '-c', 'FOO' from cmd and try again.
 | |
|                     #
 | |
|                     # Example:
 | |
|                     # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
 | |
|                     # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
 | |
|                     #  % Unknown command.
 | |
|                     # frr(config-if)# no ip ospf authentication message-digest
 | |
|                     #  % Unknown command.
 | |
|                     # frr(config-if)# no ip ospf authentication
 | |
|                     # frr(config-if)#
 | |
| 
 | |
|                     while True:
 | |
|                         try:
 | |
|                             _ = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
 | |
| 
 | |
|                         except subprocess.CalledProcessError:
 | |
| 
 | |
|                             # - Pull the last entry from cmd (this would be
 | |
|                             #   'no ip ospf authentication message-digest 1.1.1.1' in
 | |
|                             #   our example above
 | |
|                             # - Split that last entry by whitespace and drop the last word
 | |
|                             log.info('Failed to execute %s', ' '.join(cmd))
 | |
|                             last_arg = cmd[-1].split(' ')
 | |
| 
 | |
|                             if len(last_arg) <= 2:
 | |
|                                 log.error('"%s" we failed to remove this command', original_cmd)
 | |
|                                 break
 | |
| 
 | |
|                             new_last_arg = last_arg[0:-1]
 | |
|                             cmd[-1] = ' '.join(new_last_arg)
 | |
|                         else:
 | |
|                             log.info('Executed "%s"', ' '.join(cmd))
 | |
|                             break
 | |
| 
 | |
|             if lines_to_add:
 | |
|                 lines_to_configure = []
 | |
| 
 | |
|                 for (ctx_keys, line) in lines_to_add:
 | |
| 
 | |
|                     if line == '!':
 | |
|                         continue
 | |
| 
 | |
|                     cmd = line_for_vtysh_file(ctx_keys, line, False)
 | |
|                     lines_to_configure.append(cmd)
 | |
| 
 | |
|                 if lines_to_configure:
 | |
|                     random_string = ''.join(random.SystemRandom().choice(
 | |
|                                             string.ascii_uppercase +
 | |
|                                             string.digits) for _ in range(6))
 | |
| 
 | |
|                     filename = "/var/run/frr/reload-%s.txt" % random_string
 | |
|                     log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))
 | |
| 
 | |
|                     with open(filename, 'w') as fh:
 | |
|                         for line in lines_to_configure:
 | |
|                             fh.write(line + '\n')
 | |
| 
 | |
|                     try:
 | |
|                         subprocess.check_output(['/usr/bin/vtysh', '-f', filename], stderr=subprocess.STDOUT)
 | |
|                     except subprocess.CalledProcessError as e:
 | |
|                         log.warning("frr-reload.py failed due to\n%s" % e.output)
 | |
|                         reload_ok = False
 | |
|                     os.unlink(filename)
 | |
| 
 | |
|         # Make these changes persistent
 | |
|         if args.overwrite or args.filename != '/etc/frr/frr.conf':
 | |
|             subprocess.call(['/usr/bin/vtysh', '-c', 'write'])
 | |
| 
 | |
|     if not reload_ok:
 | |
|         sys.exit(1)
 |