# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
import re
from time import sleep
from django.db.models import Q
from django.template import Context
from django.template import Template
from networkapi.api_deploy import exceptions as deploy_exc
from networkapi.api_equipment.exceptions import \
AllEquipmentsAreInMaintenanceException
from networkapi.api_neighbor.models import NeighborV4
from networkapi.api_neighbor.models import NeighborV6
from networkapi.equipamento import models as eqpt_models
from networkapi.extra_logging import local
from networkapi.extra_logging import NO_REQUEST_ID
from networkapi.infrastructure.ipaddr import IPAddress
from networkapi.plugins import exceptions as plugin_exc
from networkapi.plugins.base import BasePlugin
from networkapi.settings import BGP_CONFIG_FILES_PATH
from networkapi.settings import BGP_CONFIG_TEMPLATE_PATH
from networkapi.settings import BGP_CONFIG_TOAPPLY_REL_PATH
from networkapi.settings import TFTPBOOT_FILES_PATH
log = logging.getLogger(__name__)
[docs]class Generic(BasePlugin):
TEMPLATE_NEIGHBOR_V4_ADD = 'neighbor_v4_add'
TEMPLATE_NEIGHBOR_V4_REMOVE = 'neighbor_v4_remove'
TEMPLATE_NEIGHBOR_V6_ADD = 'neighbor_v6_add'
TEMPLATE_NEIGHBOR_V6_REMOVE = 'neighbor_v6_remove'
TEMPLATE_LIST_CONFIG_ADD = 'list_config_add'
TEMPLATE_LIST_CONFIG_REMOVE = 'list_config_remove'
TEMPLATE_ROUTE_MAP_ADD = 'route_map_add'
TEMPLATE_ROUTE_MAP_REMOVE = 'route_map_remove'
MAX_TRIES = 10
RETRY_WAIT_TIME = 5
WAIT_FOR_CLI_RETURN = 1
CURRENTLY_BUSY_WAIT = 'Currently busy with copying a file'
INVALID_REGEX = '([Ii]nvalid)|overlaps with'
WARNING_REGEX = 'config ignored|Warning'
ERROR_REGEX = '[Ee][Rr][Rr][Oo][Rr]|[Ff]ail|\%|utility is occupied'
management_vrf = 'management'
admin_privileges = 15
VALID_TFTP_PUT_MESSAGE = 'bytes successfully copied'
def _deploy_pre_req(self, neighbor):
# Concatenate RouteMapEntries Lists
route_map_in = neighbor.peer_group.route_map_in
route_map_out = neighbor.peer_group.route_map_out
rms = route_map_in.route_map_entries | \
route_map_out.route_map_entries
for rm_entry in rms:
list_config_bgp = rm_entry.list_config_bgp
if not list_config_bgp.equipments.filter(id=self.equipment.id):
self.deploy_list_config_bgp(list_config_bgp)
if not route_map_in.equipments.filter(id=self.equipment.id):
self.deploy_route_map(neighbor.peer_group.route_map_in)
if not route_map_out.equipments.filter(id=self.equipment.id):
self.deploy_route_map(neighbor.peer_group.route_map_out)
def _undeploy_pre_req(self, neighbor, ip_version):
# Concatenate RouteMapEntries Lists
route_map_in = neighbor.peer_group.route_map_in
route_map_out = neighbor.peer_group.route_map_out
# Route Map IN
neighbors_v4 = NeighborV4.objects.filter(Q(
Q(peer_group__route_map_in=route_map_in) |
Q(peer_group__route_map_out=route_map_in))
).filter(created=True)
neighbors_v6 = NeighborV6.objects.filter(Q(
Q(peer_group__route_map_in=route_map_in) |
Q(peer_group__route_map_out=route_map_in))
).filter(created=True)
if ip_version == 6:
neighbors_v6.filter(
~Q(id=neighbor.id)
)
else:
neighbors_v4.filter(
~Q(id=neighbor.id)
)
if not neighbors_v4 and not neighbors_v6:
if route_map_in.equipments.filter(id=self.equipment.id):
self.undeploy_route_map(route_map_in)
# Route Map OUT
neighbors_v4 = NeighborV4.objects.filter(Q(
Q(peer_group__route_map_in=route_map_out) |
Q(peer_group__route_map_out=route_map_out))
).filter(created=True)
neighbors_v6 = NeighborV6.objects.filter(Q(
Q(peer_group__route_map_in=route_map_out) |
Q(peer_group__route_map_out=route_map_out))
).filter(created=True)
if ip_version == 6:
neighbors_v6.filter(
~Q(id=neighbor.id)
)
else:
neighbors_v4.filter(
~Q(id=neighbor.id)
)
if not neighbors_v4 and not neighbors_v6:
if route_map_out.equipments.filter(id=self.equipment.id):
self.undeploy_route_map(route_map_out)
# List Config BGP
if not neighbors_v4 and not neighbors_v6:
rms = route_map_in.route_map_entries | \
route_map_out.route_map_entries
for rm_entry in rms:
list_config_bgp = rm_entry.list_config_bgp
neighbors_v6 = NeighborV6.objects.filter(Q(
Q(peer_group__route_map_in__route_map_entries__list_config_bgp=list_config_bgp) |
Q(peer_group__route_map_out__route_map_entries__list_config_bgp=list_config_bgp))
).filter(created=True)
neighbors_v4 = NeighborV6.objects.filter(Q(
Q(peer_group__route_map_in__route_map_entries__list_config_bgp=list_config_bgp) |
Q(peer_group__route_map_out__route_map_entries__list_config_bgp=list_config_bgp))
).filter(created=True)
if ip_version == 6:
neighbors_v6.filter(
~Q(id=neighbor.id)
)
else:
neighbors_v4.filter(
~Q(id=neighbor.id)
)
if not neighbors_v4 and not neighbors_v6:
if not list_config_bgp.equipments.filter(id=self.equipment.id):
self.undeploy_list_config_bgp(list_config_bgp)
[docs] def deploy_neighbor(self, neighbor):
"""Deploy neighbor"""
self._deploy_pre_req(neighbor)
ip_version = IPAddress(str(neighbor.remote_ip)).version
template_type = self.TEMPLATE_NEIGHBOR_V4_ADD if ip_version == 4 else \
self.TEMPLATE_NEIGHBOR_V6_ADD
config = self._generate_template_dict_neighbor(neighbor)
self._operate_equipment('neighbor', template_type, config)
[docs] def undeploy_neighbor(self, neighbor):
"""Undeploy neighbor"""
ip_version = IPAddress(str(neighbor.remote_ip)).version
self._undeploy_pre_req(neighbor, ip_version)
template_type = self.TEMPLATE_NEIGHBOR_V4_REMOVE \
if ip_version == 4 else self.TEMPLATE_NEIGHBOR_V6_REMOVE
config = self._generate_template_dict_neighbor(neighbor)
self._operate_equipment('neighbor', template_type, config)
[docs] def deploy_list_config_bgp(self, list_config_bgp):
"""Deploy prefix list"""
config = self._generate_template_dict_list_config_bgp(list_config_bgp)
self._operate_equipment(
'list_config_bgp', self.TEMPLATE_LIST_CONFIG_ADD, config)
[docs] def undeploy_list_config_bgp(self, list_config_bgp):
"""Undeploy prefix list"""
config = self._generate_template_dict_list_config_bgp(list_config_bgp)
self._operate_equipment(
'list_config_bgp', self.TEMPLATE_LIST_CONFIG_REMOVE, config)
[docs] def deploy_route_map(self, route_map):
"""Deploy route map"""
config = self._generate_template_dict_route_map(route_map)
self._operate_equipment(
'route_map', self.TEMPLATE_ROUTE_MAP_ADD, config)
[docs] def undeploy_route_map(self, route_map):
"""Undeploy route map"""
config = self._generate_template_dict_route_map(route_map)
self._operate_equipment(
'route_map', self.TEMPLATE_ROUTE_MAP_REMOVE, config)
def _operate_equipment(self, type, template_type, config):
self.connect()
self._ensure_privilege_level()
file_to_deploy = self._generate_config_file(
type, template_type, config)
self._deploy_config_in_equipment(file_to_deploy)
self.close()
############
# TEMPLATE #
############
def _generate_config_file(self, type, template_type, config):
"""Load a template and write a file with the rended output.
Returns: filename with relative path to settings.TFTPBOOT_FILES_PATH
"""
request_id = getattr(local, 'request_id', NO_REQUEST_ID)
filename_out = 'bgp_{}_{}_config_{}'.format(
type, self.equipment.id, request_id)
filename = BGP_CONFIG_FILES_PATH + filename_out
rel_file_to_deploy = BGP_CONFIG_TOAPPLY_REL_PATH + filename_out
config = self._get_template_config(template_type, config)
self._save_config(filename, config)
return rel_file_to_deploy
def _get_template_config(self, template_type, config):
"""Load template file and render values in VARs"""
try:
template_file = self._load_template_file(template_type)
config_to_be_saved = template_file.render(Context(config))
except KeyError as err:
log.error('Error: %s', err)
raise deploy_exc.InvalidKeyException(err)
except Exception as err:
log.error('Error: %s ' % err)
raise plugin_exc.BGPTemplateException(err)
return config_to_be_saved
def _load_template_file(self, template_type):
"""Load template file with specific type related to equipment.
template_type: Type of template to be loaded
Returns: template string
"""
equipment_template = self._get_equipment_template(template_type)
filename = BGP_CONFIG_TEMPLATE_PATH + '/' + equipment_template.roteiro.roteiro
template_file = self._read_config(filename)
return template_file
def _get_equipment_template(self, template_type):
"""Return a script by equipment and template_type"""
try:
return eqpt_models.EquipamentoRoteiro.search(
None, self.equipment.id, template_type).uniqueResult()
except:
log.error('Template type %s not found.' % template_type)
raise plugin_exc.BGPTemplateException()
def _generate_template_dict_neighbor(self, neighbor):
"""Make a dictionary to use in template"""
key_dict = {
'AS_NUMBER': neighbor.local_asn.name,
'VRF_NAME': neighbor.remote_ip.networkipv4.vlan.ambiente.default_vrf.internal_name,
'REMOTE_IP': str(neighbor.remote_ip),
'REMOTE_AS': neighbor.remote_asn.name,
'ROUTE_MAP_IN': neighbor.peer_group.route_map_in.name,
'ROUTE_MAP_OUT': neighbor.peer_group.route_map_out.name,
'PASSWORD': neighbor.password,
'TIMER_KEEPALIVE': neighbor.timer_keepalive,
'TIMER_TIMEOUT': neighbor.timer_timeout,
'DESCRIPTION': neighbor.description,
'SOFT_RECONFIGURATION': neighbor.soft_reconfiguration,
'NEXT_HOP_SELF': neighbor.next_hop_self,
'REMOVE_PRIVATE_AS': neighbor.remove_private_as,
'COMMUNITY': neighbor.community
}
return key_dict
def _generate_template_dict_list_config_bgp(self, list_config_bgp):
"""Make a dictionary to use in template"""
key_dict = {
'TYPE': self._get_type_list(list_config_bgp.type)['config_list'],
'NAME': list_config_bgp.name,
'CONFIG': list_config_bgp.config
}
return key_dict
def _generate_template_dict_route_map(self, route_map):
"""Make a dictionary to use in template"""
entries = []
for entry_obj in route_map.route_map_entries:
action = 'permit' if entry_obj.action == 'P' else 'deny'
entry = {
'ACTION': action,
'ORDER': entry_obj.order,
'TYPE_MATCH': self._get_type_list(entry_obj.list_config_bgp.type)['route_map'],
'LIST': entry_obj.list_config_bgp.name,
'ACTION_RECONFIG': entry_obj.action_reconfig
}
entries.append(entry)
key_dict = {
'NAME': route_map.name,
'ENTRIES': entries
}
return key_dict
def _get_type_list(self, type):
types = {
'P': {
'config_list': 'prefix-list',
'route_map': 'ip address prefix-list'
},
'C': {
'config_list': 'community',
'route_map': ''
},
}
return types[type]
def _read_config(self, filename):
"""Return content from template_file"""
try:
file_handle = open(filename, 'r')
template_content = Template(file_handle.read())
file_handle.close()
except IOError, e:
log.error('Error opening template file for read: %s' % filename)
raise Exception(e)
except Exception, e:
log.error('Syntax error when parsing template: %s ' % e)
raise Exception(e)
return template_content
def _save_config(self, filename, config):
"""Write config in template file"""
try:
file_handle = open(filename, 'w')
file_handle.write(config)
file_handle.close()
except IOError, e:
log.error('Error writing to config file: %s' % filename)
raise e
##########
# DEPLOY #
##########
def _deploy_config_in_equipment(self, rel_filename):
path = os.path.abspath(TFTPBOOT_FILES_PATH + rel_filename)
if not path.startswith(TFTPBOOT_FILES_PATH):
raise deploy_exc.InvalidFilenameException(rel_filename)
return self._apply_config(rel_filename)
def _apply_config(self, filename):
if self.equipment.maintenance:
raise AllEquipmentsAreInMaintenanceException()
self._copy_script_file_to_config(filename)
def _copy_script_file_to_config(self, filename,
destination='running-config'):
"""
Copy file from TFTP server to destination
By default, plugin should apply file in running configuration (active)
"""
command = 'copy tftp://{}/{} {} vrf {}\n\n'.format(
self.tftpserver, filename, destination, self.management_vrf)
file_copied = 0
retries = 0
while(not file_copied and retries < self.MAX_TRIES):
if retries is not 0:
sleep(self.RETRY_WAIT_TIME)
try:
log.info('try: %s - sending command: %s' % (retries, command))
self.channel.send('%s\n' % command)
recv = self._wait_string(self.VALID_TFTP_PUT_MESSAGE)
file_copied = 1
except plugin_exc.CurrentlyBusyErrorException:
retries += 1
# not capable of configuring after max retries
if retries is self.MAX_TRIES:
raise plugin_exc.CurrentlyBusyErrorException()
return recv
def _ensure_privilege_level(self, privilege_level=None):
if privilege_level is None:
privilege_level = self.admin_privileges
self.channel.send('\n')
recv = self._wait_string('>|#')
self.channel.send('show privilege\n')
recv = self._wait_string('Current privilege level is')
level = re.search(
'Current privilege level is ([0-9]+).*', recv, re.DOTALL).group(1)
level = (level.split(' '))[-1]
if int(level) < privilege_level:
self.channel.send('enable\n')
recv = self._wait_string('Password:')
self.channel.send('%s\n' % self.equipment_access.enable_pass)
recv = self._wait_string('#')
def _wait_string(self, wait_str_ok_regex='', wait_str_invalid_regex=None,
wait_str_failed_regex=None):
"""As equipment goes returning a string, makes a regex and verifies if string wished was returned."""
if wait_str_invalid_regex is None:
wait_str_invalid_regex = self.INVALID_REGEX
if wait_str_failed_regex is None:
wait_str_failed_regex = self.ERROR_REGEX
string_ok = 0
recv_string = ''
while not string_ok:
while not self.channel.recv_ready():
sleep(self.WAIT_FOR_CLI_RETURN)
recv_string = self.channel.recv(9999)
file_name_string = self.removeDisallowedChars(recv_string)
for output_line in recv_string.splitlines():
if re.search(self.CURRENTLY_BUSY_WAIT, output_line):
log.warning('Need to wait - Switch busy: %s' % output_line)
raise plugin_exc.CurrentlyBusyErrorException()
elif re.search(self.WARNING_REGEX, output_line):
log.warning('Equipment warning: %s' % output_line)
elif re.search(wait_str_invalid_regex, output_line):
log.error('Equipment raised INVALID error: %s' %
output_line)
raise deploy_exc.InvalidCommandException(file_name_string)
elif re.search(wait_str_failed_regex, output_line):
log.error('Equipment raised FAILED error: %s' %
output_line)
raise deploy_exc.CommandErrorException(file_name_string)
elif re.search(wait_str_ok_regex, output_line):
log.debug('Equipment output: %s' % output_line)
# test bug switch copying 0 bytes
if output_line == '0 bytes successfully copied':
log.debug('Switch copied 0 bytes, need to try again.')
raise plugin_exc.CurrentlyBusyErrorException()
string_ok = 1
return recv_string