diff --git a/networkapi/plugins/Juniper/JUNOS/plugin.py b/networkapi/plugins/Juniper/JUNOS/plugin.py index 21fcaa62e..b38d891a0 100644 --- a/networkapi/plugins/Juniper/JUNOS/plugin.py +++ b/networkapi/plugins/Juniper/JUNOS/plugin.py @@ -16,10 +16,14 @@ import logging import time +import os.path from exceptions import IOError from networkapi.plugins.base import BasePlugin from networkapi.plugins import exceptions from networkapi.equipamento.models import EquipamentoAcesso +from networkapi.system.facade import get_value +from networkapi.system.exceptions import VariableDoesNotExistException +from django.db.utils import DatabaseError from jnpr.junos import Device from jnpr.junos.utils.config import Config from jnpr.junos.utils.start_shell import StartShell @@ -39,6 +43,8 @@ class JUNOS(BasePlugin): configuration = None quantity_of_times_to_try_lock = 3 seconds_to_wait_to_try_lock = 10 + alternative_variable_base_path_list = ['path_to_tftpboot'] + alternative_static_base_path_list = ['/mnt/scripts/tftpboot/'] def __init__(self, **kwargs): super(JUNOS, self).__init__(connect_port=830, **kwargs) @@ -63,7 +69,7 @@ def connect(self): self.equipment_access = EquipamentoAcesso.search( None, self.equipment, 'ssh').uniqueResult() except Exception: - log.error("Access type {} not found for equipment {}.".format('ssh', self.equipment.nome)) + log.error("Unknown error while accessing equipment {} in database.".format(self.equipment.nome)) raise exceptions.InvalidEquipmentAccessException() log.info("Trying to connect on host {} ... ".format(self.equipment_access.fqdn)) @@ -86,7 +92,7 @@ def connect(self): raise ConnectError except Exception, e: - log.error("Error connecting to host {}: {}".format(self.equipment_access.fqdn, e)) + log.error("Unknown error while connecting to host {}: {}".format(self.equipment_access.fqdn, e)) raise Exception def close(self): @@ -110,7 +116,7 @@ def close(self): raise ConnectClosedError except Exception, e: - log.error("Unexpected error at closing connection on host {}: {}".format(self.equipment_access.fqdn, e)) + log.error("Unknown error while closing connection on host {}: {}".format(self.equipment_access.fqdn, e)) raise Exception def copyScriptFileToConfig(self, filename, use_vrf='', destination=''): @@ -129,19 +135,24 @@ def copyScriptFileToConfig(self, filename, use_vrf='', destination=''): log.info("Trying to load file configuration for host {} ...".format(self.equipment_access.fqdn)) + # 'filename' was defined in super class, but in plugin junos the 'file_path' will be used instead + file_path = filename + file_path = self.check_configuration_file_exists(file_path) + try: - command_file = open(filename, "r") + + command_file = open(file_path, "r") command = command_file.read() - log.info("Load configuration from file {} successfully!".format(filename)) + log.info("Load configuration from file {} successfully!".format(file_path)) return self.exec_command(command) except IOError, e: - log.error("File not found {}: {}".format(filename, e)) + log.error("File not found {}: {}".format(file_path, e)) self.close() raise IOError except Exception, e: - log.error("Unexpected error occurred {}: {}".format(filename, e)) + log.error("Unknown error while accessing configuration file {}: {}".format(file_path, e)) self.close() raise Exception @@ -209,7 +220,7 @@ def exec_command(self, command, success_regex='', invalid_regex=None, error_rege raise RpcError except Exception as e: - log.error("An exception error occurred during configuration execution on host {} " + log.error("Unknown error while executing configuration on host {} " "(rollback, unlock and close will be tried for safety): {}".format(self.equipment_access.fqdn, e)) self.configuration.rollback() self.configuration.unlock() @@ -257,7 +268,7 @@ def ensure_privilege_level(self, privilege_level=None): return True except Exception as e: - log.error("An exception error occurred during the user privilege verification on host {} " + log.error("Unknown error while verifying user privilege on host {} " "(close connection will be executed for safety): {}".format(self.equipment_access.fqdn, e)) self.close() raise Exception @@ -271,6 +282,8 @@ def __try_lock(self): Returns True if success, otherwise, raise an exception. That means will NOT return a false result. """ + log.info("Trying to lock host {} ...".format(self.equipment_access.fqdn)) + for x in range(self.quantity_of_times_to_try_lock): try: self.configuration.lock() @@ -281,7 +294,7 @@ def __try_lock(self): count = x + 1 # Keep looping ... log.warning( - "Configuration still could not be locked on host {}. Automatic try in {} seconds - {}/{} {}".format( + "Host {} could not be locked. Automatic try in {} seconds - {}/{} {}".format( self.equipment_access.fqdn, self.seconds_to_wait_to_try_lock, count, @@ -289,7 +302,59 @@ def __try_lock(self): e)) if count == self.quantity_of_times_to_try_lock: - log.error("Host {} couldn't be locked: {}".format(self.equipment_access.fqdn, e)) + log.error("An error occurred while trying to lock host {}".format(self.equipment_access.fqdn)) raise Exception else: time.sleep(self.seconds_to_wait_to_try_lock) + + def check_configuration_file_exists(self, file_path): + + """ + This function try to find and build (if necessary) the configuration file path. The priorities are: + (1) build the full path from system variable base and relative file path ('file_path'); or + (2) build the full path from static variable base and relative file path ('file_path'); or + (3) return the relative path it self ('file_path') + + :param str file_path: Relative path, examples: + 'networkapi/plugins/Juniper/JUNOS/samples/sample_command.txt' or + 'networkapi/generated_config/interface/int-d_24823_config_ROR9BX3ATQG93TALJAMO2G' + + :return: Return a valid configuration file path string. Ex.: + 'networkapi/plugins/Juniper/JUNOS/samples/sample_command.txt' or + '/mnt/scripts/tftpboot/networkapi/generated_config/interface/int-d_24823_config_ROR9BX3ATQG93TALJAMO2G' + """ + + log.info("Checking configuration file exist: {}".format(file_path)) + + # Check in system variables + for variable in self.alternative_variable_base_path_list: + try: + base_path = get_value(variable) + if base_path != "": + result_path = base_path + file_path + if os.path.isfile(result_path): + log.info("Configuration file {} was found by system variable {}!".format(result_path, variable)) + return result_path + except (DatabaseError, VariableDoesNotExistException): + # DatabaseError means that variable table do not exist + pass + except Exception, e: + log.warning("Unknown error while calling networkapi.system.facade.get_value({}): {} {} ".format( + variable, e.__class__, e)) + + # Check possible static variables + for static_path in self.alternative_static_base_path_list: + result_path = static_path + file_path + if os.path.isfile(result_path): + log.info("Configuration file {} was found by static variable {}!".format(result_path, static_path)) + return result_path + + # Check if relative path is valid (for dev tests) + if os.path.isfile(file_path): + log.info("Configuration file {} was found by relative path".format(file_path)) + return file_path + + log.error("An error occurred while finding configuration file in: " + "relative path ('{}') or system variables ({}) or static paths ({})" + .format(file_path, self.alternative_variable_base_path_list, self.alternative_static_base_path_list)) + raise Exception diff --git a/networkapi/plugins/Juniper/JUNOS/tests.py b/networkapi/plugins/Juniper/JUNOS/tests.py index 837c0c9fe..2b2e3e9a7 100644 --- a/networkapi/plugins/Juniper/JUNOS/tests.py +++ b/networkapi/plugins/Juniper/JUNOS/tests.py @@ -1,6 +1,7 @@ from networkapi.test.test_case import NetworkApiTestCase from networkapi.plugins.base import BasePlugin from networkapi.plugins.Juniper.JUNOS.plugin import JUNOS +import mock from mock import patch, MagicMock @@ -116,3 +117,35 @@ def test_call_copyScriptFileToConfig(self, mock_junos_plugin): def test_call_ensure_privilege_level(self, mock_junos_plugin): mock_junos_plugin.ensure_privilege_level() mock_junos_plugin.ensure_privilege_level.assert_called_with() + + @patch('os.path.isfile') + @patch('networkapi.plugins.Juniper.JUNOS.plugin.get_value') + def test_check_configuration_file_exists_from_system_variable(self, mock_get_value, mock_is_file): + plugin = JUNOS(equipment_access=self.mock_equipment_access) + mock_get_value.return_value = "/any_base_variable_path/" + mock_is_file.return_value = True + + # isfile.return_value = True + result = plugin.check_configuration_file_exists("any_configuration_path_with_file_name") + self.assertEqual(result, "/any_base_variable_path/any_configuration_path_with_file_name") + + @patch('os.path.isfile') + def test_check_configuration_file_exists_from_static_variable(self, mock_is_file): + plugin = JUNOS(equipment_access=self.mock_equipment_access) + mock_is_file.return_value = True + first_alternative_static_base_path = plugin.alternative_static_base_path_list[0] + + result = plugin.check_configuration_file_exists("any_configuration_path_with_file_name") + self.assertEqual(result, first_alternative_static_base_path+"any_configuration_path_with_file_name") + + @patch('os.path.isfile') + def test_check_configuration_file_exists_from_relative_path(self, mock_is_file): + plugin = JUNOS(equipment_access=self.mock_equipment_access) + + # The function is_file(...), inside check_configuration_file_exists, is used two times. + # The proposed test need that function is_file(...) returns two different values at different places. + # Returning True at first and False at second time, where the principal test is executed + mock_is_file.side_effect = [False, True] + + 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