Commit 5892ca30 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch 'feat/62-tid-add-support-to-nbi-to-export-the-device-inventory-items-2' into 'develop'

Resolve "(TID) Add support to NBI to export the device inventory items"

See merge request !237
parents fc520898 5cbd46dc
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -76,6 +76,10 @@ def parse(xml_data : ET.Element) -> List[Tuple[str, Dict[str, Any]]]:
        if not component_location is None:
            add_value_from_tag(inventory['attributes'], 'location', component_location)

        component_id = xml_component.find('ocp:state/ocp:id', namespaces=NAMESPACES)
        if not component_id is None:
            add_value_from_tag(inventory['attributes'], 'id', component_id)
        
        component_type = xml_component.find('ocp:state/ocp:type', namespaces=NAMESPACES)
        if component_type is not None:
            component_type.text = component_type.text.replace('oc-platform-types:','')
@@ -109,7 +113,7 @@ def parse(xml_data : ET.Element) -> List[Tuple[str, Dict[str, Any]]]:

        component_mfg_name = xml_component.find('ocp:state/ocp:mfg-name', namespaces=NAMESPACES)
        if not component_mfg_name is None:
            add_value_from_tag(inventory['attributes'], 'manufacturer-name', component_mfg_name)
            add_value_from_tag(inventory['attributes'], 'mfg-name', component_mfg_name)
        
        component_removable = xml_component.find('ocp:state/ocp:removable', namespaces=NAMESPACES)
        if not component_removable is None:
+3 −0
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@ from common.Constants import ServiceNameEnum
from common.Settings import (
    ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name, get_log_level, get_metrics_port,
    wait_for_environment_variables)

from .NbiService import NbiService
from .rest_server.RestServer import RestServer
from .rest_server.nbi_plugins.etsi_bwm import register_etsi_bwm_api
from .rest_server.nbi_plugins.ietf_hardware import register_ietf_hardware
from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn
from .rest_server.nbi_plugins.ietf_l3vpn import register_ietf_l3vpn
from .rest_server.nbi_plugins.ietf_network import register_ietf_network
@@ -63,6 +65,7 @@ def main():

    rest_server = RestServer()
    register_etsi_bwm_api(rest_server)
    register_ietf_hardware(rest_server)
    register_ietf_l2vpn(rest_server)  # Registering L2VPN entrypoint
    register_ietf_l3vpn(rest_server)  # Registering L3VPN entrypoint
    register_ietf_network(rest_server)
+53 −0
Original line number Diff line number Diff line
# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed 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
from flask import request
from flask.json import jsonify
from flask_restful import Resource
from common.tools.context_queries.Device import get_device
from context.client.ContextClient import ContextClient
from ..tools.Authentication import HTTP_AUTH
from ..tools.HttpStatusCodes import HTTP_OK, HTTP_SERVERERROR
from .YangHandler import YangHandler

LOGGER = logging.getLogger(__name__)

class Hardware(Resource):
    @HTTP_AUTH.login_required
    def get(self, device_uuid : str):
        LOGGER.debug('Device UUID: {:s}'.format(str(device_uuid)))
        LOGGER.debug('Request: {:s}'.format(str(request)))

        try:
            context_client = ContextClient()
            device = get_device(
                context_client, device_uuid, rw_copy=False,
                include_endpoints=False, include_config_rules=False, include_components=True
            )
            if device is None:
                raise Exception('Device({:s}) not found in database'.format(str(device_uuid)))

            yang_handler = YangHandler()
            hardware_reply = yang_handler.compose(device)
            yang_handler.destroy()

            response = jsonify(hardware_reply)
            response.status_code = HTTP_OK
        except Exception as e: # pylint: disable=broad-except
            MSG = 'Something went wrong Retrieving Hardware of Device({:s})'
            LOGGER.exception(MSG.format(str(device_uuid)))
            response = jsonify({'error': str(e)})
            response.status_code = HTTP_SERVERERROR
        return response
 No newline at end of file
+132 −0
Original line number Diff line number Diff line
# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed 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 libyang, os
from common.proto.context_pb2 import Device
from typing import Dict, Optional
import json
import logging
import re
import datetime

LOGGER = logging.getLogger(__name__)
YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang')
YANG_MODULES = [
    'iana-hardware',
    'ietf-hardware'
]

class YangHandler:
    def __init__(self) -> None:
        self._yang_context = libyang.Context(YANG_DIR)
        for yang_module_name in YANG_MODULES:
            LOGGER.info('Loading module: {:s}'.format(str(yang_module_name)))
            self._yang_context.load_module(yang_module_name).feature_enable_all()

    def parse_to_dict(self, message : Dict) -> Dict:
        yang_module = self._yang_context.get_module('ietf-hardware')
        dnode : Optional[libyang.DNode] = yang_module.parse_data_dict(
            message, validate_present=True, validate=True, strict=True
        )
        if dnode is None: raise Exception('Unable to parse Message({:s})'.format(str(message)))
        message = dnode.print_dict()
        dnode.free()
        return message

    
    @staticmethod
    def convert_to_iso_date(date_str: str) -> Optional[str]:
        date_str = date_str.strip('"')
        # Define the regex pattern for ISO 8601 date format
        pattern = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[\+\-]\d{2}:\d{2})"
        # Check if the input date string matches the pattern
        if re.match(pattern, date_str):
            return date_str  # Already in ISO format
        else:
            try:
                # Parse the input date string as a datetime object
                datetime_obj = datetime.datetime.strptime(date_str, "%Y-%m-%d")
                # Convert to ISO format
                iso_date = datetime_obj.isoformat() + "Z"
                return iso_date
            except ValueError:
                return None  # Invalid date format


    def compose(self, device : Device) -> Dict:
        # compose device iterating through the components
 
        hardware = self._yang_context.create_data_path('/ietf-hardware:hardware')
        physical_index = 1
        
        for component in device.components:
            attributes = component.attributes

            component_new = hardware.create_path('component[name="{:s}"]'.format(component.name))
            component_new.create_path('name', component.name)

            #Cambiar las clases especiales, su formato  y añadir isfru 
            component_type = component.type
            if component_type == "TRANSCEIVER" :
                component_type = "module"

            if component_type == "FRU" :
                component_type = "slack"
                component_new.create_path('is-fru', True)
            else :
                component_new.create_path('is-fru', False)
                
            component_type = component_type.replace("_", "-").lower()
            component_type = 'iana-hardware:' + component_type

            component_new.create_path('class', component_type)

            #Añadir resto de atributos en IETF

            physical_index += 1
            component_new.create_path('physical-index', physical_index)

            component_new.create_path('description', attributes["description"])

            component_new.create_path('parent', component.parent)

            if attributes["mfg-date"] != "":
                mfg_date = self.convert_to_iso_date(attributes["mfg-date"])
                LOGGER.info('component[name="{:s}"]'.format(attributes["mfg-date"]))
                component_new.create_path('mfg-date', mfg_date)

            component_new.create_path('hardware-rev', attributes["hardware-rev"])
            component_new.create_path('software-rev', attributes["software-rev"])
            component_new.create_path('firmware-rev', attributes["firmware-version"])
            component_new.create_path('serial-num', attributes["serial-num"])
            component_new.create_path('mfg-name', attributes["mfg-name"])
            if attributes["id"]:
                component_new.create_path('parent-rel-pos', attributes["id"])
            
            component_new.create_path('uri', component.name)
   

            component_new.create_path('uuid', component.component_uuid.uuid)

            contains_child = []
            for component2 in device.components:
                if component.name == component2.parent : 
                 contains_child.append(component2.name)
            
            component_new.create_path('contains-child', contains_child)

        return json.loads(hardware.print_mem('json'))
                                    
    def destroy(self) -> None:
        self._yang_context.destroy()
 No newline at end of file
+21 −0
Original line number Diff line number Diff line
# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed 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.

from nbi.service.rest_server.nbi_plugins.ietf_hardware.Hardware import Hardware
from nbi.service.rest_server.RestServer import RestServer

URL_PREFIX = "/restconf/data/device=<path:device_uuid>/ietf-hardware:hardware"

def register_ietf_hardware(rest_server: RestServer):
    rest_server.add_resource(Hardware, URL_PREFIX)
 No newline at end of file
Loading