From 6b4338cfdba108a0b3b0f51851da30044fe32450 Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Mon, 23 Nov 2020 13:35:08 -0300 Subject: [PATCH 01/13] remove a equipment associated to an as number --- networkapi/equipamento/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/networkapi/equipamento/models.py b/networkapi/equipamento/models.py index f6c87f9cd..17923276c 100644 --- a/networkapi/equipamento/models.py +++ b/networkapi/equipamento/models.py @@ -786,6 +786,9 @@ def delete(self): for equipment_group in self.equipamentogrupo_set.all(): equipment_group.delete() + for asn_equipment in self.asnequipment_set.all(): + asn_equipment.delete() + super(Equipamento, self).delete() def remove(self, authenticated_user, equip_id): @@ -835,6 +838,9 @@ def delete_v3(self): for equipment_group in self.equipamentogrupo_set.all(): equipment_group.delete() + for asn_equipment in self.asnequipment_set.all(): + asn_equipment.delete() + self.delete() def create_v3(self, equipment): From de2c0db2e789e7e8c27b4e67f6a5b5aeb72df8fa Mon Sep 17 00:00:00 2001 From: malinoski Date: Mon, 30 Nov 2020 15:24:24 -0300 Subject: [PATCH 02/13] Fix example environment to rigth type interface --- dev/load_example_environment.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/load_example_environment.sql b/dev/load_example_environment.sql index d81afb44c..c9a078397 100644 --- a/dev/load_example_environment.sql +++ b/dev/load_example_environment.sql @@ -1900,8 +1900,8 @@ VALUES -- -- Dumped data of the Interface types table -- -INSERT INTO `tipo_interface` (`tipo`) -VALUES ("access"), ("trunk"); +INSERT INTO `tipo_interface` (`id_tipo_interface`, `tipo`) VALUES ('1', 'access'); +INSERT INTO `tipo_interface` (`id_tipo_interface`, `tipo`) VALUES ('2', 'trunk'); From a0e6d28dcb1b07c37a6f7c22c37e82f1e6e83493 Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Wed, 10 Feb 2021 16:02:28 -0300 Subject: [PATCH 03/13] create juniper bgp plugin --- networkapi/plugins/Juniper/JUNOS/BGP/Cli.py | 533 ++++++++++++++++++ .../plugins/Juniper/JUNOS/BGP/__init__.py | 0 2 files changed, 533 insertions(+) create mode 100644 networkapi/plugins/Juniper/JUNOS/BGP/Cli.py create mode 100644 networkapi/plugins/Juniper/JUNOS/BGP/__init__.py diff --git a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py new file mode 100644 index 000000000..ba4b4a9a8 --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py @@ -0,0 +1,533 @@ +# -*- 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__) + + +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' + VALID_TFTP_PUT_MESSAGE_NXS6001 = 'Copy complete' + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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, types, template_type, config): + + self.connect() + self._ensure_privilege_level() + file_to_deploy = self._generate_config_file( + types, template_type, config) + self._deploy_config_in_equipment(file_to_deploy) + self.close() + + ############ + # TEMPLATE # + ############ + def _generate_config_file(self, types, 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( + types, 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 Exception as e: + log.error('Template type %s not found. Error: %s' % (template_type, e)) + 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) + """ + recv = None + + vrf = self.equipment.equipamentoacesso_set.all()[0].vrf.internal_name \ + if self.equipment.equipamentoacesso_set.all()[0].vrf \ + else self.management_vrf + + command = 'copy tftp://{}/{} {} vrf {}\n\n'.format( + self.tftpserver, filename, destination, 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') + self._wait_string('>|#') + self.channel.send('show privilege\n') + if not self.waitString('Feature privilege: Disabled'): + 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') + self._wait_string('Password:') + self.channel.send('%s\n' % self.equipment_access.enable_pass) + 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 + elif re.search(self.VALID_TFTP_PUT_MESSAGE_NXS6001, output_line): + + log.debug('Equipment output: %s' % output_line) + + # test bug switch copying 0 bytes + if output_line == 'Copy failed': + log.debug('Switch copied 0 bytes, need to try again.') + raise plugin_exc.CurrentlyBusyErrorException() + string_ok = 1 + + return recv_string diff --git a/networkapi/plugins/Juniper/JUNOS/BGP/__init__.py b/networkapi/plugins/Juniper/JUNOS/BGP/__init__.py new file mode 100644 index 000000000..e69de29bb From f2a1e7f02c2e7bfcc06e9f8248365bb26280e0d5 Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Thu, 11 Feb 2021 11:59:15 -0300 Subject: [PATCH 04/13] import junos plugin --- networkapi/plugins/Juniper/JUNOS/BGP/Cli.py | 164 ++------------------ 1 file changed, 15 insertions(+), 149 deletions(-) diff --git a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py index ba4b4a9a8..d64bfcc41 100644 --- a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py +++ b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py @@ -15,8 +15,6 @@ # 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 @@ -52,19 +50,6 @@ class Generic(BasePlugin): 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' - VALID_TFTP_PUT_MESSAGE_NXS6001 = 'Copy complete' - def _deploy_pre_req(self, neighbor): # Concatenate RouteMapEntries Lists route_map_in = neighbor.peer_group.route_map_in @@ -84,11 +69,11 @@ def _deploy_pre_req(self, neighbor): 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)) @@ -112,7 +97,6 @@ def _undeploy_pre_req(self, neighbor, ip_version): 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)) @@ -229,15 +213,12 @@ def undeploy_route_map(self, route_map): def _operate_equipment(self, types, template_type, config): self.connect() - self._ensure_privilege_level() + self.ensure_privilege_level() file_to_deploy = self._generate_config_file( types, template_type, config) self._deploy_config_in_equipment(file_to_deploy) self.close() - ############ - # TEMPLATE # - ############ def _generate_config_file(self, types, template_type, config): """Load a template and write a file with the rended output. @@ -300,7 +281,8 @@ def _get_equipment_template(self, template_type): log.error('Template type %s not found. Error: %s' % (template_type, e)) raise plugin_exc.BGPTemplateException() - def _generate_template_dict_neighbor(self, neighbor): + @staticmethod + def _generate_template_dict_neighbor(neighbor): """Make a dictionary to use in template""" key_dict = { @@ -343,7 +325,8 @@ def _generate_template_dict_route_map(self, route_map): entry = { 'ACTION': action, 'ORDER': entry_obj.order, - 'TYPE_MATCH': self._get_type_list(entry_obj.list_config_bgp.type)['route_map'], + '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 } @@ -356,7 +339,8 @@ def _generate_template_dict_route_map(self, route_map): return key_dict - def _get_type_list(self, type): + @staticmethod + def _get_type_list(type_): types = { 'P': { 'config_list': 'prefix-list', @@ -367,9 +351,10 @@ def _get_type_list(self, type): 'route_map': '' }, } - return types[type] + return types[type_] - def _read_config(self, filename): + @staticmethod + def _read_config(filename): """Return content from template_file""" try: @@ -385,7 +370,8 @@ def _read_config(self, filename): return template_content - def _save_config(self, filename, config): + @staticmethod + def _save_config(filename, config): """Write config in template file""" try: @@ -396,9 +382,6 @@ def _save_config(self, filename, config): 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) @@ -412,122 +395,5 @@ 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) - """ - recv = None - - vrf = self.equipment.equipamentoacesso_set.all()[0].vrf.internal_name \ - if self.equipment.equipamentoacesso_set.all()[0].vrf \ - else self.management_vrf - - command = 'copy tftp://{}/{} {} vrf {}\n\n'.format( - self.tftpserver, filename, destination, 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') - self._wait_string('>|#') - self.channel.send('show privilege\n') - if not self.waitString('Feature privilege: Disabled'): - 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') - self._wait_string('Password:') - self.channel.send('%s\n' % self.equipment_access.enable_pass) - 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 - elif re.search(self.VALID_TFTP_PUT_MESSAGE_NXS6001, output_line): - - log.debug('Equipment output: %s' % output_line) - - # test bug switch copying 0 bytes - if output_line == 'Copy failed': - log.debug('Switch copied 0 bytes, need to try again.') - raise plugin_exc.CurrentlyBusyErrorException() - string_ok = 1 - - return recv_string + self.copyScriptFileToConfig(filename, + destination='running-config') From de3a5afef24bcd0f2a2c7b3efe3a5ce78cbf3216 Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Thu, 11 Feb 2021 12:01:18 -0300 Subject: [PATCH 05/13] create method that returns the bgp plugin --- networkapi/plugins/Juniper/JUNOS/plugin.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/networkapi/plugins/Juniper/JUNOS/plugin.py b/networkapi/plugins/Juniper/JUNOS/plugin.py index 40eb73b49..17d7034cb 100644 --- a/networkapi/plugins/Juniper/JUNOS/plugin.py +++ b/networkapi/plugins/Juniper/JUNOS/plugin.py @@ -20,6 +20,7 @@ from exceptions import IOError from networkapi.plugins.base import BasePlugin from networkapi.plugins import exceptions +from networkapi.plugins.Juniper.JUNOS.BGP.Cli import Generic as BGP from networkapi.equipamento.models import EquipamentoAcesso from networkapi.system.facade import get_value from networkapi.system.exceptions import VariableDoesNotExistException @@ -56,21 +57,29 @@ def __init__(self, **kwargs): if 'seconds_to_wait_to_try_lock' in kwargs: self.seconds_to_wait_to_try_lock = kwargs.get('seconds_to_wait_to_try_lock') + def bgp(self): + return BGP(equipment=self.equipment) + def connect(self): """ - Connects to equipment via ssh using PyEz and create connection with invoked shell object. + Connects to equipment via ssh using PyEz + and create connection with invoked shell object. :returns: - True on success or raise an exception on any fail (will NOT return a false result, due project decision). + True on success or raise an exception on any + fail (will NOT return a false result, due project decision). """ if self.equipment_access is None: try: # Collect the credentials (user and password) for equipment - self.equipment_access = EquipamentoAcesso.search(None, self.equipment, 'ssh').uniqueResult() + self.equipment_access = EquipamentoAcesso.search(None, + self.equipment, + 'ssh').uniqueResult() except Exception as e: - message = "Unknown error while accessing equipment {} in database.".format(self.equipment.nome) + message = "Unknown error while accessing equipment {} in database.".format( + self.equipment.nome) log.error("{}: {}".format(message, e)) raise exceptions.APIException(message) From 6e23f10349d5c0e91bf12d7f56f23e0eec727707 Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Thu, 18 Feb 2021 11:50:47 -0300 Subject: [PATCH 06/13] add keys to neighbor dict --- networkapi/plugins/Juniper/JUNOS/BGP/Cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py index d64bfcc41..d8b37ce30 100644 --- a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py +++ b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py @@ -287,6 +287,7 @@ def _generate_template_dict_neighbor(neighbor): key_dict = { 'AS_NUMBER': neighbor.local_asn.name, + 'LOCAL_IP': str(neighbor.local_ip), 'VRF_NAME': neighbor.remote_ip.networkipv4.vlan.ambiente.default_vrf.internal_name, 'REMOTE_IP': str(neighbor.remote_ip), 'REMOTE_AS': neighbor.remote_asn.name, @@ -299,7 +300,8 @@ def _generate_template_dict_neighbor(neighbor): 'SOFT_RECONFIGURATION': neighbor.soft_reconfiguration, 'NEXT_HOP_SELF': neighbor.next_hop_self, 'REMOVE_PRIVATE_AS': neighbor.remove_private_as, - 'COMMUNITY': neighbor.community + 'COMMUNITY': neighbor.community, + 'GROUP': "AS_{}".format(neighbor.remote_as.name) } return key_dict From 00eb45dcf945bcf0ab20739b80742b0f83b559bc Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Wed, 24 Feb 2021 13:23:24 -0300 Subject: [PATCH 07/13] get bgp plugin --- networkapi/api_neighbor/v4/facade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/networkapi/api_neighbor/v4/facade.py b/networkapi/api_neighbor/v4/facade.py index 2712467e7..e7195ccbf 100644 --- a/networkapi/api_neighbor/v4/facade.py +++ b/networkapi/api_neighbor/v4/facade.py @@ -612,7 +612,7 @@ def deploy_neighbor_v4(neighbor_id): eqpt = get_v4_equipment(neighbor) - plugin = PluginFactory.factory(eqpt) + plugin = PluginFactory.factory(eqpt, bgp=True) plugin.bgp().deploy_neighbor(neighbor) neighbor.deploy() From 586dac6439446db30c32b0be552a4d7be971017f Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Wed, 24 Feb 2021 13:27:31 -0300 Subject: [PATCH 08/13] fix bgp plugin --- networkapi/plugins/Juniper/JUNOS/BGP/Cli.py | 24 ++++++++++++++------- networkapi/plugins/Juniper/JUNOS/plugin.py | 4 ---- networkapi/plugins/factory.py | 5 +++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py index d8b37ce30..c5c1c3240 100644 --- a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py +++ b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py @@ -13,6 +13,8 @@ # 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 @@ -29,17 +31,16 @@ 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 +from networkapi.plugins.Juniper.JUNOS.plugin import JUNOS log = logging.getLogger(__name__) -class Generic(BasePlugin): +class Generic(JUNOS): TEMPLATE_NEIGHBOR_V4_ADD = 'neighbor_v4_add' TEMPLATE_NEIGHBOR_V4_REMOVE = 'neighbor_v4_remove' @@ -50,7 +51,12 @@ class Generic(BasePlugin): TEMPLATE_ROUTE_MAP_ADD = 'route_map_add' TEMPLATE_ROUTE_MAP_REMOVE = 'route_map_remove' + def bgp(self): + return Generic(equipment=self.equipment) + def _deploy_pre_req(self, neighbor): + log.info("_deploy_pre_req") + # Concatenate RouteMapEntries Lists route_map_in = neighbor.peer_group.route_map_in route_map_out = neighbor.peer_group.route_map_out @@ -180,6 +186,7 @@ def undeploy_neighbor(self, neighbor): def deploy_list_config_bgp(self, list_config_bgp): """Deploy prefix list""" + log.info("deploy_list_config_bgp") config = self._generate_template_dict_list_config_bgp(list_config_bgp) @@ -196,6 +203,7 @@ def undeploy_list_config_bgp(self, list_config_bgp): def deploy_route_map(self, route_map): """Deploy route map""" + log.info("deploy_route_map") config = self._generate_template_dict_route_map(route_map) @@ -251,8 +259,7 @@ def _get_template_config(self, template_type, config): except Exception as err: log.error('Error: %s ' % err) - raise plugin_exc.BGPTemplateException(err) - + raise self.exceptions.BGPTemplateException(err) return config_to_be_saved def _load_template_file(self, template_type): @@ -279,7 +286,7 @@ def _get_equipment_template(self, template_type): None, self.equipment.id, template_type).uniqueResult() except Exception as e: log.error('Template type %s not found. Error: %s' % (template_type, e)) - raise plugin_exc.BGPTemplateException() + raise self.exceptions.BGPTemplateException() @staticmethod def _generate_template_dict_neighbor(neighbor): @@ -301,7 +308,7 @@ def _generate_template_dict_neighbor(neighbor): 'NEXT_HOP_SELF': neighbor.next_hop_self, 'REMOVE_PRIVATE_AS': neighbor.remove_private_as, 'COMMUNITY': neighbor.community, - 'GROUP': "AS_{}".format(neighbor.remote_as.name) + 'GROUP': "AS_{}".format(neighbor.remote_asn.name) } return key_dict @@ -378,6 +385,7 @@ def _save_config(filename, config): try: file_handle = open(filename, 'w') + log.debug(filename) file_handle.write(config) file_handle.close() except IOError, e: @@ -393,7 +401,7 @@ def _deploy_config_in_equipment(self, rel_filename): return self._apply_config(rel_filename) def _apply_config(self, filename): - + log.info("_apply_config") if self.equipment.maintenance: raise AllEquipmentsAreInMaintenanceException() diff --git a/networkapi/plugins/Juniper/JUNOS/plugin.py b/networkapi/plugins/Juniper/JUNOS/plugin.py index 17d7034cb..e83ffc50b 100644 --- a/networkapi/plugins/Juniper/JUNOS/plugin.py +++ b/networkapi/plugins/Juniper/JUNOS/plugin.py @@ -20,7 +20,6 @@ from exceptions import IOError from networkapi.plugins.base import BasePlugin from networkapi.plugins import exceptions -from networkapi.plugins.Juniper.JUNOS.BGP.Cli import Generic as BGP from networkapi.equipamento.models import EquipamentoAcesso from networkapi.system.facade import get_value from networkapi.system.exceptions import VariableDoesNotExistException @@ -57,9 +56,6 @@ def __init__(self, **kwargs): if 'seconds_to_wait_to_try_lock' in kwargs: self.seconds_to_wait_to_try_lock = kwargs.get('seconds_to_wait_to_try_lock') - def bgp(self): - return BGP(equipment=self.equipment) - def connect(self): """ diff --git a/networkapi/plugins/factory.py b/networkapi/plugins/factory.py index b18ea0b62..87c047e3a 100644 --- a/networkapi/plugins/factory.py +++ b/networkapi/plugins/factory.py @@ -93,6 +93,11 @@ def get_plugin(cls, **kwargs): if re.search('JUNIPER', marca.upper(), re.DOTALL): if re.search('QFX10008', modelo.upper(), re.DOTALL) \ or re.search('QFX5120-48T', modelo.upper(), re.DOTALL): + if 'bgp' in kwargs: + bgp = kwargs.get('bgp') + if bgp: + from .Juniper.JUNOS.BGP.Cli import Generic + return Generic from .Juniper.JUNOS.plugin import JUNOS return JUNOS raise NotImplementedError('plugin not implemented') From 81957b2e4d72175d204ce9e68876875675710e57 Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Mon, 8 Mar 2021 19:37:25 -0300 Subject: [PATCH 09/13] use remote ip in the group name --- networkapi/api_neighbor/v4/facade.py | 2 +- networkapi/plugins/Juniper/JUNOS/BGP/Cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/networkapi/api_neighbor/v4/facade.py b/networkapi/api_neighbor/v4/facade.py index e7195ccbf..d3c48c2a2 100644 --- a/networkapi/api_neighbor/v4/facade.py +++ b/networkapi/api_neighbor/v4/facade.py @@ -639,7 +639,7 @@ def undeploy_neighbor_v4(neighbor_id): eqpt = get_v4_equipment(neighbor) - plugin = PluginFactory.factory(eqpt) + plugin = PluginFactory.factory(eqpt, bgp=True) plugin.bgp().undeploy_neighbor(neighbor) neighbor.undeploy() diff --git a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py index c5c1c3240..0ce1c335f 100644 --- a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py +++ b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py @@ -308,7 +308,7 @@ def _generate_template_dict_neighbor(neighbor): 'NEXT_HOP_SELF': neighbor.next_hop_self, 'REMOVE_PRIVATE_AS': neighbor.remove_private_as, 'COMMUNITY': neighbor.community, - 'GROUP': "AS_{}".format(neighbor.remote_asn.name) + 'GROUP': "GROUP_{}".format(neighbor.remote_ip) } return key_dict From 82e513226c7b66ba68ebb856ce094976b9ce1ada Mon Sep 17 00:00:00 2001 From: "laura.panzariello" Date: Fri, 12 Mar 2021 17:02:57 -0300 Subject: [PATCH 10/13] fix route-map/prefix-list undeployment --- networkapi/plugins/Juniper/JUNOS/BGP/Cli.py | 91 +++++++++++---------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py index 0ce1c335f..b99ca5172 100644 --- a/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py +++ b/networkapi/plugins/Juniper/JUNOS/BGP/Cli.py @@ -75,6 +75,7 @@ def _deploy_pre_req(self, neighbor): self.deploy_route_map(neighbor.peer_group.route_map_out) def _undeploy_pre_req(self, neighbor, ip_version): + log.info("_undeploy_pre_req") # Concatenate RouteMapEntries Lists route_map_in = neighbor.peer_group.route_map_in @@ -90,18 +91,14 @@ def _undeploy_pre_req(self, neighbor, ip_version): 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) - ) + neighbors_v4 = neighbors_v4.exclude(Q(id=neighbor.id)) + neighbors_v6 = neighbors_v6.exclude(Q(id=neighbor.id)) if not neighbors_v4 and not neighbors_v6: - if route_map_in.equipments.filter(id=self.equipment.id): + try: self.undeploy_route_map(route_map_in) + except Exception as e: + log.error("Error while undeploying route-map. E: {}".format(e)) neighbors_v4 = NeighborV4.objects.filter(Q( Q(peer_group__route_map_in=route_map_out) | @@ -113,18 +110,14 @@ def _undeploy_pre_req(self, neighbor, ip_version): 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) - ) + neighbors_v4 = neighbors_v4.exclude(Q(id=neighbor.id)) + neighbors_v6 = neighbors_v6.exclude(Q(id=neighbor.id)) if not neighbors_v4 and not neighbors_v6: - if route_map_out.equipments.filter(id=self.equipment.id): + try: self.undeploy_route_map(route_map_out) + except Exception as e: + log.error("Error while undeploying route-map. E: {}".format(e)) # List Config BGP if not neighbors_v4 and not neighbors_v6: @@ -133,28 +126,24 @@ def _undeploy_pre_req(self, neighbor, ip_version): 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)) + neighbors_v4 = NeighborV6.objects.filter(Q( + Q(peer_group__route_map_in__routemapentry__list_config_bgp=list_config_bgp) | + Q(peer_group__route_map_out__routemapentry__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)) + neighbors_v6 = NeighborV6.objects.filter(Q( + Q(peer_group__route_map_in__routemapentry__list_config_bgp=list_config_bgp) | + Q(peer_group__route_map_out__routemapentry__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) - ) + neighbors_v4 = neighbors_v4.exclude(Q(id=neighbor.id)) + neighbors_v6 = neighbors_v6.exclude(Q(id=neighbor.id)) if not neighbors_v4 and not neighbors_v6: - if not list_config_bgp.equipments.filter(id=self.equipment.id): + try: self.undeploy_list_config_bgp(list_config_bgp) + except Exception as e: + log.error("Error while undeploying prefix-list. E: {}".format(e)) def deploy_neighbor(self, neighbor): """Deploy neighbor""" @@ -175,14 +164,16 @@ def undeploy_neighbor(self, 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) + self._open() + self._operate('neighbor', template_type, config) + + self._undeploy_pre_req(neighbor, ip_version) + self._close() def deploy_list_config_bgp(self, list_config_bgp): """Deploy prefix list""" @@ -195,11 +186,13 @@ def deploy_list_config_bgp(self, list_config_bgp): def undeploy_list_config_bgp(self, list_config_bgp): """Undeploy prefix list""" + log.info("undeploy_list_config_bgp") config = self._generate_template_dict_list_config_bgp(list_config_bgp) - self._operate_equipment( - 'list_config_bgp', self.TEMPLATE_LIST_CONFIG_REMOVE, config) + self._operate('list_config_bgp', + self.TEMPLATE_LIST_CONFIG_REMOVE, + config) def deploy_route_map(self, route_map): """Deploy route map""" @@ -215,18 +208,33 @@ def undeploy_route_map(self, route_map): config = self._generate_template_dict_route_map(route_map) - self._operate_equipment( + self._operate( 'route_map', self.TEMPLATE_ROUTE_MAP_REMOVE, config) def _operate_equipment(self, types, template_type, config): self.connect() - self.ensure_privilege_level() + # self.ensure_privilege_level() file_to_deploy = self._generate_config_file( types, template_type, config) self._deploy_config_in_equipment(file_to_deploy) self.close() + def _operate(self, types, template_type, config): + + file_to_deploy = self._generate_config_file( + types, template_type, config) + self._deploy_config_in_equipment(file_to_deploy) + + def _open(self): + + self.connect() + # self.ensure_privilege_level() + + def _close(self): + + self.close() + def _generate_config_file(self, types, template_type, config): """Load a template and write a file with the rended output. @@ -315,6 +323,7 @@ def _generate_template_dict_neighbor(neighbor): def _generate_template_dict_list_config_bgp(self, list_config_bgp): """Make a dictionary to use in template""" + log.info("_generate_template_dict_list_config_bgp") key_dict = { 'TYPE': self._get_type_list(list_config_bgp.type)['config_list'], @@ -329,7 +338,7 @@ def _generate_template_dict_route_map(self, route_map): entries = [] - for entry_obj in route_map.route_map_entries: + for entry_obj in route_map.routemapentry_set.all(): action = 'permit' if entry_obj.action == 'P' else 'deny' entry = { 'ACTION': action, From 48e4de06ff4dd7065161579deb69464bcd784789 Mon Sep 17 00:00:00 2001 From: malinoski Date: Thu, 18 Mar 2021 10:52:44 -0300 Subject: [PATCH 11/13] New junos plugin feature. A filter log messages from specific libs, to a better log visualization --- networkapi/plugins/Juniper/JUNOS/plugin.py | 59 ++++++++++++++++++++++ networkapi/plugins/Juniper/JUNOS/tests.py | 35 ++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/networkapi/plugins/Juniper/JUNOS/plugin.py b/networkapi/plugins/Juniper/JUNOS/plugin.py index e83ffc50b..66096ffc0 100644 --- a/networkapi/plugins/Juniper/JUNOS/plugin.py +++ b/networkapi/plugins/Juniper/JUNOS/plugin.py @@ -36,6 +36,11 @@ CommitError, \ RpcError +from jnpr.junos.device import logger as device_logger +from ncclient.transport.ssh import logger as ssh_logger +from ncclient.transport.session import logger as session_logger +from ncclient.operations.rpc import logger as rpc_logger + log = logging.getLogger(__name__) @@ -44,7 +49,12 @@ class JUNOS(BasePlugin): configuration = None quantity_of_times_to_try_lock = 3 seconds_to_wait_to_try_lock = 10 + + # Variables defined at networkapi/database alternative_variable_base_path_list = ['path_to_tftpboot'] + detailed_log_level_var = 'detailed_junos_log_level' + detailed_log_level_used = None + alternative_static_base_path_list = ['/mnt/scripts/tftpboot/'] ignore_warning_list = ['statement not found'] @@ -56,6 +66,11 @@ def __init__(self, **kwargs): if 'seconds_to_wait_to_try_lock' in kwargs: self.seconds_to_wait_to_try_lock = kwargs.get('seconds_to_wait_to_try_lock') + # Logger objects to be disabled. Remove or add objects to array below: + loggers = [device_logger, ssh_logger, session_logger, rpc_logger] + detailed_log_level_used = self.set_detailed_junos_log_level(loggers) + log.info("Detailed Junos log level: {}".format(detailed_log_level_used)) + def connect(self): """ @@ -365,3 +380,47 @@ def check_configuration_file_exists(self, file_path): log.error("{} Could not find in: relative path ('{}'), system variables ({}) or static paths ({})".format( message, file_path, self.alternative_variable_base_path_list, self.alternative_static_base_path_list)) raise exceptions.APIException(message) + + def set_detailed_junos_log_level(self, loggers): + + """ + Used to disable logs from detailed and specific libs used in Junos plugin, for example: + If 'ERROR' is defined, higher log messages will be disabled, like 'WARNING', 'INFO' and 'DEBUG'. + + :param str loggers: Array of logger objects + :return: A valid level error ('ERROR', 'INFO', etc.), otherwise None + """ + + default_log_level = 'ERROR' + log_level = None + + # Trying to get the variable from networkapi. + try: + log_level = get_value(self.detailed_log_level_var) + except DatabaseError as e: + log.warning("Database error while getting '{}' variable: {}".format( + self.detailed_log_level_var, e)) + except VariableDoesNotExistException as e: + log.warning("Variable '{}' does not exist: {}".format( + self.detailed_log_level_var, e)) + except Exception as e: + log.warning("Unknown error while getting '{}' variable: {} ".format( + self.detailed_log_level_var, e)) + + # If could not get the value earlier, a default value will be used + if log_level is None or log_level == "": + log.warning("Could not get '{}' variable from networkapi. ".format( + self.detailed_log_level_var, default_log_level)) + log_level = default_log_level + + # Trying to set the level to each log object. + for logger in loggers: + try: + logger.setLevel(log_level) + except Exception as e: + log.error("Invalid log level '{}'. The default {} will be used: {}".format( + log_level, default_log_level, e)) + log_level = default_log_level # to be used at next iteration too + logger.setLevel(log_level) + + return log_level diff --git a/networkapi/plugins/Juniper/JUNOS/tests.py b/networkapi/plugins/Juniper/JUNOS/tests.py index f338fba2b..17bd869f6 100644 --- a/networkapi/plugins/Juniper/JUNOS/tests.py +++ b/networkapi/plugins/Juniper/JUNOS/tests.py @@ -5,6 +5,10 @@ from mock import patch, MagicMock from jnpr.junos.exception import RPCError, RpcError, LockError, ConfigLoadError, CommitError +from jnpr.junos.device import logger as device_logger +from ncclient.transport.ssh import logger as ssh_logger +from ncclient.transport.session import logger as session_logger +from ncclient.operations.rpc import logger as rpc_logger class JunosPluginTest(NetworkApiTestCase): @@ -13,7 +17,7 @@ class JunosPluginTest(NetworkApiTestCase): How to use: cd GloboNetworkAPI - docker exec -it netapi_app ./fast_start_test_reusedb.sh networkapi/plugins/Juniper/JUNOS + docker exec -it netapi_app ./fast_start_test_reusedb.sh networkapi/plugins/Juniper/JUNOS/tests.py General notes: - autospec=True: responds to methods that actually exist in the real class @@ -257,3 +261,32 @@ def test_check_configuration_file_exists_from_relative_path(self, mock_is_file): result = plugin.check_configuration_file_exists("any_configuration_path_with_file_name") self.assertEqual(result, "any_configuration_path_with_file_name") # Must be the same + + def test_set_detailed_junos_log_level_not_defined_at_db(self): + + plugin = JUNOS(equipment_access=self.mock_equipment_access) + loggers = [device_logger, ssh_logger, session_logger, rpc_logger] + result = plugin.set_detailed_junos_log_level(loggers) + self.assertEqual(result, 'ERROR') + + @patch('networkapi.plugins.Juniper.JUNOS.plugin.get_value') + def test_set_detailed_junos_log_level_defined_at_db(self, mock_get_value): + + mock_get_value.return_value = 'WARNING' + + plugin = JUNOS(equipment_access=self.mock_equipment_access) + loggers = [device_logger, ssh_logger, session_logger, rpc_logger] + result = plugin.set_detailed_junos_log_level(loggers) + + self.assertEqual(result, 'WARNING') + + @patch('networkapi.plugins.Juniper.JUNOS.plugin.get_value') + def test_set_detailed_junos_log_level_defined_wrong_at_db(self, mock_get_value): + + mock_get_value.return_value = 'WRONG' + + plugin = JUNOS(equipment_access=self.mock_equipment_access) + loggers = [device_logger, ssh_logger, session_logger, rpc_logger] + result = plugin.set_detailed_junos_log_level(loggers) + + self.assertEqual(result, 'ERROR') From 4fa2857cacbfd5eb3f2b8fc1b1ee2dd9e797b760 Mon Sep 17 00:00:00 2001 From: malinoski Date: Thu, 18 Mar 2021 13:00:05 -0300 Subject: [PATCH 12/13] Code rollback because it was not tested enough. --- dev/load_example_environment.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/load_example_environment.sql b/dev/load_example_environment.sql index c9a078397..d81afb44c 100644 --- a/dev/load_example_environment.sql +++ b/dev/load_example_environment.sql @@ -1900,8 +1900,8 @@ VALUES -- -- Dumped data of the Interface types table -- -INSERT INTO `tipo_interface` (`id_tipo_interface`, `tipo`) VALUES ('1', 'access'); -INSERT INTO `tipo_interface` (`id_tipo_interface`, `tipo`) VALUES ('2', 'trunk'); +INSERT INTO `tipo_interface` (`tipo`) +VALUES ("access"), ("trunk"); From aa8ad143014070037daab3841aface8a4ee2e269 Mon Sep 17 00:00:00 2001 From: malinoski Date: Thu, 18 Mar 2021 13:32:01 -0300 Subject: [PATCH 13/13] Setting database default value to detailed log level --- dev/load_example_environment.sql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dev/load_example_environment.sql b/dev/load_example_environment.sql index d81afb44c..23831d560 100644 --- a/dev/load_example_environment.sql +++ b/dev/load_example_environment.sql @@ -1903,6 +1903,13 @@ VALUES INSERT INTO `tipo_interface` (`tipo`) VALUES ("access"), ("trunk"); +INSERT INTO `variables` (`name`, `value`, `description`) +VALUES +( + 'detailed_junos_log_level', + 'ERROR', + 'Used to disable logs for detailed and specific libs used in plugin Junos' +);