diff --git a/Makefile b/Makefile index 8675e2c94..a3d3236a7 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # Docker image version -NETAPI_IMAGE_VERSION := 2.1.0 +NETAPI_IMAGE_VERSION := 3.1.0 # Gets git current branch curr_branch := $(shell git symbolic-ref --short -q HEAD) diff --git a/networkapi/api_asn/models.py b/networkapi/api_asn/models.py index f88d510c8..e10bd7a49 100644 --- a/networkapi/api_asn/models.py +++ b/networkapi/api_asn/models.py @@ -61,6 +61,28 @@ def get_by_pk(cls, id): cls.log.error(u'Failure to search the ASN.') raise exceptions.AsnError(u'Failure to search the ASN.') + def get_by_asn(cls, asn): + """Get AS by id. + + :return: AS. + + :raise AsnNotFoundError: As not registered. + :raise AsnError: Failed to search for the As. + :raise OperationalError: Lock wait timeout exceeded + """ + try: + return Asn.objects.get(asn=asn) + except ObjectDoesNotExist: + cls.log.error(u'ASN not found. pk {}'.format(id)) + raise exceptions.AsnNotFoundError(id) + except OperationalError: + cls.log.error(u'Lock wait timeout exceeded.') + raise OperationalError() + except Exception: + cls.log.error(u'Failure to search the ASN.') + raise exceptions.AsnError(u'Failure to search the ASN.') + + def create_v4(self, as_map): """Create ASN.""" @@ -138,7 +160,7 @@ class Meta(BaseModel.Meta): managed = True @classmethod - def get_by_pk(cls, id): + def get_by_pk(cls, ids=None, asn=None, equipment=None): """Get AsnEquipment by id. :return: AsnEquipment. @@ -148,16 +170,26 @@ def get_by_pk(cls, id): :raise OperationalError: Lock wait timeout exceeded """ try: - return AsnEquipment.objects.get(id=id) + logging.info("get asn_equipment by id, asn or equipment") + if ids: + return AsnEquipment.objects.get(id=int(ids)) + elif asn: + return AsnEquipment.objects.filter(asn=int(asn)) + elif equipment: + return AsnEquipment.objects.filter(equipment__id=int(equipment)) + + return AsnEquipment.objects.all() + except ObjectDoesNotExist: cls.log.error(u'AsnEquipment not found. pk {}'.format(id)) raise exceptions.AsnEquipmentNotFoundError(id) except OperationalError: cls.log.error(u'Lock wait timeout exceeded.') raise OperationalError() - except Exception: - cls.log.error(u'Failure to search the AS.') - raise exceptions.AsnEquipmentError(u'Failure to search the AS.') + except Exception as e: + cls.log.error(u'Failure to search the ASNEquipment. E: %s' % e) + raise exceptions.AsnEquipmentError( + u'Failure to search the ASNEquipment. E: %s' % e) def create_v4(self, as_equipment): """Create AsnEquipment relationship.""" @@ -176,3 +208,15 @@ def delete_v4(self): """Delete AsnEquipment relationship.""" super(AsnEquipment, self).delete() + + def update_v4(self, asn_equipment): + """Update ASNEquipment """ + + equipment = get_model('equipamento', 'Equipamento') + + self.equipment = equipment().get_by_pk( + asn_equipment.get('equipment')[0]) + self.asn = Asn().get_by_pk(asn_equipment.get('asn')) + self.save() + + return self diff --git a/networkapi/api_asn/v4/facade.py b/networkapi/api_asn/v4/facade.py index a3c935ced..c46799978 100644 --- a/networkapi/api_asn/v4/facade.py +++ b/networkapi/api_asn/v4/facade.py @@ -4,6 +4,7 @@ from django.core.exceptions import FieldError from networkapi.api_asn.models import Asn +from networkapi.api_asn.models import AsnEquipment from networkapi.api_asn.v4 import exceptions from networkapi.api_asn.v4.exceptions import AsnErrorV4 from networkapi.api_asn.v4.exceptions import AsnNotFoundError, AsnError @@ -38,7 +39,22 @@ def get_as_by_id(as_id): try: as_ = Asn.get_by_pk(id=as_id) - except AsnNotFoundError, e: + except AsnNotFoundError as e: + raise exceptions.AsnDoesNotExistException(str(e)) + + return as_ + + +def get_as_by_asn(asn_): + """Return an AS by id. + + Args: + asn: ASN + """ + + try: + as_ = Asn.get_by_asn(asn_) + except AsnNotFoundError as e: raise exceptions.AsnDoesNotExistException(str(e)) return as_ @@ -56,9 +72,9 @@ def get_as_by_ids(autonomous_systems_ids): try: as_ = get_as_by_id(as_id).id as_ids.append(as_) - except exceptions.AsnDoesNotExistException, e: + except exceptions.AsnDoesNotExistException as e: raise ObjectDoesNotExistException(str(e)) - except Exception, e: + except Exception as e: raise NetworkAPIException(str(e)) as_s = Asn.objects.filter(id__in=as_ids) @@ -72,13 +88,13 @@ def update_as(as_): try: as_obj = get_as_by_id(as_.get('id')) as_obj.update_v4(as_) - except AsnErrorV4, e: + except AsnErrorV4 as e: raise ValidationAPIException(str(e)) - except ValidationAPIException, e: + except ValidationAPIException as e: raise ValidationAPIException(str(e)) - except exceptions.AsnDoesNotExistException, e: + except exceptions.AsnDoesNotExistException as e: raise ObjectDoesNotExistException(str(e)) - except Exception, e: + except Exception as e: raise NetworkAPIException(str(e)) return as_obj @@ -90,11 +106,11 @@ def create_as(as_): try: as_obj = Asn() as_obj.create_v4(as_) - except AsnErrorV4, e: + except AsnErrorV4 as e: raise ValidationAPIException(str(e)) - except ValidationAPIException, e: + except ValidationAPIException as e: raise ValidationAPIException(str(e)) - except Exception, e: + except Exception as e: raise NetworkAPIException(str(e)) return as_obj @@ -107,12 +123,190 @@ def delete_as(as_ids): try: as_obj = get_as_by_id(as_id) as_obj.delete_v4() - except exceptions.AsnDoesNotExistException, e: + except exceptions.AsnDoesNotExistException as e: raise ObjectDoesNotExistException(str(e)) - except exceptions.AsnAssociatedToEquipmentError, e: + except exceptions.AsnAssociatedToEquipmentError as e: raise ValidationAPIException(str(e)) - except AsnError, e: + except AsnError as e: raise NetworkAPIException(str(e)) - except Exception, e: + except Exception as e: raise NetworkAPIException(str(e)) + +def get_as_equipment_by_search(search=dict()): + """Return a list of ASEquipment's by dict.""" + + try: + as_equipment = AsnEquipment.get_by_pk() + as_map = build_query_to_datatable_v3(as_equipment, search) + except FieldError as e: + raise ValidationAPIException(str(e)) + except Exception as e: + raise NetworkAPIException(str(e)) + else: + return as_map + + +def get_as_equipment_by_id(as_equipment_id=None): + """Return an ASEquipment by id. + + Args: + as_equipment_id: Id of ASEquipment + + """ + + try: + as_equipment_list = list() + for asn in as_equipment_id: + as_equipment = AsnEquipment.get_by_pk(ids=asn) + as_equipment_list.append(as_equipment) + except AsnNotFoundError as e: + raise exceptions.AsnDoesNotExistException(str(e)) + + return as_equipment_list + + +def get_as_equipment_by_asn(asn_id=None): + """Return an ASEquipment by asn id. + + Args: + asn_id: Id of ASN + + """ + + try: + as_equipment = list() + for asn in asn_id: + as_equipment += AsnEquipment.get_by_pk(asn=asn) + + except AsnNotFoundError as e: + raise exceptions.AsnDoesNotExistException(str(e)) + + return as_equipment + + +def get_as_equipment_by_equip(equipment_id=None): + """Return an ASEquipment by equipment id. + + Args: + equipment_id: Id of Equipment + + """ + + try: + as_equipment = list() + for equip in equipment_id: + as_equipment += AsnEquipment.get_by_pk(equipment=equip) + + except AsnNotFoundError as e: + raise exceptions.AsnDoesNotExistException(str(e)) + + return as_equipment + + +def create_asn_equipment(asn_equipment): + """Create ASNEquipment.""" + + try: + asn_equipment_list = list() + + for equipment in asn_equipment.get("equipment"): + obj = dict() + obj["equipment"] = equipment + obj["asn"] = asn_equipment.get("asn") + as_obj = AsnEquipment() + as_obj.create_v4(obj) + asn_equipment_list.append({'id': as_obj.id}) + + except AsnErrorV4 as e: + raise ValidationAPIException(str(e)) + except ValidationAPIException as e: + raise ValidationAPIException(str(e)) + except Exception as e: + raise NetworkAPIException(str(e)) + + return asn_equipment_list + + +def delete_asn_equipment(as_ids): + """Delete ASNEquipment.""" + + try: + asn_equipment = AsnEquipment() + obj = asn_equipment.get_by_pk(ids=as_ids) + obj.delete_v4() + except exceptions.AsnDoesNotExistException as e: + raise ObjectDoesNotExistException(str(e)) + except exceptions.AsnAssociatedToEquipmentError as e: + raise ValidationAPIException(str(e)) + except AsnError as e: + raise NetworkAPIException(str(e)) + except Exception as e: + raise NetworkAPIException(str(e)) + + +def delete_asn_equipment_by_asn(asn_id): + """Delete ASNEquipment.""" + + try: + asn_obj = AsnEquipment() + asn_equipment = asn_obj.get_by_pk(asn=asn_id) + for obj in asn_equipment: + obj.delete_v4() + + except exceptions.AsnDoesNotExistException as e: + raise ObjectDoesNotExistException(str(e)) + except exceptions.AsnAssociatedToEquipmentError as e: + raise ValidationAPIException(str(e)) + except AsnError as e: + raise NetworkAPIException(str(e)) + except Exception as e: + raise NetworkAPIException(str(e)) + + +def update_asn_equipment(as_): + """Update ASNEquipment.""" + + try: + as_obj = AsnEquipment() + if not as_.get('id'): + raise Exception("The object do not have the id.") + asn_equipment = as_obj.get_by_pk(ids=as_.get('id')) + asn_equipment.update_v4(as_) + except AsnErrorV4 as e: + raise ValidationAPIException(str(e)) + except ValidationAPIException as e: + raise ValidationAPIException(str(e)) + except exceptions.AsnDoesNotExistException as e: + raise ObjectDoesNotExistException(str(e)) + except Exception as e: + raise NetworkAPIException(str(e)) + + return asn_equipment + + +def update_asn_equipment_by_asn(asn_id, as_): + """ + Update ASNEquipment. + Return new asn_equipments new ids + """ + + try: + as_obj = AsnEquipment() + asn_equipment = as_obj.get_by_pk(asn=asn_id) + for obj in asn_equipment: + obj.delete_v4() + + asn_equipment_obj = create_asn_equipment(as_) + + except AsnErrorV4 as e: + raise ValidationAPIException(str(e)) + except ValidationAPIException as e: + raise ValidationAPIException(str(e)) + except exceptions.AsnDoesNotExistException as e: + raise ObjectDoesNotExistException(str(e)) + except Exception as e: + raise NetworkAPIException(str(e)) + + return asn_equipment_obj + diff --git a/networkapi/api_asn/v4/specs/asn_equipment_post.json b/networkapi/api_asn/v4/specs/asn_equipment_post.json new file mode 100644 index 000000000..19ef79eac --- /dev/null +++ b/networkapi/api_asn/v4/specs/asn_equipment_post.json @@ -0,0 +1,28 @@ +{ + "title": "ASNEquipment Post", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ + "asn_equipment" + ], + "properties": { + "networks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "asn": { + "type": "integer" + }, + "equipment": { + "type": "array" + } + }, + "required": [ + "asn", + "equipment" + ] + } + } + } +} diff --git a/networkapi/api_asn/v4/urls.py b/networkapi/api_asn/v4/urls.py index a8763e2b1..a270ed294 100644 --- a/networkapi/api_asn/v4/urls.py +++ b/networkapi/api_asn/v4/urls.py @@ -6,6 +6,12 @@ urlpatterns = patterns( '', + url(r'^asnequipment/asn/((?P[;\w]+)/)?$', + views.AsEquipmentDBView.as_view()), + url(r'^asnequipment/equipment/((?P[;\w]+)/)?$', + views.AsEquipmentDBView.as_view()), + url(r'^asnequipment/((?P[;\w]+)/)?$', + views.AsEquipmentDBView.as_view()), url(r'^as/((?P[;\w]+)/)?$', views.AsDBView.as_view()), ) diff --git a/networkapi/api_asn/v4/views.py b/networkapi/api_asn/v4/views.py index 6d1314bdb..838fee1a3 100644 --- a/networkapi/api_asn/v4/views.py +++ b/networkapi/api_asn/v4/views.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Create your views here. +import logging from django.db.transaction import commit_on_success from rest_framework import status from rest_framework.permissions import IsAuthenticated @@ -19,6 +20,9 @@ from networkapi.util.json_validate import raise_json_validate +log = logging.getLogger(__name__) + + class AsDBView(CustomAPIView): @logs_method_apiview @@ -106,3 +110,106 @@ def delete(self, request, *args, **kwargs): facade.delete_as(obj_ids) return Response({}, status=status.HTTP_200_OK) + + +class AsEquipmentDBView(CustomAPIView): + + @logs_method_apiview + @raise_json_validate('') + @permission_classes_apiview((IsAuthenticated, Read)) + @prepare_search + def get(self, request, *args, **kwargs): + """Returns a list of AS's by ids ou dict.""" + + if not kwargs.get('obj_ids') and\ + not kwargs.get('asn_ids') and\ + not kwargs.get('equip_ids'): + obj_model = facade.get_as_equipment_by_search(self.search) + as_s = obj_model['query_set'] + only_main_property = False + else: + obj_model = None + only_main_property = True + if kwargs.get('obj_ids'): + as_ids = kwargs.get('obj_ids').split(';') + as_s = facade.get_as_equipment_by_id(as_ids) + elif kwargs.get('asn_ids'): + as_ids = kwargs.get('asn_ids').split(';') + as_s = facade.get_as_equipment_by_asn(as_ids) + elif kwargs.get('equip_ids'): + as_ids = kwargs.get('equip_ids').split(';') + as_s = facade.get_as_equipment_by_equip(as_ids) + + serializer_as = serializers.AsnEquipmentV4Serializer( + as_s, + many=True, + fields=self.fields, + include=self.include, + exclude=self.exclude, + kind=self.kind + ) + + data = render_to_json( + serializer_as, + main_property='asn_equipment', + obj_model=obj_model, + request=request, + only_main_property=only_main_property + ) + + return Response(data, status=status.HTTP_200_OK) + + @logs_method_apiview + @raise_json_validate('asn_equipment_post_v4') + @permission_classes_apiview((IsAuthenticated, Write)) + @commit_on_success + def post(self, request, *args, **kwargs): + """Create new ASNEquipment.""" + + as_s = request.DATA + json_validate(SPECS.get('asn_equipment_post_v4')).validate(as_s) + response = list() + for as_ in as_s['asn_equipment']: + response = facade.create_asn_equipment(as_) + + return Response(response, status=status.HTTP_201_CREATED) + + @logs_method_apiview + @permission_classes_apiview((IsAuthenticated, Write)) + @commit_on_success + def delete(self, request, *args, **kwargs): + """Delete AS.""" + + if not kwargs.get('asn_ids'): + + obj_ids = kwargs['obj_ids'].split(';') + for obj in obj_ids: + facade.delete_asn_equipment(obj) + else: + obj_ids = kwargs['asn_ids'].split(';') + for obj in obj_ids: + facade.delete_asn_equipment_by_asn(obj) + + return Response({}, status=status.HTTP_200_OK) + + @logs_method_apiview + # @raise_json_validate('as_put_v4') + @permission_classes_apiview((IsAuthenticated, Write)) + @commit_on_success + def put(self, request, *args, **kwargs): + """Update AS.""" + + asn_equipment = request.DATA + # json_validate(SPECS.get('as_put_v4')).validate(as_s) + response = list() + + if not kwargs.get('asn_ids'): + for as_ in asn_equipment['asn_equipment']: + as_obj = facade.update_asn_equipment(as_) + response.append({'id': as_obj.id}) + else: + for as_ in asn_equipment['asn_equipment']: + response = facade.update_asn_equipment_by_asn( + kwargs.get('asn_ids'), as_) + + return Response(response, status=status.HTTP_200_OK) diff --git a/networkapi/api_rack/facade.py b/networkapi/api_rack/facade.py index b7a603e1e..083e22425 100644 --- a/networkapi/api_rack/facade.py +++ b/networkapi/api_rack/facade.py @@ -36,7 +36,6 @@ from networkapi.api_rack import serializers as rack_serializers from networkapi.api_rack import exceptions from networkapi.api_rack import provision -from networkapi.api_rack import autoprovision from networkapi.system import exceptions as var_exceptions from networkapi.system.facade import get_value as get_variable from networkapi.api_rest.exceptions import ValidationAPIException, ObjectDoesNotExistException, \ @@ -140,7 +139,6 @@ def get_fabric(idt=None, name=None, id_dc=None): else: fabric = fabric_obj.get_dcrooms() - for i in fabric: fabric_list.append(rack_serializers.DCRoomSerializer(i).data) @@ -154,7 +152,7 @@ def update_fabric_config(fabric_id, fabric_dict): try: fabriconfig = ast.literal_eval(fabric.config) - except: + except Exception: fabriconfig = dict() fabric_dict = fabric_dict.get("config") @@ -195,10 +193,12 @@ def save_rack_dc(rack_dict): rack.id_sw1 = Equipamento().get_by_pk(rack_dict.get('id_sw1')) rack.id_sw2 = Equipamento().get_by_pk(rack_dict.get('id_sw2')) rack.id_sw3 = Equipamento().get_by_pk(rack_dict.get('id_ilo')) - rack.dcroom = DatacenterRooms().get_dcrooms(idt=rack_dict.get('fabric_id')) if rack_dict.get('fabric_id') else None + rack.dcroom = DatacenterRooms().get_dcrooms( + idt=rack_dict.get('fabric_id')) if rack_dict.get('fabric_id') else None if not rack.nome: - raise exceptions.InvalidInputException("O nome do Rack não foi informado.") + raise exceptions.InvalidInputException( + "O nome do Rack não foi informado.") rack.save_rack() return rack @@ -218,8 +218,8 @@ def update_rack(rack_id, rack): rack_obj.id_sw1 = Equipamento().get_by_pk(int(rack.get("id_sw1"))) rack_obj.id_sw2 = Equipamento().get_by_pk(int(rack.get("id_sw2"))) rack_obj.id_ilo = Equipamento().get_by_pk(int(rack.get("id_ilo"))) - rack_obj.dcroom = DatacenterRooms().get_dcrooms(idt=rack.get('fabric_id')) if rack.get('fabric_id') \ - else None + rack_obj.dcroom = DatacenterRooms().get_dcrooms( + idt=rack.get('fabric_id')) if rack.get('fabric_id') else None rack_obj.save() @@ -242,13 +242,16 @@ def get_rack(fabric_id=None, rack_id=None): try: if fabric_id: rack = rack_obj.get_rack(dcroom_id=fabric_id) - rack_list = rack_serializers.RackSerializer(rack, many=True).data if rack else list() + rack_list = rack_serializers.RackSerializer( + rack, many=True).data if rack else list() elif rack_id: rack = rack_obj.get_rack(idt=rack_id) - rack_list = [rack_serializers.RackSerializer(rack).data] if rack else list() + rack_list = [rack_serializers.RackSerializer( + rack).data] if rack else list() else: rack = rack_obj.get_rack() - rack_list = rack_serializers.RackSerializer(rack, many=True).data if rack else list() + rack_list = rack_serializers.RackSerializer( + rack, many=True).data if rack else list() return rack_list @@ -327,9 +330,10 @@ def _buscar_ip(id_sw): for ip_equip in ips_equip: ip_sw = ip_equip.ip - if not ip_sw == None: + if not ip_sw is None: if regexp.search(ip_sw.networkipv4.vlan.ambiente.ambiente_logico.nome) is not None: - return str(ip_sw.oct1) + '.' + str(ip_sw.oct2) + '.' + str(ip_sw.oct3) + '.' + str(ip_sw.oct4) + return str(ip_sw.oct1) + '.' + str(ip_sw.oct2) + '.' \ + + str(ip_sw.oct3) + '.' + str(ip_sw.oct4) return "" @@ -368,7 +372,9 @@ def gerar_arquivo_config(ids): oob["modelo"] = rack.id_ilo.modelo.nome equips.append(oob) except: - raise Exception("Erro: Informações incompletas. Verifique o cadastro do Datacenter, do Fabric e do Rack") + raise Exception("Erro: Informações incompletas. " + "Verifique o cadastro do Datacenter, " + "do Fabric e do Rack") prefixspn = "SPN" prefixlf = "LF-" @@ -395,13 +401,15 @@ def gerar_arquivo_config(ids): pass except: raise Exception( - "Erro ao buscar o roteiro de configuracao ou as interfaces associadas ao equipamento: %s." - % equip.get("nome")) + "Erro ao buscar o roteiro de configuracao ou as interfaces " + "associadas ao equipamento: %s." % equip.get("nome")) try: - equip["roteiro"] = _buscar_roteiro(equip.get("id"), "CONFIGURACAO") + equip["roteiro"] = _buscar_roteiro(equip.get("id"), + "CONFIGURACAO") equip["ip_mngt"] = _buscar_ip(equip.get("id")) except: - raise Exception("Erro ao buscar os roteiros do equipamento: %s" % equip.get("nome")) + raise Exception("Erro ao buscar os roteiros do equipamento: " + "%s" % equip.get("nome")) # autoprovision.autoprovision_splf(rack, equips) # autoprovision.autoprovision_coreoob(rack, equips) @@ -448,7 +456,8 @@ def _create_spnlfenv(user, rack): for spn in range(spines): amb_log_name = "SPINE0" + str(spn+1) + "LEAF" try: - id_amb_log = models_env.AmbienteLogico().get_by_name(amb_log_name).id + id_amb_log = models_env.AmbienteLogico().get_by_name( + amb_log_name).id except: amb_log_dict = models_env.AmbienteLogico() amb_log_dict.nome = amb_log_name @@ -518,7 +527,8 @@ def _create_spnlfvlans(rack, user): base_rack = 1 vlan_base = env.min_num_vlan_1 vlan_number = int(vlan_base) + int(rack_number) + (spn-1)*base_rack - vlan_name = "VLAN_" + env.divisao_dc.nome + "_" + env.ambiente_logico.nome + "_" + rack.nome + vlan_name = "VLAN_" + env.divisao_dc.nome + "_" + env.ambiente_logico.nome \ + + "_" + rack.nome for net in env.configs: prefix = int(net.subnet_mask) @@ -788,7 +798,8 @@ def _create_lflf_envs(rack): for env in env_lf: config_subnet = [] for net in env.configs: - cidr = list(IPNetwork(net.network).subnet(int(net.subnet_mask)))[rack.numero] + cidr = list(IPNetwork(net.network).subnet(int( + net.subnet_mask)))[rack.numero] network = { 'network': str(cidr), 'ip_version': str(net.ip_version), @@ -907,9 +918,12 @@ def _create_oobvlans(rack, user): oct4, prefix = var.split('/') netmask = str(new_v4.netmask) mask1, mask2, mask3, mask4 = netmask.split('.') - network = dict(oct1=oct1, oct2=oct2, oct3=oct3, oct4=oct4, prefix=prefix, mask_oct1=mask1, mask_oct2=mask2, - mask_oct3=mask3, mask_oct4=mask4, cluster_unit=None, vlan=vlan.id, - network_type=config.id_network_type.id, environmentvip=None) + network = dict(oct1=oct1, oct2=oct2, oct3=oct3, + oct4=oct4, prefix=prefix, mask_oct1=mask1, + mask_oct2=mask2, mask_oct3=mask3, mask_oct4=mask4, + cluster_unit=None, vlan=vlan.id, + network_type=config.id_network_type.id, + environmentvip=None) log.debug("Network allocated: "+ str(network)) facade_redev4_v3.create_networkipv4(network, user) @@ -968,6 +982,8 @@ def allocate_env_vlan(user, rack_id): rack_env.allocate() + rack_env.create_asn_equipment() + return rack_env.rack @@ -980,6 +996,7 @@ def deallocate_env_vlan(user, rack_id): rack_env.rack_vlans_remove() rack_env.rack_environment_remove() + # rack_env.rack_asn_remove() rack_env.deallocate() @@ -992,59 +1009,87 @@ def api_foreman(rack): NETWORKAPI_FOREMAN_PASSWORD = get_variable("foreman_password") FOREMAN_HOSTS_ENVIRONMENT_ID = get_variable("foreman_hosts_environment_id") except ObjectDoesNotExist: - raise var_exceptions.VariableDoesNotExistException("Erro buscando as variáveis relativas ao Foreman.") + raise var_exceptions.VariableDoesNotExistException( + "Erro buscando as variáveis relativas ao Foreman.") - foreman = Foreman(NETWORKAPI_FOREMAN_URL, (NETWORKAPI_FOREMAN_USERNAME, NETWORKAPI_FOREMAN_PASSWORD), api_version=2) + foreman = Foreman(NETWORKAPI_FOREMAN_URL, + (NETWORKAPI_FOREMAN_USERNAME, + NETWORKAPI_FOREMAN_PASSWORD), + api_version=2) - # for each switch, check the switch ip against foreman know networks, finds foreman hostgroup + # for each switch, check the switch ip against foreman know networks, + # finds foreman hostgroup # based on model and brand and inserts the host in foreman # if host already exists, delete and recreate with new information - for [switch, mac] in [[rack.id_sw1, rack.mac_sw1], [rack.id_sw2, rack.mac_sw2], [rack.id_ilo, rack.mac_ilo]]: - # Get all foremand subnets and compare with the IP address of the switches until find it - if mac == None: - raise RackConfigError(None, rack.nome, ("Could not create entry for %s. There is no mac address." % - switch.nome)) + for [switch, mac] in [[rack.id_sw1, rack.mac_sw1], + [rack.id_sw2, rack.mac_sw2], + [rack.id_ilo, rack.mac_ilo]]: + # Get all foremand subnets and compare with the IP address + # of the switches until find it + if mac is None: + raise RackConfigError( + None, rack.nome, + ("Could not create entry for %s. " + "There is no mac address." % switch.nome)) ip = _buscar_ip(switch.id) - if ip == None: - raise RackConfigError(None, rack.nome, ("Could not create entry for %s. There is no management IP." % - switch.nome)) + if ip is None: + raise RackConfigError( + None, rack.nome, + ("Could not create entry for %s. " + "There is no management IP." % switch.nome)) switch_cadastrado = 0 for subnet in foreman.subnets.index()['results']: network = IPNetwork(ip + '/' + subnet['mask']).network - # check if switches ip network is the same as subnet['subnet']['network'] e subnet['subnet']['mask'] + # check if switches ip network is the same as subnet['subnet']['network'] + # e subnet['subnet']['mask'] if network.__str__() == subnet['network']: subnet_id = subnet['id'] - hosts = foreman.hosts.index(search = switch.nome)['results'] + hosts = foreman.hosts.index(search=switch.nome)['results'] if len(hosts) == 1: - foreman.hosts.destroy(id = hosts[0]['id']) + foreman.hosts.destroy(id=hosts[0]['id']) elif len(hosts) > 1: - raise RackConfigError(None, rack.nome, ("Could not create entry for %s. There are multiple entries " - "with the sam name." % switch.nome)) + raise RackConfigError( + None, rack.nome, + ("Could not create entry for %s. " + "There are multiple entries " + "with the sam name." % switch.nome)) # Lookup foreman hostgroup # By definition, hostgroup should be Marca+"_"+Modelo hostgroup_name = switch.modelo.marca.nome + "_" + switch.modelo.nome - hostgroups = foreman.hostgroups.index(search = hostgroup_name) + hostgroups = foreman.hostgroups.index(search=hostgroup_name) if len(hostgroups['results']) == 0: - raise RackConfigError(None, rack.nome, "Could not create entry for %s. Could not find hostgroup %s " - "in foreman." % (switch.nome, hostgroup_name)) - elif len(hostgroups['results'])>1: - raise RackConfigError(None, rack.nome, "Could not create entry for %s. Multiple hostgroups %s found" - " in Foreman." % (switch.nome, hostgroup_name)) + raise RackConfigError( + None, rack.nome, + "Could not create entry for %s. " + "Could not find hostgroup %s " + "in foreman." % (switch.nome, hostgroup_name)) + elif len(hostgroups['results']) > 1: + raise RackConfigError( + None, rack.nome, + "Could not create entry for %s. " + "Multiple hostgroups %s found" + " in Foreman." % (switch.nome, hostgroup_name)) else: hostgroup_id = hostgroups['results'][0]['id'] - foreman.hosts.create(host = {'name': switch.nome, 'ip': ip, 'mac': mac, - 'environment_id': FOREMAN_HOSTS_ENVIRONMENT_ID, - 'hostgroup_id': hostgroup_id, 'subnet_id': subnet_id, - 'build': 'true', 'overwrite': 'true'}) + foreman.hosts.create(host={'name': switch.nome, + 'ip': ip, + 'mac': mac, + 'environment_id': FOREMAN_HOSTS_ENVIRONMENT_ID, + 'hostgroup_id': hostgroup_id, + 'subnet_id': subnet_id, + 'build': 'true', + 'overwrite': 'true'}) switch_cadastrado = 1 if not switch_cadastrado: - raise RackConfigError(None, rack.nome, "Unknown error. Could not create entry for %s in foreman." % - switch.nome) + raise RackConfigError( + None, rack.nome, + "Unknown error. Could not create entry for " + "%s in foreman." % switch.nome) # ################################################### old diff --git a/networkapi/api_rack/rackenvironments.py b/networkapi/api_rack/rackenvironments.py index c3d1d0f95..a3a932e50 100644 --- a/networkapi/api_rack/rackenvironments.py +++ b/networkapi/api_rack/rackenvironments.py @@ -23,6 +23,8 @@ class RackEnvironment: def __init__(self, user, rack_id): self.rack = Rack().get_by_pk(rack_id) self.user = user + self.dcroom_config = self._get_config_file() + self.rack_asn = self._get_rack_asn() @staticmethod def save_environment(self, env): @@ -290,22 +292,7 @@ def prod_environment_save(self): id_grupo_l3 = grupo_l3_dict.id pass - if self.rack.dcroom.config: - fabricconfig = self.rack.dcroom.config - else: - log.debug("sem configuracoes do fabric %s" % str(self.rack.dcroom.id)) - fabricconfig = list() - - try: - fabricconfig = json.loads(fabricconfig) - except: - pass - - try: - fabricconfig = ast.literal_eval(fabricconfig) - log.debug("config -ast: %s" % str(fabricconfig)) - except: - pass + fabricconfig = self.dcroom_config environment = list() for env in prod_envs: @@ -376,18 +363,7 @@ def children_prod_environment_save(self): except Exception as e: raise Exception("Erro: %s" % e) - if self.rack.dcroom.config: - fabricconfig = self.rack.dcroom.config - else: - log.debug("No fabric configurations %s" % str(self.rack.dcroom.id)) - fabricconfig = list() - - try: - fabricconfig = json.loads(fabricconfig) - fabricconfig = ast.literal_eval(fabricconfig) - log.debug("config -ast: %s" % str(fabricconfig)) - except: - log.debug("Error loading fabric json.") + fabricconfig = self.dcroom_config environment = None father_id = env.id @@ -519,3 +495,66 @@ def rack_vlans_remove(self): vlan.delete_v3() log.debug("Vlans: %s. total: %s" % (vlans, len(vlans))) + + def rack_asn_remove(self): + from networkapi.api_asn.models import Asn + from networkapi.api_asn.v4 import facade + + if self.rack_asn: + asn = facade.get_as_by_asn(self.rack_asn) + asn_equipment = facade.get_as_equipment_by_asn([asn]) + log.debug(asn_equipment) + for obj in asn_equipment: + obj.delete_v4() + + Asn.delete_as([asn]) + + def create_asn_equipment(self): + from networkapi.api_asn.v4 import facade + + if self.rack_asn: + try: + asn_obj = dict(name=self.rack_asn, description=self.rack.nome) + asn = facade.create_as(asn_obj) + + obj = dict(asn=asn.id, equipment=[self.rack.id_sw1.id, + self.rack.id_sw2.id, + self.rack.id_ilo.id]) + facade.create_asn_equipment(obj) + except Exception as e: + log.debug("Error while trying to create the asn %s for rack %s. " + "E: %s" % (self.rack_asn, self.rack.nome, e)) + + def _get_rack_asn(self): + + if self.dcroom_config: + BGP = self.dcroom_config.get("BGP") + BASE_AS_LFS = int(BGP.get("leafs")) + rack_as = BASE_AS_LFS + self.rack.numero + + if rack_as: + return rack_as + else: + return None + + def _get_config_file(self): + + fabricconfig = self.rack.dcroom.config + + try: + fabricconfig = json.loads(fabricconfig) + log.debug("type -ast: %s" % str(type(fabricconfig))) + except: + pass + + try: + fabricconfig = ast.literal_eval(fabricconfig) + log.debug("config -ast: %s" % str(fabricconfig)) + except: + pass + + if fabricconfig: + return fabricconfig + else: + log.debug("sem configuracoes do fabric %s" % str(self.rack.dcroom.id)) + return list() diff --git a/networkapi/api_rack/views.py b/networkapi/api_rack/views.py index 602533c1d..f691b1424 100644 --- a/networkapi/api_rack/views.py +++ b/networkapi/api_rack/views.py @@ -179,7 +179,6 @@ def post(self, *args, **kwargs): except Exception as e: log.exception(e) raise exceptions.RackAplError(e) - # SEPARAR AQUI! datas = dict() success_map = dict() @@ -227,7 +226,6 @@ def post(self, *args, **kwargs): raise api_exceptions.NetworkAPIException(e) -#===============================================@==================================================== class RackConfigView(APIView): @commit_on_success diff --git a/networkapi/plugins/Juniper/JUNOS/__init__.py b/networkapi/plugins/Juniper/JUNOS/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/networkapi/plugins/Juniper/JUNOS/plugin.py b/networkapi/plugins/Juniper/JUNOS/plugin.py new file mode 100644 index 000000000..ec8a29b44 --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/plugin.py @@ -0,0 +1,362 @@ +# -*- 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 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 +from jnpr.junos.exception import \ + ConnectError, \ + ConfigLoadError, \ + LockError, \ + UnlockError, \ + ConnectClosedError, \ + CommitError, \ + RpcError + +log = logging.getLogger(__name__) + + +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/'] + ignore_warning_list = ['statement not found'] + + def __init__(self, **kwargs): + super(JUNOS, self).__init__(connect_port=830, **kwargs) + if 'quantity_of_times_to_try_lock' in kwargs: + self.quantity_of_times_to_try_lock = kwargs.get('quantity_of_times_to_try_lock') + + 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 connect(self): + + """ + 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). + """ + + 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() + except Exception: + 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)) + + try: + self.remote_conn = Device( + host=self.equipment_access.fqdn, + user=self.equipment_access.user, + passwd=self.equipment_access.password, + port=self.connect_port) + self.remote_conn.open() + self.configuration = Config(self.remote_conn) + + if self.remote_conn.connected: + log.info("The connection on host {} was opened successfully!".format(self.equipment_access.fqdn)) + return True + + except ConnectError as e: + log.error("Could not connect to Juniper host {}: {}".format(self.equipment_access.fqdn, e)) + raise ConnectError(e) + + except Exception, e: + log.error("Unknown error while connecting to host {}: {}".format(self.equipment_access.fqdn, e)) + raise Exception + + def close(self): + + """ + Disconnect to equipment via ssh using PyEz. + + :returns: + True on success or raise an exception on any fail (will NOT return a false result, due project decision). + """ + + log.info("Trying to close connection on host {} ... ".format(self.equipment_access.fqdn)) + + try: + self.remote_conn.close() + log.info("The connection was closed successfully on host {}!".format(self.equipment_access.fqdn)) + return True + + except ConnectClosedError, e: + log.error("Cannot close connection on host {}: {}".format(self.equipment_access.fqdn, e)) + raise ConnectClosedError + + except Exception, e: + log.error("Unknown error while closing connection on host {}: {}".format(self.equipment_access.fqdn, e)) + raise Exception + + def copyScriptFileToConfig(self, filename): + + """ + Receives the file path (usually in /mnt/scripts/tftpboot/networkapi/generated_config/interface/) + where contains the command to be executed in the equipment + + :param str filename: must contain the full path and file name + :param str use_vrf: not used + :param str destination: not used + + :returns: + Returns a success message, otherwise, raise an exception. That means will NOT return a false result. + """ + + 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(file_path, "r") + command = command_file.read() + log.info("Load configuration from file {} successfully!".format(file_path)) + return self.exec_command(command) + + except IOError, e: + log.error("File not found {}: {}".format(file_path, e)) + self.close() + raise IOError + + except Exception, e: + log.error("Unknown error while accessing configuration file {}: {}".format(file_path, e)) + self.close() + raise Exception + + def exec_command(self, command): + + """ + Execute a junos command 'set' in the equipment. + + :param str command: junos command 'set' + :param str success_regex: not used + :param str invalid_regex: not used + :param str error_regex: not used + + :returns: + Returns a success message, otherwise, raise an exception. That means will NOT return a false result. + """ + + log.info("Trying to execute a configuration on host {} ... ".format(self.equipment_access.fqdn)) + + try: + self.__try_lock() + self.configuration.rollback() + self.configuration.load(command, format='set', ignore_warning=self.ignore_warning_list) + self.configuration.commit_check() + self.configuration.commit() + self.configuration.unlock() + + result_message = "Configuration junos was executed successfully on {}".format(self.equipment_access.fqdn) + log.info(result_message) + return result_message + + except LockError as e: + log.error("Couldn't lock host {} " + "(close will be tried for safety): {}".format(self.equipment_access.fqdn, e)) + self.close() + raise LockError + + except UnlockError as e: + log.error("Couldn't unlock host {} " + "(rollback and close will be tried for safety): {}".format(self.equipment_access.fqdn, e)) + self.configuration.rollback() + self.close() + raise UnlockError + + except ConfigLoadError as e: + log.error("Couldn't load configuration on host {} " + "(rollback, unlock and close will be tried for safety): {}".format(self.equipment_access.fqdn, e)) + self.configuration.rollback() + self.configuration.unlock() + self.close() + raise ConfigLoadError + + except CommitError as e: + log.error("Couldn't commit configuration on {} " + "(rollback, unlock and close will be tried for safety): {}".format(self.equipment_access.fqdn, e)) + self.configuration.rollback() + self.configuration.unlock() + self.close() + raise CommitError + + except RpcError as e: + log.error("A remote procedure call exception occurred on host {} " + "(close will be tried for safety): {}".format(self.equipment_access.fqdn, e)) + self.close() + raise RpcError + + except Exception as e: + 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() + self.close() + raise Exception + + def ensure_privilege_level(self): + + """ + Ensure privilege level verifying if the current user is super-user. + + :returns: + Returns True if success, otherwise, raise an exception (it will NOT return a false result) + """ + + log.info("Trying to ensure privilege level for user {} on host {} ... ".format( + self.equipment_access.user, self.equipment_access.fqdn)) + + try: + # timeout: duration of time in seconds must wait for the expected result + ss = StartShell(self.remote_conn, timeout=2) + ss.open() + output = ss.run('cli -c "show cli authorization"') + + # output is a tuple [bool, string], example: + # (False, u'cli -c "show cli authorization"\r\r\nCurrent user: \'root \' class \'super-user\ ....) + # This string will be parsed to get the user class: + + # get the target part and split it by \n + result = output[1].split('\n') + + # get the target part; split by '; and get the target part + current_user_class = result[1].split("'")[3] + + if current_user_class != 'super-user': + log.error("Couldn't validate user {} privileges on host {}. " + "User class is '{}' and need to be 'super-user'. " + "(close connection will be executed for safety)" + .format(self.equipment_access.user, self.equipment_access.fqdn, current_user_class)) + self.close() + raise Exception + else: + log.info("The privilege for user {} ('super-user') was satisfied on host {}!".format( + self.equipment_access.user, self.equipment_access.fqdn)) + return True + + except Exception as e: + 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 + + def __try_lock(self): + + """ + Try to lock equipment. + + :returns: + 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() + log.info("Host {} was locked successfully!".format(self.equipment_access.fqdn)) + return True + except (LockError, RpcError, Exception), e: + + count = x + 1 + # Keep looping ... + log.warning( + "Host {} could not be locked. Automatic try in {} seconds - {}/{} {}".format( + self.equipment_access.fqdn, + self.seconds_to_wait_to_try_lock, + count, + self.quantity_of_times_to_try_lock, + e)) + + if count == self.quantity_of_times_to_try_lock: + 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/samples/__init__.py b/networkapi/plugins/Juniper/JUNOS/samples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/networkapi/plugins/Juniper/JUNOS/samples/sample_command.txt b/networkapi/plugins/Juniper/JUNOS/samples/sample_command.txt new file mode 100644 index 000000000..7e210088a --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/samples/sample_command.txt @@ -0,0 +1 @@ +set interfaces gr-0/0/0 description "Some description in file for gr-0/0/0" \ No newline at end of file diff --git a/networkapi/plugins/Juniper/JUNOS/samples/sample_command_line.py b/networkapi/plugins/Juniper/JUNOS/samples/sample_command_line.py new file mode 100644 index 000000000..79add3b3a --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/samples/sample_command_line.py @@ -0,0 +1,60 @@ +""" +How to use: +python sample_command_line.py -device 'HOST' -user 'SSH_USER_NAME' -password 'SSH_USER_PASSWORD' -command 'COMMAND' +""" + +from lxml import etree +from jnpr.junos import Device +from jnpr.junos.utils.config import Config +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('-device', help='Input host', type=str) +parser.add_argument('-user', help='Input user name', type=str) +parser.add_argument('-password', help='Input password', type=str) +parser.add_argument('-command', help='Input command', type=str) +args, unknown = parser.parse_known_args() + +device = args.device +user = args.user +password = args.password +command = args.command + +try: + + dev = Device(host=device, user=user, password=password, gather_facts=False) + open_result = dev.open() + print("Open connection ... {}".format(open_result.connected)) + + conf = Config(dev) + print("Load config ...") + + lock_response = conf.lock() + print("Locking config ... {}".format(lock_response)) + + rollback_response = conf.rollback() + print("Rollback config ... {}".format(rollback_response)) + + load_result = conf.load(command, format='set', ignore_warning=['statement not found']) + load_result_tostring = etree.tostring(load_result, encoding='unicode', pretty_print=True) + print("Load command ... \n{}".format(load_result_tostring)) + + commit_check_result = conf.commit_check() + print("Check result ... {}".format(commit_check_result)) + + commit_result = conf.commit() + print("Commit ... {}".format(commit_result)) + + unlock_response = conf.unlock() + print("Unlocking config ... {}".format(unlock_response)) + + close_response = dev.close() + print("Close connection ... {}".format("Success" if not open_result.connected else "Failed")) + + print ("DONE") + +except Exception, e: + + print(e) + close_response = dev.close() + print("Closed connection? {}".format(not open_result.connected)) diff --git a/networkapi/plugins/Juniper/JUNOS/samples/sample_networkapi_junos_plugin.py b/networkapi/plugins/Juniper/JUNOS/samples/sample_networkapi_junos_plugin.py new file mode 100644 index 000000000..f378aa296 --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/samples/sample_networkapi_junos_plugin.py @@ -0,0 +1,65 @@ +""" +How to use: + +Step 1 - Edit this file variables accordingly: + host = 'HOSTNAME_OR_IP' + user = 'SSH_USER_NAME' + password = 'SSH_USER_PASSWORD' + +Step 2 - Go to another terminal and execute the test (for ANY code modification, please restart this step) + docker exec -it netapi_app python ./manage.py shell + execfile('networkapi/plugins/Juniper/JUNOS/samples/sample_networkapi_junos_plugin.py') + +Step 3 [optional] - Check more logs in networkapi: + docker exec -it netapi_app tail -f /tmp/networkapi.log + +Step 4 - Check result in equipment, exp.: + ssh SSH_USER@HOSTNAME_OR_IP + cli + show interfaces gr-0/0/0 +""" + +import logging +from networkapi.plugins.Juniper.JUNOS.plugin import JUNOS + + +# Temporary Equipment class +class EquipamentoAcesso: + def __init__(self, fqdn, user, password): + self.fqdn = fqdn + self.user = user + self.password = password + + +# Config log messages +log = logging.getLogger(__name__) +log_test_prefix = '[Junos Plugin]' +log.debug('%s Start sample' % log_test_prefix) + +# Requirements (args, equipment and plugin) +host = 'HOSTNAME_OR_IP' +user = 'SSH_USER_NAME' +password = 'SSH_USER_PASSWORD' + +# Temporary equipment access object (defined above as EquipamentoAcesso) +equipment_access = EquipamentoAcesso(host, user, password) + +# NetworkAPI junos plugin object +equip_plugin = JUNOS(equipment_access=equipment_access) + +""" OPEN CONNECTION """ +print("Open connection {}...".format(host)) +print("Connection result: {}".format(equip_plugin.connect())) + +""" CHECK PRIVILEGES """ +print("Check privilege {}...".format(host)) +print("Privilege result: {}".format(equip_plugin.ensure_privilege_level())) + +""" EXECUTE CONFIGURATION """ +print("Execute configuration file {}...".format(host)) +# equip_plugin.exec_command(command='set interfaces gr-0/0/0 description "Some description teste3 for gr-0/0/0 at "') +print("Execute configuration result: {}".format(equip_plugin.copyScriptFileToConfig(filename="networkapi/plugins/Juniper/JUNOS/samples/sample_command.txt"))) + +""" CLOSE CONNECTION """ +print("Close connection {}...".format(host)) +print("Close connection result: {}".format(equip_plugin.close())) diff --git a/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone.py b/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone.py new file mode 100644 index 000000000..0fe798be9 --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone.py @@ -0,0 +1,49 @@ +""" +Use example: +python networkapi/plugins/Juniper/JUNOS/samples/sample_standalone.py -device 'HOST' -user 'SSH_USER' -password 'SSH_USER_PASSWORD' +""" + +from jnpr.junos import Device +from jnpr.junos.utils.config import Config +from jnpr.junos.exception import ConnectError, ConfigLoadError, LockError, UnlockError, ConnectClosedError, CommitError +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('-device', help='Input host name', type=str) +parser.add_argument('-user', help='Input user name', type=str) +parser.add_argument('-password', help='Input password', type=str) +args, unknown = parser.parse_known_args() + +device = args.device +user = args.user +password = args.password + +print("Open connection ...") +dev = Device(host=device, user=user, password=password, gather_facts=False) +dev.open() + +print("Load config ...") +conf = Config(dev) + +print("Locking config ...") +conf.lock() + +print("Rollback config ...") +conf.rollback() + +data = 'set interfaces gr-0/0/0 description "Some description sample standalone for gr-0/0/0"' +conf.load(data, format='set') # syntax error raises exception + +print("Check ...") +conf.commit_check() + +print("Commit ...") +conf.commit() + +print("Unlocking config ...") +conf.unlock() + +print("Close connection ... ") +dev.close() + +print ("DONE") diff --git a/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_modular.py b/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_modular.py new file mode 100644 index 000000000..6f03ba808 --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_modular.py @@ -0,0 +1,116 @@ +""" +Use example: +python networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_modular.py -device 'HOST' -user 'SSH_USER' -password 'SSH_USER_PASSWORD' +""" + +from jnpr.junos import Device +from jnpr.junos.utils.config import Config +from jnpr.junos.exception import ConnectError, ConfigLoadError, LockError, UnlockError, ConnectClosedError +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('-device', help='Input host name', type=str) +parser.add_argument('-user', help='Input user name', type=str) +parser.add_argument('-password', help='Input password', type=str) +args, unknown = parser.parse_known_args() + + +class Junos: + + def __init__(self, device, username, password): + + self.device = device + self.username = username + self.password = password + self.remote_conn = Device(host=device, user=username, password=password, gather_facts=False) + self.configuration = Config(self.remote_conn) + + def connect(self): + try: + self.remote_conn.open() + except ConnectError as e: + print("Cannot connect: {0}".format(e)) + except Exception, e: + print("An error occurred to connect: {0}".format(e)) + + def exec_command(self, command): + print("\tLocking configuration ...") + self._lock_configuration() + + print("\tLoad configuration ...") + self._load_configuration(data=command, format_type='set') + + print("\tCommit ...") + self._commit() + + print("\tUnlocking configuration ...") + self._unlock_configuration() + + def close(self): + try: + self.remote_conn.close() + except ConnectClosedError, e: + print("Cannot close connection: {0}".format(e)) + except Exception, e: + print("An error occurred to close connect: {0}".format(e)) + + def _load_configuration(self, data, format_type): + # Ps.: If syntax error found, it will be excepted here + try: + self.configuration.load(data, format=format_type) + except ConfigLoadError as err: + print("Cannot load configuration: {0}".format(err)) + self.close() + except Exception, e: + print("An error occurred to load configuration: {0}".format(e)) + + def _lock_configuration(self): + try: + # Guarantee a clean configuration (lock then clean) + self.configuration.lock() + self.configuration.rollback() + except LockError as err: + print("Unable to lock configuration: {0}".format(err)) + self.close() + except Exception, e: + print("An error occurred to lock configuration: {0}".format(e)) + + def _unlock_configuration(self): + try: + self.configuration.unlock() + except UnlockError as e: + print("Unable to unlock configuration: {0}".format(e)) + self.close() + except Exception, e: + print("An error occurred to unlock configuration: {0}".format(e)) + + def _commit(self): + try: + if self.configuration.commit_check(): + self.configuration.commit() + else: + self.configuration.rollback() + print("An error occurred on commit configuration") + raise Exception + except Exception, e: + print("An error occurred to commit configuration: {0}".format(e)) + + +def main(device, username, password): + + plugin = Junos(device, username, password) + + print("Open connection ...") + plugin.connect() # In the future, the connect function will send an equipment (to validate maintenance) + + print("Execute configuration ...") + plugin.exec_command( + command='set interfaces gr-0/0/0 description "Some description sample standalone modular gr-0/0/0"') + + print("Close connection ...") + plugin.close() + + +if __name__ == "__main__": + main(args.device, args.user, args.password) + diff --git a/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_responses.py b/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_responses.py new file mode 100644 index 000000000..df5957c2e --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_responses.py @@ -0,0 +1,70 @@ +""" +Use example: +python networkapi/plugins/Juniper/JUNOS/samples/sample_standalone_responses.py -device 'HOST' -user 'SSH_USER' -password 'SSH_USER_PASSWORD' +""" + +from jnpr.junos import Device +from jnpr.junos.utils.config import Config +from jnpr.junos.exception import ConnectError, ConfigLoadError +from lxml import etree +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('-device', help='Input host name', type=str) +parser.add_argument('-user', help='Input user name', type=str) +parser.add_argument('-password', help='Input password', type=str) +args, unknown = parser.parse_known_args() + +device = args.device +user = args.user +password = args.password +dev = None + +try: + + print("\nOpen connection ... dev.open()") + dev = Device(host=device, user=user, password=password, gather_facts=False) + open_result = dev.open() + print(open_result.connected) # True/False + + conf = Config(dev) + data = 'set interfaces gr-0/0/0 description "Some description 3 for gr-0/0/0"' + + print("\nLock configuration ... conf.lock();") + lock_response = conf.lock() + print(lock_response) + + print("\nRollback configuration ... conf.rollback()") + rollback_response = conf.rollback() # True/False + print(rollback_response) + + print("\nLoad configuration ... conf.load(data") + load_result = conf.load(data, format='set') + print(etree.tostring(load_result, encoding='unicode', pretty_print=True)) # XML + + print("Commit check configuration ... conf.commit_check()") + commit_check_result = conf.commit_check() + print(commit_check_result) # True/False + + print("\nCommit configuration ... conf.commit()") + commit_result = conf.commit() + print(commit_result) # True/False + + print("\nUnlock configuration ... conf.unlock()") + unlock_response = conf.unlock() + print(unlock_response) + + print("\nClose connection ... .close()") + close_response = dev.close() + print(close_response) # None + # print(open_result.connected) # False/True + +except ConnectError as err: + print("Cannot connect to device: {0}".format(err)) + dev.close() +except ConfigLoadError as err: + print("Cannot load configuration: {0}".format(err)) + dev.close() +except Exception as err: + print("Some error occurred: {0}".format(err)) + dev.close() diff --git a/networkapi/plugins/Juniper/JUNOS/tests.py b/networkapi/plugins/Juniper/JUNOS/tests.py new file mode 100644 index 000000000..6dbf67910 --- /dev/null +++ b/networkapi/plugins/Juniper/JUNOS/tests.py @@ -0,0 +1,179 @@ +from networkapi.test.test_case import NetworkApiTestCase +from networkapi.plugins.base import BasePlugin +from networkapi.plugins.Juniper.JUNOS.plugin import JUNOS +from mock import patch, MagicMock +from jnpr.junos.exception import ConnectError, LockError + + +class JunosPluginTest(NetworkApiTestCase): + + """ + Junos plugin tests + + How to use: + cd GloboNetworkAPI + docker exec -it netapi_app ./fast_start_test_reusedb.sh networkapi/plugins/Juniper/JUNOS + + General notes: + - autospec=True: responds to methods that actually exist in the real class + - assert_called_once_with() assert if an important step was called + """ + + @patch('networkapi.equipamento.models.EquipamentoAcesso', autospec=True) + def setUp(self, mock_equipment_access): + + """ + Equipment mock is a fake class used in specific test cases below + """ + + mock_equipment_access.fqdn = 'any fqdn' + mock_equipment_access.user = 'any user' + mock_equipment_access.password = 'any password' + self.mock_equipment_access = mock_equipment_access + + def test_create_junos_with_super_class(self): + + """ + test_create_junos_with_super_class - test if Junos plugin is the BasePlugin + """ + + plugin = JUNOS() + self.assertIsInstance(plugin, BasePlugin) + + @patch('networkapi.plugins.Juniper.JUNOS.plugin.Device', autospec=True) + def test_connect_success(self, mock_device): + + """ + test_connect_success - Simulate connect success using Junos Device mock. + + Note: All internal functions in Device Class are mocked, raising no exceptions on it + + Warning: For an unknown reason was not possible to use + 'jnpr.junos.Device' instead 'networkapi.plugins.Juniper.JUNOS.plugin.Device' + """ + + # Mocking result + mock_device.return_value.connected = True + + # Close real test + plugin = JUNOS(equipment_access=self.mock_equipment_access) + connection_response = plugin.connect() + + # Asserts + plugin.remote_conn.open.assert_called_with() + self.assertIsNotNone(plugin.configuration) + self.assertEqual(connection_response, True) + + def test_connect_wrong_data_exception(self): + + """ + test_connect_wrong_data_exception + """ + + plugin = JUNOS(equipment_access=self.mock_equipment_access) + with self.assertRaises(ConnectError): + plugin.connect() + + @patch('jnpr.junos.utils.config.Config') + def test_exec_command_success(self, mock_config): + + """ + test_exec_command_success - This test asserts the success of the complete workflow of executing any command. + Note: All internal functions in Config Class are mocked, raising no exceptions on it + """ + + # Mocking result + plugin = JUNOS(equipment_access=self.mock_equipment_access) + plugin.plugin_try_lock = MagicMock() + plugin.plugin_try_lock.return_value = True + plugin.configuration = mock_config + + # Close real test + exec_command_response = plugin.exec_command("any command") + + # Assert + plugin.configuration.rollback.assert_called_once() + plugin.configuration.load.assert_called_once() + plugin.configuration.commit_check.assert_called_once() + plugin.configuration.commit.assert_called_once() + plugin.configuration.unlock.assert_called_once() + self.assertIsNotNone(exec_command_response) + + @patch('jnpr.junos.Device') + def test_call_close_success(self, mock_device): + + """ + test_call_close_success - Test if the plugin junos plugin close a connection with the expected asserts + """ + + # Mocking + mock_device.connected = False + plugin = JUNOS(equipment_access=self.mock_equipment_access) + plugin.remote_conn = mock_device + + # Close to a real test: + close_response = plugin.close() + + # Asserts + plugin.remote_conn.close.assert_called_once_with() + self.assertTrue(close_response, True) + + @patch('networkapi.plugins.Juniper.JUNOS.plugin.JUNOS', autospec=True) + def test_call_copyScriptFileToConfig(self, mock_junos_plugin): + mock_junos_plugin.copyScriptFileToConfig("any file path") + mock_junos_plugin.copyScriptFileToConfig.assert_called_with("any file path") + + @patch('networkapi.plugins.Juniper.JUNOS.plugin.StartShell') + def test_call_ensure_privilege_level_success(self, mock_start_shell): + + """ + test_call_ensure_privilege_level_success + + Note: The shell run function expects an array as a return value, + and ensure_privilege_level() parse it to ensure the privilege. + """ + + mock_start_shell.return_value.run.return_value = [ + False, + u'cli -c "show cli authorization"\r\r\nCurrent user: \'root \' class \'super-user\'\r'] + + plugin = JUNOS(equipment_access=self.mock_equipment_access) + result = plugin.ensure_privilege_level() + self.assertTrue(result) + + def test_call_ensure_privilege_level_fail(self): + plugin = JUNOS(equipment_access=self.mock_equipment_access) + with self.assertRaises(Exception): + plugin.ensure_privilege_level() + + @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 diff --git a/networkapi/plugins/Juniper/__init__.py b/networkapi/plugins/Juniper/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/networkapi/plugins/factory.py b/networkapi/plugins/factory.py index 80a1a589b..86790383d 100644 --- a/networkapi/plugins/factory.py +++ b/networkapi/plugins/factory.py @@ -90,6 +90,11 @@ def get_plugin(cls, **kwargs): if re.search('CUMULUS', modelo.upper(), re.DOTALL): from .Cumulus.plugin import Cumulus return Cumulus + 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): + from .Juniper.JUNOS.plugin import JUNOS + return JUNOS raise NotImplementedError('plugin not implemented') @classmethod diff --git a/networkapi/settings.py b/networkapi/settings.py index 80b496116..9e30d6775 100644 --- a/networkapi/settings.py +++ b/networkapi/settings.py @@ -546,6 +546,10 @@ def local_files(path): PROJECT_ROOT_PATH, 'api_asn/v4/specs/as_put.json' ), + 'asn_equipment_post_v4': os.path.join( + PROJECT_ROOT_PATH, + 'api_asn/v4/specs/asn_equipment_post.json' + ), 'equipment_post_v4': os.path.join( PROJECT_ROOT_PATH, 'api_equipment/v4/specs/equipment_post.json' diff --git a/requirements.txt b/requirements.txt index 56cf99a05..eeafa2ce6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ ipaddr==2.1.11 ipython==5.3.0 json-delta==2.0 json-spec[ip]==0.10.1 +junos-eznc==2.5.3 kombu==3.0.37 multiprocessing==2.6.2.1 MySQL-python==1.2.3 @@ -36,4 +37,20 @@ stompest==2.1.6 suds==0.4 schema==0.6.6 supervisor==3.3.4 - +Jinja2==2.9 +MarkupSafe==1.1.1 +PyYAML==5.3 +certifi==2018.10.15 +chardet==3.0.4 +gtextfsm==0.2.1 +lxml==4.5.2 +ncclient==0.6.3 +ntc-templates==1.0.0 +pyparsing==2.4.7 +pyserial==3.4 +scp==0.13.2 +selectors2==2.0.2 +terminal==0.4.0 +transitions==0.6.9 +urllib3==1.24.1 +yamlordereddictloader==0.4.0 diff --git a/requirements_test.txt b/requirements_test.txt index 550329e2d..68d2bad27 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -31,4 +31,22 @@ PyYAML==3.10 Sphinx==1.2.2 sphinx-rtd-theme==0.1.6 sure==1.2.7 -urllib3==1.21.1 +#urllib3==1.21.1 + +Jinja2==2.9 +MarkupSafe==1.1.1 +PyYAML==5.3 +certifi==2018.10.15 +chardet==3.0.4 +gtextfsm==0.2.1 +lxml==4.5.2 +ncclient==0.6.3 +ntc-templates==1.0.0 +pyparsing==2.4.7 +pyserial==3.4 +scp==0.13.2 +selectors2==2.0.2 +terminal==0.4.0 +transitions==0.6.9 +urllib3==1.24.1 +yamlordereddictloader==0.4.0 diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile index 61499a912..970d99bd9 100644 --- a/scripts/docker/Dockerfile +++ b/scripts/docker/Dockerfile @@ -1,7 +1,9 @@ # # Base stage # -FROM alpine:latest as base +FROM alpine:3.7 as base + +RUN apk update RUN apk add --update --no-cache py2-pip \ libldap \ @@ -12,7 +14,15 @@ RUN apk add --update --no-cache py2-pip \ iputils \ bind-tools \ ncurses5-libs \ - curl + curl \ + libffi-dev \ + py-pynacl \ + libffi-dev \ + libxslt-dev \ + gcc \ + libc-dev \ + python-dev + RUN pip install --no-cache --upgrade pip RUN pip install --no-cache virtualenv