Commit e55ebe3b authored by Pablo Armingol's avatar Pablo Armingol
Browse files

first version

parent 8f996309
Loading
Loading
Loading
Loading
+426 −0
Original line number Diff line number Diff line
from typing import Dict, List


class FunctionDefinition:
        
    #Metodo para leer el campo vpn-services
    @staticmethod  
    def readVpnServices(vpnServicesALeer: Dict) -> List:
        # Acceder al campo 'vpn-service' que es un array de Dict con un campo vpn-id
        vpn_service_list = vpnServicesALeer.get('vpn-service', [])#Utilizar get para manejar casos donde 'vpn-service' no está presente
        vpn_ids = [] #Array con todos los ID de cada VPN
        
        # Iterar sobre los elementos de la lista
        for vpn_serviceEnFor in vpn_service_list:
            vpn_id : str = vpn_serviceEnFor.get('vpn-id') #obtengo vpn-id del json iterando
            # baseDatos["vpn-id_" + str(i)] = vpn_id  # Utilizar f-strings para formatear las claves
            vpn_ids.append(vpn_id)
            
        return vpn_ids
    
    #Metodo para escribir en la baseDatos lo leido en vpn-services
    @staticmethod
    def writeVpnServices(listaVPNs : List, baseDatos : Dict) -> Dict:
        
        baseDatos["VPN_IDs"] = listaVPNs
        #baseDatos.update({"VPN_IDs": vpn_ids}) #ambas lineas son validas para añadir al Dict
        baseDatos["VPNs_Dict"] = {}
        #Obtengo cada vpn-id del array y lo meto en VPNs_Dict
        for clave in baseDatos["VPN_IDs"]:
            baseDatos["VPNs_Dict"][clave] = {}#ya tengo un Dict asociado a cada clave
            
        return baseDatos #devuelve la baseDatos pasada por argumento pero actualizada
                    
    #Lee el array site
    @staticmethod
    def readSiteArray(site: List, baseDatos: Dict) -> Dict:
        print("Elementos en site: " + str(len(site)))
        temp : Dict = {} #Diccionario temporal que guarda el ultimo site leido
        temp2 : Dict = {} #Diccionario temporal que registra todos los sites leidos
        lista_SitesID = [] #lista que almacenara los IDs de cada site del array
        i = 1
        for siteElement in site:
            temp = FunctionDefinition.readSiteElement(lista_SitesID, siteElement) #Leo un site y me quedo con ID y Dict
            temp2.update({lista_SitesID[-1] : temp}) # Almaceno esos datos en temp2
            i += 1            
       
        res : Dict = {}
        res["sitesIDs"] = lista_SitesID
        res["sites_Dict"] = temp2 # A sites_Dict se le añaden todos los sites
        return res
            
    #Lee un site
    @staticmethod
    def readSiteElement(listaID: List, siteElement: Dict) -> Dict:
        res: Dict = {}
        listaID.append(siteElement.get('site-id'))
        res.update({"site-id": siteElement.get('site-id')})
        res.update({"management": siteElement.get("management")}) #revistar si está bien
        res.update({"devices": FunctionDefinition.readDevices(siteElement.get('devices'))})
        res.update({"routing-protocol" : FunctionDefinition.readRoutingProtocols(siteElement.get('routing-protocols'))})
        res.update({"site-network-accesses" : FunctionDefinition.readSiteNetworkAccesses(siteElement.get('site-network-accesses'))})
        return res
    #FALTAN MUCHOS CAMPOS A LEER EN site, como locations, site-diversity, maximum-routes, security, service, traffic-protection

    #Lee array de Devices. Si hay más de un device, cambiar código
    @staticmethod
    def readDevices(devices: Dict) -> Dict:
        res: Dict = {}
        devices_list: List = devices.get('device', [])  # Cambié 'devices' por 'device' aquí
        if len(devices_list) > 1:
            i: int = 1
            for eachDevice in devices_list:
                res.update({f"device-id {i}": eachDevice.get('device-id')})
                i += 1
        elif len(devices_list) == 1:
            res.update({"device-id": devices_list[0].get('device-id')})
        return res

    @staticmethod
    def readRoutingProtocols(routingProtocols : Dict) -> Dict:
        res : Dict = {} #todo el dict routing-protocols
        rpArray : List = routingProtocols.get("routing-protocol") #el array routing-protocol           
        res.update({"type":rpArray[0].get("type")})#escribo el tipo en el diccionario
        tipoRoutingProtocol : str = rpArray[0].get("type")#obtiene string "ietf-l3vpn-svc:static"
        tipoResumido : str = tipoRoutingProtocol.split(":")[1] #se queda con la palabra, ospf, bgp, static,... sin el ietf...
        #print("El tipo de routingProtocol es: "+ str(tipoResumido))
        def case1():            
            ospfEntrada : Dict = rpArray[0].get("ospf",{}) #Funciona solo en caso de que el array tenga un elemento
            ospfIntermedio : Dict = {}
            ospfIntermedio.update({"address-family":ospfEntrada.get("address-family",{})})
            ospfIntermedio.update({"area-address":ospfEntrada.get("area-address",{})})
            ospfIntermedio.update({"metric":ospfEntrada.get("metric",{})})
            arrayShamLinksEntrada : List = ospfEntrada.get("sham-links",[]) #el array
            shamLinksFinal : Dict = {}
            arrayShamLinksFinal : List = []
            for elemento in arrayShamLinksEntrada:
                shamLinksFinal.update({"target-site":elemento.get("target-site")})
                shamLinksFinal.update({"metric":elemento.get("metric")})
                arrayShamLinksFinal.append(shamLinksFinal)
                
            ospfIntermedio.update({"sham-links":shamLinksFinal})
                        
            res.update({"ospf":ospfIntermedio})
        
        def case2():
            bgpEntrada : Dict = rpArray[0].get("bgp",{})
            bgpSalida : Dict = {}
            bgpSalida.update({"autonomous-system":bgpEntrada.get("autonomous-system")})
            bgpSalida.update({"address-family":bgpEntrada.get("address-family")})
            res.update({"bgp":bgpSalida})
        
        def case3(): #SIMPLEMENTE COPIA, NO LEE. A IMPLEMENTAR
            cascadedDict: Dict = rpArray[0].get("static", {}).get("cascaded-lan-prefixes", {})
            valueRoutingProtocol = cascadedDict
            res.update({"static":valueRoutingProtocol})   
            print("se ha copiado 'static' pero no se ha leido, falta implementar")        
             
        def case4():
            ripEntrada : Dict = rpArray[0].get("rip",{})
            ripSalida : Dict = {}
            ripSalida.update({"address-family":ripEntrada.get("address-family")})
            res.update({"rip":ripSalida})
            
        def case5():
            vrrpEntrada : Dict = rpArray[0].get("vrrp",{})
            vrrpSalida : Dict = {}
            vrrpSalida.update({"address-family":vrrpEntrada.get("address-family")})
            res.update({"vrrp":vrrpSalida})        
        
        def default():
            #return ValueError(f"Tipo de protocolo no compatible: {tipoResumido}")
            print("TIPO DE PROTOCOLO NO VALIDO")
        
        switch = {
            "ospf": case1,
            "bgp": case2,
            "static": case3,
            "rip":case4,
            "vrrp": case5
        }
        #res.update({tipoResumido: switch.get(tipoResumido, default)()})
        switch.get(tipoResumido)()
        # print("_______________________d____________"+str(res))
        return res
    
    
    @staticmethod
    def readSiteNetworkAccesses(siteNetworkAccesses : Dict) -> Dict:
        siteNetworkAccessArray : List = siteNetworkAccesses.get("site-network-access")
        res : Dict = {} 
        arrayDeIDs : List = []       
        for siteNetworkAccess in siteNetworkAccessArray:
            variableIncial : str = siteNetworkAccess.get("site-network-access-id")
            variableIncial = variableIncial.replace(" ", "-")#ESTO LO HAGO PARA QUE NO HAYA PROBLEMAS CON LOS ESPACIOS A LA HORA DE LEER EL JSON
            arrayDeIDs.append(variableIncial)
        res.update({"site-network-accesses-IDs":arrayDeIDs})
        dictSiteNetworkAccesses : Dict = {}
        
        for site in arrayDeIDs:
            dictDeCadaID : Dict = {}
            
            # try:
            dictDeCadaID.update({"site-network-access-type":siteNetworkAccess.get("site-network-access-type")})
            dictDeCadaID.update({"site-role":siteNetworkAccess.get("vpn-attachment", {}).get("site-role")}) 
            #dictDeCadaID.update({"location-flavor" : FunctionDefinition.readLocationFlavor(siteNetworkAccess.get("location-flavor"))}) DE MOMENTO DA ERROR PORQUE ESE CAMPO NO EXISTE
            #dictDeCadaID.update({"access-diversity" : FunctionDefinition.readAccessDiversity(siteNetworkAccess.get("access-diversity"))}) CAMPO NO EXISTE AUN
            #dictDeCadaID.update({"bearer" : FunctionDefinition.readBearer(siteNetworkAccess.get("bearer"))}) CAMPO NO EXISTE AUN
            dictDeCadaID.update({"ip-connection":FunctionDefinition.readIpConnection(siteNetworkAccess.get("ip-connection"))})
            dictDeCadaID.update({"routing-protocols":FunctionDefinition.readRoutingProtocols(siteNetworkAccess.get("routing-protocols"))})
            dictDeCadaID.update({"service":FunctionDefinition.readService(siteNetworkAccess.get("service"))})
            # except NoJSONFieldException as e:
            #     print(e)
                
            dictSiteNetworkAccesses.update({site:dictDeCadaID})
        
        res.update({"site-network-accesses-Dict": dictSiteNetworkAccesses})       
        return res
    
    @staticmethod
    def readLocationFlavor(locationFlavor : Dict) -> Dict:
        res : Dict = {}
        
        if "location" in locationFlavor:
            res.update({"location-reference":locationFlavor.get("location", {}).get("location-reference")})
        elif "device" in locationFlavor:
            res.update({"device-reference":locationFlavor.get("device", {}).get("device-reference")})
        else:
            raise NoJSONFieldException("location-flavor")
            
        return res
        
    @staticmethod
    def readAccessDiversity(accessDiversity : Dict) -> Dict:
        res : Dict = {}
        arrayGroupsIni : List = accessDiversity.get("groups",{}).get("group") #obtengo el array de objetos
        arrayGroupsIDs : List = []
        for groupId in arrayGroupsIni:
            arrayGroupsIDs.append(groupId.get("group-id"))             
        res.update({"groups":arrayGroupsIDs})
        
        arrayConstraintsIni : List = accessDiversity.get("constraints", {}).get("constraint")
        arrayConstraintsFin : List = []
        for constraint in arrayConstraintsIni: #recordar que constraint es un objeto dentro del array arrayConstraintsIni
            dictConstraint : Dict = {}
            dictConstraint.update({"constraint-type" : constraint.get("constraint-type")})
            dictTargetFlavor : Dict = constraint.get("target", {}).get("target-flavor")
            
            if "id" in dictTargetFlavor:
                dictConstraint.update({"target-flavor": dictTargetFlavor.get("id")})
            elif "all-accesses" in dictTargetFlavor:
                dictConstraint.update({"target-flavor": dictTargetFlavor.get("all-accesses")})
            elif "all-groups" in dictTargetFlavor:
                dictConstraint.update({"target-flavor": dictTargetFlavor.get("all-groups")}) 
            else:
                raise NoJSONFieldException("read-access-divesity>constraints>constraint>target>target-flavor")               
            arrayConstraintsFin.append(dictConstraint)
            
        res.update({"constraints":arrayConstraintsFin})       
        return res

    @staticmethod
    def readBearer(bearer : Dict) -> Dict:
        res : Dict = {}
        
        res.update({"always-on":bearer.get("always-on")})
        res.update({"bearer-reference":bearer.get("bearer-reference")})
        res.update({"requested-type":bearer.get("requested-type",{}).get("requested-type")})
        res.update({"strict":bearer.get("requested-type",{}).get("strict")})       
        
        return res
    
    @staticmethod
    def readIpConnection(ipConnection : Dict) -> Dict: #IPV4 E IPV6 TIENEN LOS MISMOS CAMPOS
        res : Dict = {}
        if ipConnection.get("ipv4") is not None:
            res.update({"ipv4": FunctionDefinition.readIpv4_Ipv6(ipConnection.get("ipv4"))})
        elif ipConnection.get("ipv6") is not None:
            res.update({"ipv6":FunctionDefinition.readIpv4_Ipv6(ipConnection.get("ipv6"))}) 
        else:
            raise NoJSONFieldException("ip-connection>ipv4/6")
            
        bfd : Dict = ipConnection.get("oam",{}).get("bfd")
        if bfd is not None:
            oamFinal : Dict = {}
            oamFinal.update({"enabled":bfd.get("enabled")})
            holdtime : Dict = bfd.get("holdtime")
            holdtimeFinal : Dict = {}
            if "fixed" in holdtime:
                holdtimeFinal.update({"fixed":holdtime.get("fixed")})
            elif "profile" in holdtime:
                holdtimeFinal.update({"profile":holdtime.get("profile")})
            else:
                raise NoJSONFieldException("oam>bfd>holdtime")
            
            oamFinal.update({"holdtime":holdtimeFinal})
            res.update({"oam":oamFinal})
        
        return res
    
    @staticmethod
    def readIpv4_Ipv6(ipv : Dict) -> Dict :
        res : Dict = {}
        
        res.update({"address-allocation-type":ipv.get("address-allocation-type")})
        
        providerDHCP : Dict = ipv.get("provider-dhcp") #None si no existe ese campo, puede mejorarse eficiencia
        if providerDHCP is not None:            
            providerDHCPFinal : Dict = {}        
            providerDHCPFinal.update({"provider-dhcp":providerDHCP.get("provider-address")})
            providerDHCPFinal.update({"prefix-length":providerDHCP.get("prefix-lenght")})
            addressAsign : Dict = providerDHCP.get("address-asign")
            addressAsignFinal : Dict = {}
            if "number" in addressAsign:
                addressAsignFinal.update({"number":addressAsign.get("number")})
            elif "explicit" in addressAsign:
                addressAsignFinal.update({"explicit":addressAsign.get("explicit")})
            else:
                raise NoJSONFieldException("ipv4/6>address-asign")
            
            providerDHCPFinal.update({"address-asign":addressAsignFinal})         
            res.update({"provider-dhcp":providerDHCPFinal})
        
        
        dhcpRelay : Dict = ipv.get("dhcp-relay")
        if dhcpRelay is not None:
            dhcpRelayFinal : Dict = {}
            dhcpRelayFinal.update({"provider-address":dhcpRelay.get("provider-address")})
            dhcpRelayFinal.update({"prefix-lenght":dhcpRelay.get("prefix-lenght")})
            dhcpRelayFinal.update({"server-ip-address":dhcpRelay.get("customer-dhcp-servers",{}).get("server-ip-address")})
            res.update({"dhcp-relay":dhcpRelayFinal})
        
        addresses : Dict = ipv.get("addresses")
        if addresses is not None:
            addressesFinal : Dict = {}
            addressesFinal.update({"provider-address":addresses.get("provider-address")})
            addressesFinal.update({"customer-address":addresses.get("customer-address")})
            addressesFinal.update({"prefixe-length":addresses.get("prefixe-length")})
            res.update({"addresses":addressesFinal})
        
        return res
    
    # @staticmethod
    # def readSecurity(security : Dict) -> Dict:
    #     res : Dict = {}
        
    #     res.update({"authentication" : security.get("authentication")})
    #     encryption : Dict = security.get("encryption")
    #     encryptionFinal : Dict = {}
        
        
    #     return res
    
    #readService todavía me da error
    @staticmethod
    def readService(service : Dict) -> Dict :
        res : Dict = {}
        #try:                
        
        res.update({"svc-input-bandwidth":service.get("svc-input-bandwidth")})
        res.update({"svc-output-bandwidth":service.get("svc-output-bandwidth")})
        res.update({"svc-mtu":service.get("svc-mtu")})
        qos : Dict = service.get("qos")
        ruleArrayIni : List = qos.get("qos-classification-policy")
        ruleArrayFin : List = []
        for elementRule in ruleArrayIni : 
            elementRuleFin : Dict = {}
            elementRuleFin.update({"id":elementRule.get("id")})
            matchTypeIni : Dict = elementRule.get("match-type")
            #CUIDADO, FALTAN MUCHOS CAMPOS DENTRO DE match-flow Y match-application POR PROGRAMAR
            matchTypeFin : Dict = {}
            if "match-flow" in matchTypeIni:
                matchTypeFin.update({"match-flow":matchTypeIni})
            elif "match-application" in matchTypeIni:
                matchTypeFin.update({"match-application" : matchTypeIni})
            #else:
                #raise NoJSONFieldException("qos>qos-classification-policy>match-type")
                
            
            elementRuleFin.update({"match-type":matchTypeFin})
            elementRuleFin.update({"target-class-id":elementRule.get("target-class-id")})
            ruleArrayFin.append(elementRuleFin)
        res.update({"qos-classification-policy":ruleArrayFin})
        
        #Clase que lee el campo custom varias lineas mas adelante
        
        def readCustom (custom : Dict) -> Dict: #custom es un Dict que es: {classes: [class-id: *, ..., latency: {flavor:{lowest:*///boundary:*}}],[...],[...]}
            res : Dict = {}
                        
            arrayClassesEntrada : List = custom.get("class") #un array con elementos indeterminados
            arrayClassesFinal : List = []
            
            for elementEntrada in arrayClassesEntrada:
                elementFinal : Dict = {}
                elementFinal.update({"class-id":elementEntrada.get("class-id")})
                elementFinal.update({"direction":elementEntrada.get("direction")})
                elementFinal.update({"rate-limit":elementEntrada.get("rate-limit")})
                #Campo latency--->
                    #latency: {
                    #     flavor:{
                    #         lowest:*
                    #         o quizás
                    #         boundary:*
                    #     }
                    # }
                                    
                latency : Dict = elementEntrada.get("latency")
                jitter : Dict = elementEntrada.get("jitter")
                if latency is not None:
                    if "use-lowest-latency" in latency:
                        elementFinal.update({"latency-flavor-lowest":latency.get("use-lowest-latency")})
                    elif "latency-boundary" in latency:
                        elementFinal.update({"latency-flavor-boundary":latency.get("latency-boundary")})
                elif jitter is not None:
                    if "use-lowest-jitter" in jitter:
                        elementFinal.update({"jitter-flavor-lowest":jitter.get("use-lowest-jiter")})
                    elif "latency-boundary" in jitter:
                        elementFinal.update({"jitter-flavor-boundary":jitter.get("latency-boundary")})
                    else:
                        print("Debe haber un campo lowest o boundary dentro de jitter>flavor")
                else:
                    print("FALTAN CAMPOS EN CUSTOM")
                
                bandwidthEntrada : Dict = elementEntrada.get("bandwidth")
                bandwidthSalida : Dict = {}
                bandwidthSalida.update({"guaranteed-bw-percent":bandwidthEntrada.get("guaranteed-bw-percent")})
                bandwidthSalida.update({"end-to-end":bandwidthEntrada.get("end-to-end")})
                elementFinal.update({"bandwidth":bandwidthSalida})
                
                arrayClassesFinal.append(elementFinal)
                
            res.update({"latency-flavor":arrayClassesFinal})
            return res
            #END READCUSTOM
        
        qosProfileEntrada : Dict = qos.get("qos-profile")
        qosProfileFin : Dict = {}
                
        if "profile" in qosProfileEntrada:
            qosProfileFin.update({"standard-profile":qosProfileEntrada.get("standard")})
        elif "classes" in qosProfileEntrada:
            qosProfileFin.update({"custom-classes":readCustom(qosProfileEntrada.get("classes"))})
        else:
            print("falta campo en qos>qos-profile")
            #raise NoJSONFieldException("qos>qos-profile")
                    
        res.update({"qos-profile":qosProfileFin})
        res.update({"carrierscarrier":qos.get("carrierscarrier")})
        multicastEntrada : Dict = qos.get("multicast")
        multicastSalida : Dict = {}
        multicastSalida.update({"multicast-site-type":multicastEntrada.get("multicast-site-type")})
        multicastAddressSalida : Dict = {}
        multicastAddressSalida.update({"ipv4":multicastEntrada.get("multicast-address-family", {}).get("ipv4")})
        multicastAddressSalida.update({"ipv6":multicastEntrada.get("multicast-address-family", {}).get("ipv6")})
        multicastSalida.update({"multicast-address-family":multicastAddressSalida})
        multicastSalida.update({"protocol-type":multicastEntrada.get("protocol-type")})
        
        res.update({"multicast":multicastSalida})
            
        # except KeyError:
        #     print("falta alguna clave en service")
        # except NoJSONFieldException as e:
        #     print(e)        
        # finally:  
        return res
    
    
 No newline at end of file
+286 −0

File added.

Preview size limit exceeded, changes collapsed.

+60 −11
Original line number Diff line number Diff line
@@ -13,7 +13,9 @@
# limitations under the License.

import logging
from typing import Dict
import json

from typing import Dict, List
from flask import request
from flask.json import jsonify
from flask_restful import Resource
@@ -23,6 +25,11 @@ from nbi.service.rest_server.nbi_plugins.tools.Authentication import HTTP_AUTH
from .Handlers import process_site, process_vpn_service
from .YangValidator import YangValidator

# from FunctionDefinition import FunctionDefinition
# from FunctionDefinitionNetwork import FunctionDefinitionNetwork
# import FunctionDefinition
# import FunctionDefinitionNetwork

LOGGER = logging.getLogger(__name__)

class L3VPN_Services(Resource):
@@ -32,22 +39,64 @@ class L3VPN_Services(Resource):

    @HTTP_AUTH.login_required
    def post(self):
        
        jsonAValidar = open("/home/ubuntu/Documents/json1.json", "r")
        jsonAValidarData : str = jsonAValidar.read()
        jsonAValidarDict : Dict = json.loads(jsonAValidarData) 
        output_file_path = "/home/ubuntu/Documents/EstructuraIntermedia.json"
        
        if not request.is_json: raise UnsupportedMediaType('JSON payload is required')
        request_data : Dict = request.json
        LOGGER.debug('Request: {:s}'.format(str(request_data)))
        # LOGGER.debug('Request: {:s}'.format(str(request_data)))
        LOGGER.debug('Request: {:s}'.format(str(jsonAValidarDict)))

        yang_validator = YangValidator('ietf-l3vpn-svc')
        request_data = yang_validator.parse_to_dict(request_data)
        request_data = yang_validator.parse_to_dict(jsonAValidarDict)
        yang_validator.destroy()

        errors = []
        #se supone que a partir de aqui ya ha validado
        
        jParse = jsonAValidarDict

        ietf_l3vpn : Dict  = jParse['ietf-l3vpn-svc:l3vpn-svc']

        vpn_services : Dict = ietf_l3vpn['vpn-services']
        
        bd : Dict[any, any] = {} #Base datos de variables del JSON

        bd = FunctionDefinition.writeVpnServices( FunctionDefinition.readVpnServices(vpn_services) , bd)
        
        sites : List = ietf_l3vpn['sites']['site'] #En sites se almacena el valor de la clave 'sites'.
       
        #### FUNCIONA SOLO CON 1 VPN ####
        lista_VPNs = bd["VPN_IDs"]
        bd["VPNs_Dict"][lista_VPNs[0]].update(FunctionDefinition.readSiteArray(sites, bd))
        ####  ---------------------  ####

        with open(output_file_path, 'w') as json_file:
            json.dump(bd, json_file, indent=3)
            
        jsonL3VPN_net : Dict = FunctionDefinitionNetwork.write()
        # print(json.dumps(jsonL3VPN_net, indent = 3))








        ######

        # errors = []

        # for vpn_service in request_data['l3vpn-svc']['vpn-services']['vpn-service']:
        #     process_vpn_service(vpn_service, errors)

        for vpn_service in request_data['l3vpn-svc']['vpn-services']['vpn-service']:
            process_vpn_service(vpn_service, errors)
        # for site in request_data['l3vpn-svc']['sites']['site']:
        #     process_site(site, errors)

        for site in request_data['l3vpn-svc']['sites']['site']:
            process_site(site, errors)
        # response = jsonify(errors)
        # response.status_code = HTTP_CREATED if len(errors) == 0 else HTTP_SERVERERROR
        # return response
    
 No newline at end of file
        response = jsonify(errors)
        response.status_code = HTTP_CREATED if len(errors) == 0 else HTTP_SERVERERROR
        return response
+6 −0
Original line number Diff line number Diff line
from src.nbi.service.rest_server.nbi_plugins.ietf_l3vpn import L3VPN_Services
#from src.nbi.service.rest_server.nbi_plugins.ietf_l3vpn.L3VPN_Services import L3VPN_Services


service : L3VPN_Services = L3VPN_Services()
service.post()
+25 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading