Commit 55c6d4ea authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Tests - Tools - SIMAP Server/Client

- Code cleanup
parent 496149e2
Loading
Loading
Loading
Loading
+0 −203
Original line number Diff line number Diff line
{
  "ietf-network:networks": {
    "network": [
      {
        "network-id": "te",
        "node": [
          {"node-id": "ONT1", "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "500"}]},
          {"node-id": "ONT2", "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "500"}]},
          {"node-id": "OLT",  "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "201"}, {"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "PE1",  "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "P1",   "ietf-network-topology:termination-point": [{"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "P2",   "ietf-network-topology:termination-point": [{"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "PE2",  "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "OA",   "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "OTN1", "ietf-network-topology:termination-point": [{"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "OTN2", "ietf-network-topology:termination-point": [{"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "OE",   "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "500"}, {"tp-id": "501"}]},
          {"node-id": "POP1", "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "201"}, {"tp-id": "500"}]},
          {"node-id": "POP2", "ietf-network-topology:termination-point": [{"tp-id": "200"}, {"tp-id": "201"}, {"tp-id": "500"}]}
        ],
        "ietf-network-topology:link": [
          {"link-id": "L1",  "source": {"source-node": "ONT1", "source-tp": "500"}, "destination": {"dest-node": "OLT",  "dest-tp": "200"}},
          {"link-id": "L2",  "source": {"source-node": "ONT2", "source-tp": "500"}, "destination": {"dest-node": "OLT",  "dest-tp": "201"}},
          {"link-id": "L5",  "source": {"source-node": "PE1",  "source-tp": "500"}, "destination": {"dest-node": "P1",   "dest-tp": "500"}},
          {"link-id": "L6",  "source": {"source-node": "PE1",  "source-tp": "501"}, "destination": {"dest-node": "P2",   "dest-tp": "500"}},
          {"link-id": "L9",  "source": {"source-node": "P1",   "source-tp": "501"}, "destination": {"dest-node": "PE2",  "dest-tp": "500"}},
          {"link-id": "L10", "source": {"source-node": "P2",   "source-tp": "501"}, "destination": {"dest-node": "PE2",  "dest-tp": "501"}},
          {"link-id": "L7",  "source": {"source-node": "OA",   "source-tp": "500"}, "destination": {"dest-node": "OTN1", "dest-tp": "500"}},
          {"link-id": "L8",  "source": {"source-node": "OA",   "source-tp": "501"}, "destination": {"dest-node": "OTN2", "dest-tp": "500"}},
          {"link-id": "L11", "source": {"source-node": "OTN1", "source-tp": "501"}, "destination": {"dest-node": "OE",   "dest-tp": "500"}},
          {"link-id": "L12", "source": {"source-node": "OTN2", "source-tp": "501"}, "destination": {"dest-node": "OE",   "dest-tp": "501"}},
          {"link-id": "L3",  "source": {"source-node": "OLT",  "source-tp": "500"}, "destination": {"dest-node": "PE1",  "dest-tp": "200"}},
          {"link-id": "L4",  "source": {"source-node": "OLT",  "source-tp": "501"}, "destination": {"dest-node": "OA",   "dest-tp": "200"}},
          {"link-id": "L13", "source": {"source-node": "PE2",  "source-tp": "200"}, "destination": {"dest-node": "POP1", "dest-tp": "500"}},
          {"link-id": "L14", "source": {"source-node": "OE",   "source-tp": "200"}, "destination": {"dest-node": "POP2", "dest-tp": "500"}}
        ]
      },
      {
        "network-id": "simap-trans",
        "supporting-network": [{"network-ref": "te"}],
        "node": [
          {"node-id": "site1", "supporting-node": [{"network-ref": "te", "node-ref": "PE1"}], "ietf-network-topology:termination-point": [
            {"tp-id": "200", "supporting-termination-point": [{"network-ref": "te", "node-ref": "PE1", "tp-ref": "200"}]},
            {"tp-id": "500", "supporting-termination-point": [{"network-ref": "te", "node-ref": "PE1", "tp-ref": "500"}]},
            {"tp-id": "501", "supporting-termination-point": [{"network-ref": "te", "node-ref": "PE1", "tp-ref": "501"}]}
          ]},
          {"node-id": "site2", "supporting-node": [{"network-ref": "te", "node-ref": "PE2"}], "ietf-network-topology:termination-point": [
            {"tp-id": "200", "supporting-termination-point": [{"network-ref": "te", "node-ref": "PE2", "tp-ref": "200"}]},
            {"tp-id": "500", "supporting-termination-point": [{"network-ref": "te", "node-ref": "PE2", "tp-ref": "500"}]},
            {"tp-id": "501", "supporting-termination-point": [{"network-ref": "te", "node-ref": "PE2", "tp-ref": "501"}]}
          ]}
        ],
        "ietf-network-topology:link": [
          {"link-id": "Trans-L1",
            "source": {"source-node": "site1", "source-tp": "500"},
            "destination": {"dest-node": "site2", "dest-tp": "500"},
            "supporting-link": [
              {"network-ref": "te", "link-ref": "L5"},
              {"network-ref": "te", "link-ref": "L9"}
            ]
          }
        ]
      },
      {
        "network-id": "simap-aggnet",
        "supporting-network": [
          {
            "network-ref": "te"
          },
          {
            "network-ref": "simap-trans"
          }
        ],
        "node": [
          {"node-id": "sdp1", "supporting-node": [
              {
                "network-ref": "te",
                "node-ref": "OLT"
              }
            ],
            "ietf-network-topology:termination-point": [
              {"tp-id": "200",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "OLT", "tp-ref": "200"}]},
              {"tp-id": "201",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "OLT", "tp-ref": "201"}]},
              {"tp-id": "500",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "OLT", "tp-ref": "500"}]},
              {"tp-id": "501",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "OLT", "tp-ref": "501"}]}
            ]
          },
          {"node-id": "sdp2", "supporting-node": [
              {
                "network-ref": "te",
                "node-ref": "POP1"
              }
            ],
            "ietf-network-topology:termination-point": [
              {"tp-id": "200",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "POP1", "tp-ref": "200"}]},
              {"tp-id": "201",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "POP1", "tp-ref": "201"}]},
              {"tp-id": "500",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "POP1", "tp-ref": "500"}]}
            ]
          }
        ],
        "ietf-network-topology:link": [
          {
            "link-id": "AggNet-L1",
            "source": {
              "source-node": "sdp1",
              "source-tp": "500"
            },
            "destination": {
              "dest-node": "sdp2",
              "dest-tp": "500"
            },
            "supporting-link": [
              {
                "network-ref": "te",
                "link-ref": "L3"
              },
              {
                "network-ref": "simap-trans",
                "link-ref": "Trans-L1"
              },
              {
                "network-ref": "te",
                "link-ref": "L13"
              }
            ]
          }
        ]
      },
      {
        "network-id": "simap-e2e",
        "supporting-network": [
          {
            "network-ref": "te"
          },
          {
            "network-ref": "simap-trans"
          }
        ],
        "node": [
          {"node-id": "sdp1", "supporting-node": [
              {
                "network-ref": "te",
                "node-ref": "ONT1"
              }
            ],
            "ietf-network-topology:termination-point": [
              {"tp-id": "200",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "ONT1", "tp-ref": "200"}]},
              {"tp-id": "500",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "ONT1", "tp-ref": "500"}]}
            ]
          },
          {"node-id": "sdp2", "supporting-node": [
              {
                "network-ref": "te",
                "node-ref": "POP1"
              }
            ],
            "ietf-network-topology:termination-point": [
              {"tp-id": "200",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "POP1", "tp-ref": "200"}]},
              {"tp-id": "201",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "POP1", "tp-ref": "201"}]},
              {"tp-id": "500",                "supporting-termination-point": [
                  {"network-ref": "te", "node-ref": "POP1", "tp-ref": "500"}]}
            ]
          }
        ],
        "ietf-network-topology:link": [
          {
            "link-id": "E2E-L1",
            "source": {
              "source-node": "sdp1",
              "source-tp": "500"
            },
            "destination": {
              "dest-node": "sdp2",
              "dest-tp": "500"
            },
            "supporting-link": [
              {
                "network-ref": "te",
                "link-ref": "L1"
              },
              {
                "network-ref": "simap-aggnet",
                "link-ref": "AggNet-L1"
              }
            ]
          }
        ]
      }
    ]
  },
  "ietf-yang-schema-mount:schema-mounts": {}
}
+0 −261
Original line number Diff line number Diff line
import json, libyang, logging
from typing import Any, Dict
from flask import Response, abort, jsonify, request
from flask_restful import Resource
from simap_server.YangHandler import YangHandler
from ..resources.HttpStatusCodesEnum import HttpStatusCodesEnum

LOGGER = logging.getLogger(__name__)

def get_key_path(key_str):
    ''' Support composite keys separated by commas '''
    return tuple(key_str.split(',')) if ',' in key_str else key_str

class RestConfDispatch(Resource):
    def __init__(self, datastore : Any, yang_handler : YangHandler) -> None:
        super().__init__()
        self._datastore = datastore
        self._yang_handler = yang_handler

    def get(self, subpath : str = '') -> Response:
        data = self._yang_handler.get(subpath)
        if data is None: abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
        response = jsonify(data)
        response.status_code = HttpStatusCodesEnum.SUCCESS_OK
        return response

#        LOGGER.info('[get] subpath={:s}'.format(str(subpath)))
#        if len(subpath) == 0: abort(HttpStatusCodesEnum.SVR_ERR_NOT_IMPLEMENTED)
#
#        subpath_parts = subpath.split('/')
#        root_node_path = '/{:s}'.format(str(subpath_parts[0]))
#        root_node : libyang.DContainer = self._yang_handler.get_data_path(root_node_path)
#        LOGGER.info('[get] root_node={:s}'.format(str(root_node.print_mem('json'))))
#
#        yang_obj = root_node.find_path(subpath)
#        if yang_obj is None:
#            LOGGER.exception('Not Found')
#            abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
#
#        json_data = json.loads(str(yang_obj.print_mem('json')))
#        LOGGER.info('json_data={:s}'.format(str(json_data)))
#        #yang_obj.unlink()
#        #yang_obj.free()
#
#        response = jsonify(json_data)
#        response.status_code = HttpStatusCodesEnum.SUCCESS_OK
#        return response


#        #str_data = yang_if.print_mem('json')
#        #json_data = json.loads(str_data)
#        #json_data = json_data['openconfig-interfaces:interface'][0]
#        #str_data = json.dumps(json_data, indent=4)
#        #LOGGER.info('Resulting Request (after unlink): {:s}'.format(str_data))
#
#        module_paths = self._yang_handler.get_module_paths()
#
#        
#        LOGGER.info('parts={:s}'.format(str(parts)))
#
#        # Traverse to the last element
#        current : Dict[str, Any] = self._datastore
#        for part in parts:
#            element_type = module_paths.get(part)
#            #if isinstance(element_type, )
#            current : Dict[str, Any] = current.setdefault(part, {})
#
#        LOGGER.info('datastore={:s}'.format(str(self._datastore)))
#
#        leaf = parts[-2]
#        leaf_or_key = parts[-1] # key for list or value for leaf-list
#        key = get_key_path(leaf_or_key)
#
#        if key is None:
#            if leaf not in current:
#                LOGGER.exception('Not Found')
#                abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
#            item = current[leaf]
#        else:
#            item = current.get(leaf, {}).get(key)
#            if item is None: 
#                LOGGER.exception('Not Found')
#                abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
#
#        response = jsonify(item)
#        response.status_code = HttpStatusCodesEnum.SUCCESS_OK
#        return response

    def post(self, subpath : str) -> Response:
        try:
            payload = json.dumps(request.get_json(force=True))
        except Exception:
            LOGGER.exception('Invalid JSON')
            abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST)

        created = self._yang_handler.create(subpath, payload)
        if not created: abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST)
        response = jsonify({'status': 'created'})
        response.status_code = HttpStatusCodesEnum.SUCCESS_CREATED
        return response
           

#        parts = subpath.strip('/').split('/')
#
#        # Traverse to the last container or list
#        current = self._datastore
#        for part in parts[:-2]:
#            current = current.setdefault(part, {})
#
#        leaf = parts[-2]
#        leaf_or_key = parts[-1] # key for list or value for leaf-list
#        key = get_key_path(leaf_or_key)
#        if key is None:
#            LOGGER.exception('Missing key')
#            abort(HttpStatusCodesEnum.CLI_ERR_CONFLICT, description='Missing key')
#        if key in current.get(leaf, {}):
#            LOGGER.exception('Key already exists')
#            abort(HttpStatusCodesEnum.CLI_ERR_CONFLICT, description='Key already exists')
#
#
#        try:
#            json_data = json.dumps({leaf: [payload]} if isinstance(key, tuple) else {leaf: payload})
#            self._yang_handler._yang_context.parse_data_mem(json_data, format='json', config=True, trusted=True)
#
#            if key is None:
#                if leaf not in current:
#                    current[leaf] = payload
#            else:
#                current.setdefault(leaf, {})
#                if key not in current[leaf]:
#                    current[leaf][key] = payload
#
#            response = jsonify({'status': 'validated'})
#            response.status_code = HttpStatusCodesEnum.SUCCESS_CREATED
#            return response
#        except Exception as e:
#            LOGGER.exception('YANG validation failed')
#            abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST, description=str(e))

    def put(self, subpath : str) -> Response:
        parts = subpath.strip('/').split('/')

        # Traverse to the last container or list
        current = self._datastore
        for part in parts[:-2]:
            current = current.setdefault(part, {})

        leaf = parts[-2]
        leaf_or_key = parts[-1] # key for list or value for leaf-list
        key = get_key_path(leaf_or_key)

        try:
            payload = request.get_json(force=True)
        except Exception:
            LOGGER.exception('Invalid JSON')
            abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST, description='Invalid JSON')

        try:
            json_data = json.dumps({leaf: [payload]} if isinstance(key, tuple) else {leaf: payload})
            self._yang_handler._yang_context.parse_data_mem(json_data, format='json', config=True, trusted=True)

            if key is None:
                current[leaf] = payload
            else:
                current.setdefault(leaf, {})
                current[leaf][key] = payload

            response = jsonify({'status': 'validated'})
            response.status_code = HttpStatusCodesEnum.SUCCESS_CREATED
            return response
        except Exception as e:
            LOGGER.exception('YANG validation failed')
            abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST, description=str(e))

    def patch(self, subpath : str) -> Response:
        parts = subpath.strip('/').split('/')

        # Traverse to the last container or list
        current = self._datastore
        for part in parts[:-2]:
            current = current.setdefault(part, {})

        leaf = parts[-2]
        leaf_or_key = parts[-1] # key for list or value for leaf-list
        key = get_key_path(leaf_or_key)

        try:
            payload = request.get_json(force=True)
        except Exception:
            LOGGER.exception('Invalid JSON')
            abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST, description='Invalid JSON')

        try:
            json_data = json.dumps({leaf: [payload]} if isinstance(key, tuple) else {leaf: payload})
            self._yang_handler._yang_context.parse_data_mem(json_data, format='json', config=True, trusted=True)

            if key is None:
                if leaf not in current:
                    LOGGER.exception('Not Found')
                    abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
                current[leaf].update(payload)
            else:
                if key not in current.get(leaf, {}):
                    LOGGER.exception('Not Found')
                    abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
                current[leaf][key].update(payload)

            response = jsonify({'status': 'patched'})
            response.status_code = HttpStatusCodesEnum.SUCCESS_OK
            return response
        except Exception as e:
            LOGGER.exception('YANG validation failed')
            abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST, description=str(e))

    def delete(self, subpath : str) -> Response:
        parts = subpath.strip('/').split('/')

        # Traverse to the last container or list
        current = self._datastore
        for part in parts[:-2]:
            current = current.setdefault(part, {})

        leaf = parts[-2]
        leaf_or_key = parts[-1] # key for list or value for leaf-list
        key = None
        leaf_list_item = None

        # Determine if it's a leaf-list or a list key
        if leaf in current and isinstance(current[leaf], list):
            # It's a leaf-list
            leaf_list_item = leaf_or_key
        else:
            key = get_key_path(leaf_or_key)

        if leaf not in current:
            LOGGER.exception('Not Found')
            abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')

        if key is not None:
            if key in current[leaf]:
                del current[leaf][key]
                response = jsonify({})
                response.status_code = HttpStatusCodesEnum.SUCCESS_NO_CONTENT
                return response
            else:
                LOGGER.exception('Not Found')
                abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
        elif leaf_list_item is not None:
            try:
                current[leaf].remove(leaf_list_item)
                response = jsonify({})
                response.status_code = HttpStatusCodesEnum.SUCCESS_NO_CONTENT
                return response
            except ValueError:
                LOGGER.exception('Not Found')
                abort(HttpStatusCodesEnum.CLI_ERR_NOT_FOUND, description='Not Found')
        else:
            del current[leaf]
            response = jsonify({})
            response.status_code = HttpStatusCodesEnum.SUCCESS_NO_CONTENT
            return response
+0 −59
Original line number Diff line number Diff line
import json, libyang, logging

YANG_SEARCH_PATH = './yang'
YANG_MODULES = [
    'ietf-inet-types',
    'simap-telemetry',
    'ietf-network-topology',
    'ietf-network',
]

logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger(__name__)

def main() -> None:
    yang_context = libyang.Context(YANG_SEARCH_PATH)
    for yang_module_name in YANG_MODULES:
        LOGGER.info('Loading module: {:s}'.format(str(yang_module_name)))
        yang_module = yang_context.load_module(yang_module_name)
        yang_module.feature_enable_all()

    startup = {}
    datastore = yang_context.parse_data_mem(json.dumps(startup), fmt='json')

    path = '/ietf-network:networks'
    result = datastore.find_path(path)
    json_result = result.print_mem(fmt='json', with_siblings=True, pretty=True, keep_empty_containers=True, include_implicit_defaults=True)
    LOGGER.info('{:s} => {:s}'.format(path, str(json_result)))

    path    = '/ietf-network:networks'
    payload = json.dumps({"networks":{"network":[]}})
    yang_context.create_data_path(
        path, parent=datastore, value=json.dumps(payload), update=False
    )

    path = '/ietf-network:networks'
    result = datastore.find_path(path)
    json_result = result.print_mem(fmt='json', with_siblings=True, pretty=True, keep_empty_containers=True, include_implicit_defaults=True)
    LOGGER.info('{:s} => {:s}'.format(path, str(json_result)))

    path    = '/ietf-network:networks/network[network-id="simap-1"]'
    payload = json.dumps({"network":[{"network-id":"simap-1"}]})
    yang_context.create_data_path(
        path, parent=datastore, value=json.dumps(payload), update=False
    )

    path = '/ietf-network:networks'
    result = datastore.find_path(path)
    json_result = result.print_mem(fmt='json', with_siblings=True, pretty=True, keep_empty_containers=True, include_implicit_defaults=True)
    LOGGER.info('{:s} => {:s}'.format(path, str(json_result)))

    path = '/ietf-network:networks/network[network-id="simap-1"]'
    result = datastore.find_path(path)
    json_result = result.print_mem(fmt='json', with_siblings=True, pretty=True, keep_empty_containers=True, include_implicit_defaults=True)
    LOGGER.info('{:s} => {:s}'.format(path, str(json_result)))

    yang_context.destroy()

if __name__ == '__main__':
    main()
+0 −162

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −23
Original line number Diff line number Diff line
import re

path = '/ietf-network:networks/network[network-id="simap1"]/node[node-id="n1"]'
payload = {'ietf-network:node': {'node-id': 'n1', 'ietf-network-topology:termination-point': [{'tp-id': '201'}]}}

if not path.startswith('/'):
    raise ValueError('Path must start with "/"')

for elem in path.strip('/').split('/'):
    match = re.match(r"(?P<name>[^\[]+)(?P<predicates>(\[[^\]]+\])*)", elem)
    if not match:
        raise ValueError(f'Invalid path segment: {elem}')

    tag = match.group('name')
    predicates_raw = match.group('predicates')

    if 

    predicates = dict()
    for pred in re.findall(r"\[([^\]=]+)='([^']+)'\]", predicates_raw):
        predicates[pred[0]] = pred[1]

    if len(predicates) > 0:
Loading