From e31a5ec49e1ef3e697378c454aa0994274512d77 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Tue, 20 Jul 2021 12:31:01 -0400 Subject: [PATCH 01/67] Update README.md development guidance. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 97b1825..73a4093 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ 1. Ensure you're able to run the existing code in your own [development environment](#setup). 2. Create a descriptive [GitHub issue](https://github.com/atlanticwave-sdx/datamodel/issues) that outlines what feature you plan to contribute. -3. Clone the repository, and start from the most recent version of the [develop branch](https://github.com/atlanticwave-sdx/datamodel.git). +3. Clone the repository, and start from the most recent version of the [develop branch](https://github.com/atlanticwave-sdx/datamodel/tree/develop). 4. Name your branch using the Github issue number as a prefix along with a brief name that corresponds to your feature (e.g., `8-how-to-contribute`). -5. Once satisfied with your completed and tested work, submit a [pull request](https://github.com/atlanticwave-sdx/datamodel.git) against the **develop** branch so that your code can be reviewed by the team. +5. Once satisfied with your completed and tested work, submit a [pull request](https://github.com/atlanticwave-sdx/datamodel/pulls) against the **develop** branch so that your code can be reviewed by the team. Notes: From 895db457a480668c6370c42794c6fe81a23b22b9 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 23 Jul 2021 17:40:35 -0400 Subject: [PATCH 02/67] topology parsing and validating and unittest --- models/__init__.py | 16 ++++----- parsing/topologyhandler.py | 23 +++++++------ {tests => test}/__init__.py | 0 test/test_topology_handler.py | 29 ++++++++++++++++ test/test_topology_validator.py | 33 +++++++++++++++++++ .../{validator.py => topologyvalidator.py} | 8 ++--- 6 files changed, 87 insertions(+), 22 deletions(-) rename {tests => test}/__init__.py (100%) create mode 100644 test/test_topology_handler.py create mode 100644 test/test_topology_validator.py rename validation/{validator.py => topologyvalidator.py} (97%) diff --git a/models/__init__.py b/models/__init__.py index 3e33fe0..a179732 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -14,11 +14,11 @@ from __future__ import absolute_import # import models into model package -from datamodel.models.connection import Connection -from datamodel.models.link import Link -from datamodel.models.link_measurement_period import LinkMeasurementPeriod -from datamodel.models.location import Location -from datamodel.models.node import Node -from datamodel.models.port import Port -from datamodel.models.service import Service -from datamodel.models.topology import Topology +from .connection import Connection +from .link import Link +from .link_measurement_period import LinkMeasurementPeriod +from .location import Location +from .node import Node +from .port import Port +from .service import Service +from .topology import Topology diff --git a/parsing/topologyhandler.py b/parsing/topologyhandler.py index feda46b..e2a6f8e 100644 --- a/parsing/topologyhandler.py +++ b/parsing/topologyhandler.py @@ -1,5 +1,8 @@ -from models import topology +import json +from models.topology import Topology +from .exceptions import MissingAttributeException +MANIFEST_FILE = None class TopologyHandler(): @@ -7,19 +10,17 @@ class TopologyHandler(): Handler for parsing the topology descritpion in json """ - import json - from datamodel.models.topology import Topology - from .exception import MissingAttributeException - - def __init__(self): + def __init__(self,topology_filename): super().__init__() - self.topology_file=MANIFEST_FILE=None + self.topology_file = topology_filename self.topology = None - def _import_topology(self, topology_filename): - with open(topology_filename) as data_file: + def import_topology(self): + with open(self.topology_file, 'r', encoding='utf-8') as data_file: data = json.load(data_file) + self.topology = self.import_topology_data(data) + def import_topology_data(self, data): try: id = data['id'] name=data['name'] @@ -31,6 +32,8 @@ def _import_topology(self, topology_filename): raise MissingAttributeException() topology=Topology(id=id, name=name, version=version, time_stamp=time_stamp, nodes=nodes, links=links) - + + return topology + def get_topology(): return self.topology diff --git a/tests/__init__.py b/test/__init__.py similarity index 100% rename from tests/__init__.py rename to test/__init__.py diff --git a/test/test_topology_handler.py b/test/test_topology_handler.py new file mode 100644 index 0000000..8fd81d0 --- /dev/null +++ b/test/test_topology_handler.py @@ -0,0 +1,29 @@ +import unittest + +import parsing + +from parsing.topologyhandler import TopologyHandler +from parsing.exceptions import DataModelException + +TOPOLOGY_AMLIGHT = './test/amlight.json' + +class TestTopologyHandler(unittest.TestCase): + + def setUp(self): + self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) # noqa: E501 + + def tearDown(self): + pass + + def testImportTopology(self): + try: + print("Test") + self.handler.import_topology() + print(self.handler.topology) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py new file mode 100644 index 0000000..593dfc2 --- /dev/null +++ b/test/test_topology_validator.py @@ -0,0 +1,33 @@ +import unittest + +import parsing + +from validation.topologyvalidator import TopologyValidator +from parsing.topologyhandler import TopologyHandler +from parsing.exceptions import DataModelException + +TOPOLOGY_AMLIGHT = './test/amlight.json' + +class TopologyValidator(unittest.TestCase): + + def setUp(self): + self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) + self.validator = TopologyValidator() # noqa: E501 + + def tearDown(self): + pass + + def testTopology(self): + try: + print("Import Topology:") + self.handler.import_topology() + self.validator.topology(self.handler.topology) + self.validator.isValida() + print(self.handler.topology) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/validation/validator.py b/validation/topologyvalidator.py similarity index 97% rename from validation/validator.py rename to validation/topologyvalidator.py index 66785ca..182b1a5 100644 --- a/validation/validator.py +++ b/validation/topologyvalidator.py @@ -3,10 +3,10 @@ Synopsis: A validation class to evaluate that the supplied Topology object contains expected data format """ from collections.abc import Iterable -from grenml.models import Topology, Institution, Node, Link, GLOBAL_INSTITUTION_ID -from grenml.models.meta import GRENMLObject, Location, Lifetime -from re import match +from models import Topology, Node, Link, Location + ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' + class TopologyValidator: """ The validation class made to validate a Topology @@ -36,7 +36,7 @@ def validate(self, topology=None, raise_error=True): return errors def _validate_topology(self, topology: Topology, parent=None): """ - Validate that the topology provided meets the XSD standards. + Validate that the topology provided meets the JSON schema. A topology must have the following: - It must meet object default standards. - It must have a Primary owner assigned From 55769121d39e6d2fcf9a5dcaa23fa188326adaa2 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 23 Jul 2021 17:41:35 -0400 Subject: [PATCH 03/67] added a simple topology json file for testing --- test/amlight.json | 138 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 test/amlight.json diff --git a/test/amlight.json b/test/amlight.json new file mode 100644 index 0000000..69248fb --- /dev/null +++ b/test/amlight.json @@ -0,0 +1,138 @@ +{ + "id": "id", + "links": [ + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "id", + "latency": 146582.15146899645, + "name": "miami-Boca.amLight.sdx", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:10", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "10", + "status": "up" + } + ], + "short_name": "Miami-BocaRaton", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "id", + "latency": 146582.15146899645, + "name": "miami-Boca.amLight.sdx", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:10", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "10", + "status": "up" + } + ], + "short_name": "Miami-BocaRaton", + "total_bandwidth": 80083.7389632821 + } + ], + "name": "amLight", + "nodes": [ + { + "id": "id", + "location": { + "address": "Miami", + "latitude": -28.51107891831147, + "longitude": -79.57947854792273 + }, + "name": "urn:sdx:node:amlight.net:Novi06", + "ports": [ + { + "id": "id", + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "id", + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + } + ], + "short_name": "Novi06" + }, + { + "id": "id", + "location": { + "address": "Miami", + "latitude": -28.51107891831147, + "longitude": -79.57947854792273 + }, + "name": "urn:sdx:node:amlight.net:Novi06", + "ports": [ + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + } + ], + "short_name": "Novi06" + } + ], + "time_stamp": "2000-01-23T04:56:07+00:00", + "version": 1 + } \ No newline at end of file From be99da1b02b45e03379075230aadb43ca63b1e2d Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 23 Jul 2021 17:44:15 -0400 Subject: [PATCH 04/67] update the readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 97b1825..0279548 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,12 @@ This set of updates mainly come from the domain monitoring system which is suppo ## Topology description schemas There are defined in the *schema* subfolder. Some attributes of each objects are requied (Can be found in the API definition) while some are optional. Two attributes are worth of mentioning: (1) In the *service* object, there is a *vender* attribute for the domain to list device vendors that are NOT in its domain, (2) in the topology, link, node, and port objects, there is an *private* attibute for the domain to list attributes that need to kept private.:wq +## Unittest +``` +python -m unittest -v test.test_topology_handler +``` +``` +python -m unittest -v test.test_topology_validator +``` + ## Accompanying AW-SDX Projects From dc21e93cd8099a9bd1fdd8f0e60858dab44d669f Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 6 Aug 2021 12:23:20 -0400 Subject: [PATCH 05/67] adding exceptions and more on validation --- models/topology.py | 2 + parsing/exceptions.py | 4 +- parsing/topologyhandler.py | 7 +- schemas/Port.json | 2 +- schemas/Topology.json | 5 +- test/test_topology_validator.py | 2 +- validation/topologyvalidator.py | 257 +++++++++++++++++++++++--------- 7 files changed, 201 insertions(+), 78 deletions(-) diff --git a/models/topology.py b/models/topology.py index e7e0a9b..f3e51ab 100644 --- a/models/topology.py +++ b/models/topology.py @@ -15,6 +15,8 @@ import six +GLOBAL_INSTITUTION_ID = 'urn:ogf:networking:global' + class Topology(object): """NOTE: This class is auto generated by the swagger code generator program. diff --git a/parsing/exceptions.py b/parsing/exceptions.py index 8920de0..9914ffe 100644 --- a/parsing/exceptions.py +++ b/parsing/exceptions.py @@ -10,7 +10,7 @@ class MissingAttributeException(DataModelException): """ def __init__(self, attribute_name, expected_attribute): - self.element_name = attribute_name + self.attribute_name = attribute_name self.expected_attribute = expected_attribute def __str__(self): @@ -19,3 +19,5 @@ def __str__(self): self.attribute_name, ) + + diff --git a/parsing/topologyhandler.py b/parsing/topologyhandler.py index e2a6f8e..dc3a4cb 100644 --- a/parsing/topologyhandler.py +++ b/parsing/topologyhandler.py @@ -24,14 +24,15 @@ def import_topology_data(self, data): try: id = data['id'] name=data['name'] + service=data['domain_service'] version=data['version'] time_stamp=data['time_stamp'] nodes=data['nodes'] links=data['links'] - except: - raise MissingAttributeException() + except KeyError as e: + raise MissingAttributeException(e.args[0],e.args[0]) - topology=Topology(id=id, name=name, version=version, time_stamp=time_stamp, nodes=nodes, links=links) + topology=Topology(id=id, name=name, domain_service = service, version=version, time_stamp=time_stamp, nodes=nodes, links=links) return topology diff --git a/schemas/Port.json b/schemas/Port.json index 4c55586..55aedc3 100644 --- a/schemas/Port.json +++ b/schemas/Port.json @@ -12,7 +12,7 @@ "type": "string" }, "name": { - "description": "This is supposed to be an urn", + "description": "This is supposed to be a full name", "type": "string" }, "node": { diff --git a/schemas/Topology.json b/schemas/Topology.json index 80dfb4c..0d2aa26 100644 --- a/schemas/Topology.json +++ b/schemas/Topology.json @@ -4,9 +4,11 @@ "type": "object", "properties": { "id": { + "description": "This is supposed to be an unique identifier, starting with urn:ogf:networking:global", "type": "string" }, "name": { + "description": "This is supposed to be a readable name", "type": "string" }, "time_stamp": { @@ -14,8 +16,9 @@ "format": "date-time" }, "version": { + "description": "This is supposed to be in ISO format", "type": "number", - "minimum": 0 + "minimum": 0, }, "domain_service": { "$ref": "Service" diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index 593dfc2..f098857 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -22,7 +22,7 @@ def testTopology(self): print("Import Topology:") self.handler.import_topology() self.validator.topology(self.handler.topology) - self.validator.isValida() + self.validator.isValid() print(self.handler.topology) except DataModelException as e: print(e) diff --git a/validation/topologyvalidator.py b/validation/topologyvalidator.py index 182b1a5..a784c92 100644 --- a/validation/topologyvalidator.py +++ b/validation/topologyvalidator.py @@ -3,7 +3,9 @@ Synopsis: A validation class to evaluate that the supplied Topology object contains expected data format """ from collections.abc import Iterable -from models import Topology, Node, Link, Location +from models import Topology, Service, Node, Link, Location +from models.topology import GLOBAL_INSTITUTION_ID +from re import match ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' @@ -34,11 +36,12 @@ def validate(self, topology=None, raise_error=True): if errors and raise_error: raise ValueError('\n'.join(errors)) return errors - def _validate_topology(self, topology: Topology, parent=None): + def _validate_topology(self, topology: Topology): """ Validate that the topology provided meets the JSON schema. A topology must have the following: - - It must meet object default standards. + - It must meet object standard + - It must have the default fields: id, name, version, time_stamp, nodes, and links - It must have a Primary owner assigned - It must have its primary owner in its institution list - It must have the global institution in the institution list @@ -47,25 +50,13 @@ def _validate_topology(self, topology: Topology, parent=None): :return: A list of any issues in the data. """ errors = [] - errors += self._validate_object_defaults(topology, parent) - if not topology.primary_owner: - errors.append('Topology {} ID: {} must have a primary owner'.format(topology.name, topology.id)) - elif topology.primary_owner not in topology.institutions: - errors.append( - 'Topology {} ID: {} primary owner ID {} must be in the Topologies Institutions'.format( - topology.name, topology.id, topology.primary_owner, - ) - ) - if not topology._parent == parent: - errors.append( - 'Topology {} parent topology does not match. {} != {}'.format( - topology.id, topology._parent, parent - ) - ) - if GLOBAL_INSTITUTION_ID not in topology.institutions: + errors += self._validate_object_defaults(topology) + + if GLOBAL_INSTITUTION_ID not in topology.id: errors.append('Global Institution must be in Topology {}'.format(topology.id)) - for inst in topology.institutions: - errors += self._validate_institution(inst, topology) + + for service in topology.domain_service: + errors += self._validate_service(service, topology) for node in topology.nodes: errors += self._validate_node(node, topology) for link in topology.links: @@ -73,7 +64,8 @@ def _validate_topology(self, topology: Topology, parent=None): for sub_topology in topology.topologies: errors += self._validate_topology(sub_topology, topology) return errors - def _validate_institution(self, institution: Institution, topology: Topology): + + def _validate_service(self, service: Service, topology: Topology): """ Validate that the institution provided meets the XSD standards. A institution must have the following: @@ -85,56 +77,66 @@ def _validate_institution(self, institution: Institution, topology: Topology): :return: A list of any issues in the data. """ errors = [] - errors += self._validate_object_defaults(institution, topology) - errors += self._validate_location(institution, False) + errors += self._validate_object_defaults(service, topology) + return errors - def _validate_node(self, node: Node, topology: Topology): + + def _validate_version(self, version, time_stamp, topology: Topology): """ - Validate that the node provided meets the XSD standards. - A node must have the following: + Validate that the institution provided meets the XSD standards. + A institution must have the following: - It must meet object default standards. - It's Location values are valid - - It's Lifetime values are valid - - All owners in the node must be strings - - All owners in the node must be IDs for an institution in the parent topologies institution list - - The node must have its parent Topology's primary owner in its list of owners - :param node: The Node being evaluated. + - The Institution Type must be in the list of valid Institution types + :param institution: The Institution being evaluated. :param topology: The Parent Topology. :return: A list of any issues in the data. - """ - errors = [] - errors += self._validate_object_defaults(node, topology) - errors += self._validate_location(node) - errors += self._validate_lifetime(node) - for owner in node._owners: - if not isinstance(owner, str): + """ + errors = [] + if version: + if not isinstance(version, str): errors.append( - 'Node {} owner {} should be a string. Not {}'.format( - node.id, owner, owner.__class__.__name__ + '{} Version must be a String'.format( + topology.id, ) ) - if topology and owner not in topology.institutions: + elif not match(ISO_FORMAT, version): errors.append( - 'Node {} listed owner id {} does not exist in parent Topology {}'.format( - node.id, owner, topology.id + '{} Version must be datetime ISO format'.format( + topology.id, ) ) - if topology and topology.primary_owner not in node.owners: + + if not match(ISO_FORMAT, time_stamp): errors.append( - 'Node {} does not have the Topology primary owner {} in its list of owners'.format( - node.id, topology.primary_owner + '{} time_stamp needs to be in full ISO format'.format( + time_stamp, ) ) + return errors + + def _validate_node(self, node: Node, topology: Topology): + """ + Validate that the node provided meets the XSD standards. + A node must have the following: + - It must meet object default standards. + - It's Location values are valid + :param node: The Node being evaluated. + :param topology: The Parent Topology. + :return: A list of any issues in the data. + """ + errors = [] + errors += self._validate_object_defaults(node, topology) + errors += self._validate_location(node) + + return errors + def _validate_link(self, link: Link, topology: Topology): """ Validate that the link provided meets the XSD standards. A link must have the following: - It must meet object default standards. - - It's Lifetime values are valid - - All owners in the link must be strings - - All owners in the link must be IDs for an institution in the parent topologies institution list - - The link must have its parent Topology's primary owner in its list of owners - A link can only connect to 2 nodes - The nodes that a link is connected to must be in the parent Topology's nodes list :param link: The Link being evaluated. @@ -143,26 +145,7 @@ def _validate_link(self, link: Link, topology: Topology): """ errors = [] errors += self._validate_object_defaults(link, topology) - errors += self._validate_lifetime(link) - for owner in link._owners: - if not isinstance(owner, str): - errors.append( - 'Link {} owner {} should be a string. Not {}'.format( - link.id, owner, owner.__class__.__name__ - ) - ) - if topology and owner not in topology.institutions: - errors.append( - 'Link {} listed owner id {} does not exist in parent Topology {}'.format( - link.id, owner, topology.id - ) - ) - if topology and topology.primary_owner not in link.owners: - errors.append( - 'Link {} does not have the Topology primary owner {} in its list of owners'.format( - link.id, topology.primary_owner - ) - ) + if len(link._nodes) != 2: errors.append( 'Link {} must connect between 2 Nodes. Currently {}'.format( @@ -182,4 +165,136 @@ def _validate_link(self, link: Link, topology: Topology): link.id, node, topology.id ) ) + return errors + + def _validate_object_defaults(self, sdx_object): + """ + Validate that the object provided default fields meets the XSD standards. + The object must have the following: + - The object must have an ID + - The object ID must be a string + - The object must have a name + - The object name must be a string + - If the object has a short name, it must be a string + - If the object has a version, it must be a string in ISO format + - All the additional properties on the object are proper + :param sdx_object: The sdx Model Object being evaluated. + :return: A list of any issues in the data. + """ + errors = [] + if not sdx_object.id: + errors.append('{} must have an ID'.format(sdx_object.__class__.__name__)) + if not isinstance(sdx_object.id, str): + errors.append('{} ID must be a string'.format(sdx_object.__class__.__name__)) + if not sdx_object.name: + errors.append( + '{} {} must have a name'.format( + sdx_object.__class__.__name__, sdx_object.id, + ) + ) + if not isinstance(sdx_object.name, str): + errors.append( + '{} {} name must be a String'.format( + sdx_object.__class__.__name__, sdx_object.id, + ) + ) + if sdx_object.short_name: + if not isinstance(sdx_object.short_name, str): + errors.append( + '{} {} Short name must be a string'.format( + sdx_object.__class__.__name__, sdx_object.id, + ) + ) + + errors += self._validate_additional_properties(sdx_object) + return errors + + def _validate_location(self, location: Location, enforce_coordinates=True): + """ + Validate that the object location fields meets the XSD standards. + The location must have the following: + - A location must have a longitude + - A location's longitude muse be a floating point value + - A location's longitude must be between -180 and -180 + - A location must have a latitude + - A location must be a floating point value + - A location's latitude must be between -90 and 90 + - A location's altitude must be a floating point value + - A location's UN/LOCODE must be a string value + - A location's address must be a string or a list of strings + :param location: The Location Object being evaluated. + :param enforce_coordinates: A boolean determining if longitude and latitude should be enforced + :return: A list of any issues in the data. + """ + errors = [] + if location.longitude is None and enforce_coordinates: + errors.append( + '{} {} Longitude must be set to a value'.format( + location.__class__.__name__, location.id, + ) + ) + try: + if location.longitude is not None: + if not -180 <= float(location.longitude) <= 180: + errors.append( + '{} {} Longitude must be a value that is between -180 and 180'.format( + location.__class__.__name__, location.id, + ) + ) + except ValueError: + errors.append( + '{} {} Longitude must be a value that coordinates to a Floating point value'.format( + location.__class__.__name__, location.id, + ) + ) + + if location.latitude is None and enforce_coordinates: + errors.append( + '{} {} Latitude must be set to a value'.format( + location.__class__.__name__, location.id + ) + ) + try: + if location.latitude is not None: + if not -90 <= float(location.latitude) <= 90: + errors.append( + '{} {} Latitude must be a value that is between -90 and 90'.format( + location.__class__.__name__, location.id, + ) + ) + except ValueError: + errors.append( + '{} {} Latitude must be a value that coordinates to a Floating point value'.format( + location.__class__.__name__, location.id, + ) + ) + + try: + if location.altitude: + float(location.altitude) + except ValueError: + errors.append( + '{} {} Altitude must be a value that coordinates to a Floating point value'.format( + location.__class__.__name__, location.id, + ) + ) + + if location.unlocode and not isinstance(location.unlocode, str): + errors.append( + '{} {} UN/LOCODE must be a string value'.format(location.__class__.__name__, location.id) + ) + if isinstance(location.addresses, Iterable): + for address in location.addresses: + if not type(address) == str: + errors.append( + '{} {} Address {} must be a string'.format( + location.__class__.__name__, location.id, address + ) + ) + else: + errors.append( + '{} {} Addresses should be a list of strings'.format( + location.__class__.__name__, location.id + ) + ) return errors \ No newline at end of file From bfc82abf776296f46bcb4fea309e5522b45ffa01 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 13 Aug 2021 19:05:44 -0400 Subject: [PATCH 06/67] adding connection request parser, validator, and unittest --- models/connection.py | 23 +++++++----------- models/topology.py | 24 +++++++------------ parsing/connectionhandler.py | 39 +++++++++++++++++++++++++++++++ schemas/Service.json | 2 +- test/amlight.json | 5 +++- test/test_connection_handler.py | 0 test/test_connection_validator.py | 0 test/test_topology_handler.py | 23 +++++++++++++++--- validation/connectionvalidator.py | 0 9 files changed, 82 insertions(+), 34 deletions(-) create mode 100644 parsing/connectionhandler.py create mode 100644 test/test_connection_handler.py create mode 100644 test/test_connection_validator.py create mode 100644 validation/connectionvalidator.py diff --git a/models/connection.py b/models/connection.py index d335ebb..65396fb 100644 --- a/models/connection.py +++ b/models/connection.py @@ -53,30 +53,25 @@ class Connection(object): def __init__(self, id=None, name=None, ingress_port=None, egress_port=None, quantity=None, start_time=None, end_time=None, status=None, complete=False): # noqa: E501 """Connection - a model defined in Swagger""" # noqa: E501 - self._id = None - self._name = None - self._ingress_port = None - self._egress_port = None self._quantity = None self._start_time = None self._end_time = None self._status = None self._complete = None - self.discriminator = None - self.id = id - self.name = name - self.ingress_port = ingress_port - self.egress_port = egress_port + self._id = id + self._name = name + self._ingress_port = ingress_port + self._egress_port = egress_port if quantity is not None: - self.quantity = quantity + self._quantity = quantity if start_time is not None: - self.start_time = start_time + self._start_time = start_time if end_time is not None: - self.end_time = end_time + self._end_time = end_time if status is not None: - self.status = status + self._status = status if complete is not None: - self.complete = complete + self._complete = complete @property def id(self): diff --git a/models/topology.py b/models/topology.py index f3e51ab..54a7f1e 100644 --- a/models/topology.py +++ b/models/topology.py @@ -53,25 +53,19 @@ class Topology(object): def __init__(self, id=None, name=None, domain_service=None, version=None, time_stamp=None, nodes=None, links=None, private_attributes=None): # noqa: E501 """Topology - a model defined in Swagger""" # noqa: E501 - self._id = None - self._name = None + self._domain_service = None - self._version = None - self._time_stamp = None - self._nodes = None - self._links = None self._private_attributes = None - self.discriminator = None - self.id = id - self.name = name + self._id = id + self._name = name if domain_service is not None: - self.domain_service = domain_service - self.version = version - self.time_stamp = time_stamp - self.nodes = nodes - self.links = links + self._domain_service = domain_service + self._version = version + self._time_stamp = time_stamp + self._nodes = nodes + self._links = links if private_attributes is not None: - self.private_attributes = private_attributes + self._private_attributes = private_attributes @property def id(self): diff --git a/parsing/connectionhandler.py b/parsing/connectionhandler.py new file mode 100644 index 0000000..7c2bab1 --- /dev/null +++ b/parsing/connectionhandler.py @@ -0,0 +1,39 @@ +import json +from models.connection import Connection +from .exceptions import MissingAttributeException + +MANIFEST_FILE = None + +class ConnectionHandler(): + + """" + Handler for parsing the connection request descritpion in json + """ + + def __init__(self,connection_data): + super().__init__() + self.connection = None + + self._id = None + self._name = None + self._ingress_port = None + self._egress_port = None + + def import_connection_data(self, data): + try: + id = data['id'] + name=data['name'] + ingress_port=data['ingress_port'] + egress_port=data['elf._egress_port'] + quantity=data['quantity'] + start_time=data['start_time'] + end_time=data['end_time'] + except KeyError as e: + raise MissingAttributeException(e.args[0],e.args[0]) + + connection=Connection(id=id, name=name, domain_service = service, version=version, time_stamp=time_stamp, nodes=nodes, links=links) + + return connection + + def get_connection(): + return self.connection diff --git a/schemas/Service.json b/schemas/Service.json index b886226..571a5c1 100644 --- a/schemas/Service.json +++ b/schemas/Service.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Port", + "title": "Service", "type": "object", "properties": { "owner": { diff --git a/test/amlight.json b/test/amlight.json index 69248fb..173068c 100644 --- a/test/amlight.json +++ b/test/amlight.json @@ -134,5 +134,8 @@ } ], "time_stamp": "2000-01-23T04:56:07+00:00", - "version": 1 + "version": 1, + "domain_service": { + "owner":"FIU" + } } \ No newline at end of file diff --git a/test/test_connection_handler.py b/test/test_connection_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_connection_validator.py b/test/test_connection_validator.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_topology_handler.py b/test/test_topology_handler.py index 8fd81d0..becf473 100644 --- a/test/test_topology_handler.py +++ b/test/test_topology_handler.py @@ -11,18 +11,35 @@ class TestTopologyHandler(unittest.TestCase): def setUp(self): self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) # noqa: E501 - + self.handler.import_topology() def tearDown(self): pass def testImportTopology(self): try: - print("Test") - self.handler.import_topology() + print("Test Topology") print(self.handler.topology) except DataModelException as e: print(e) + return False + return True + + def testImportTopologyNodes(self): + print("Test Nodes: at least one:") + nodes=self.handler.topology.nodes + if nodes==None or len(nodes)==0: + print("Nodes are empty") + return False + print(nodes[0]) + return True + + def testImportTopologyLinks(self): + print("Test Links: at least one") + links=self.handler.topology.links + if links==None or len(links)==0: + print("Links are empty") return False + print(links[0]) return True if __name__ == '__main__': diff --git a/validation/connectionvalidator.py b/validation/connectionvalidator.py new file mode 100644 index 0000000..e69de29 From 5f4b99d0aefbdb76a1027e213797ce95fae2c820 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sat, 14 Aug 2021 23:30:25 -0400 Subject: [PATCH 07/67] connection_handler unittest --- parsing/connectionhandler.py | 16 ++++++++++------ test/test_connection_handler.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/parsing/connectionhandler.py b/parsing/connectionhandler.py index 7c2bab1..a09d8dc 100644 --- a/parsing/connectionhandler.py +++ b/parsing/connectionhandler.py @@ -23,17 +23,21 @@ def import_connection_data(self, data): try: id = data['id'] name=data['name'] - ingress_port=data['ingress_port'] - egress_port=data['elf._egress_port'] - quantity=data['quantity'] - start_time=data['start_time'] - end_time=data['end_time'] + start=data['start_time'] + end=data['end_time'] + ingress=data['ingress_port'] + egress=data['egress_port'] except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) - connection=Connection(id=id, name=name, domain_service = service, version=version, time_stamp=time_stamp, nodes=nodes, links=links) + connection=Connection(id=id, name=name, start_time = start, end_time = end, ingress_port = ingress, egress_port = egress) return connection + + def import_connection(self,file): + with open(file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.connection = self.import_connection_data(data) def get_connection(): return self.connection diff --git a/test/test_connection_handler.py b/test/test_connection_handler.py index e69de29..71861d4 100644 --- a/test/test_connection_handler.py +++ b/test/test_connection_handler.py @@ -0,0 +1,28 @@ +import unittest + +import parsing + +from parsing.connectionhandler import ConnectionHandler +from parsing.exceptions import DataModelException + +CONNECTION_P2P = './test/p2p.json' + +class TestConnectionHandler(unittest.TestCase): + + def setUp(self): + self.handler = ConnectionHandler(CONNECTION_P2P) # noqa: E501 + def tearDown(self): + pass + + def testImportConnection(self): + try: + print("Test Connection") + self.handler.import_connection(CONNECTION_P2P) + print(self.handler.connection) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 8403ec84f220bc2a293803a2c9f434c6c441078c Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sun, 15 Aug 2021 22:12:02 -0400 Subject: [PATCH 08/67] addding parsing handlers for other objects --- models/connection.py | 10 ++- models/link.py | 23 +++-- models/node.py | 13 ++- models/port.py | 19 ++-- parsing/connectionhandler.py | 15 ++-- parsing/linkhandler.py | 44 ++++++++++ parsing/nodehandler.py | 36 ++++++++ parsing/porthandler.py | 36 ++++++++ parsing/servicehandler.py | 36 ++++++++ test/p2p.json | 22 +++++ test/test_connection_handler.py | 2 +- test/test_connection_validator.py | 34 ++++++++ test/test_topology_validator.py | 6 +- validation/connectionvalidator.py | 140 ++++++++++++++++++++++++++++++ validation/topologyvalidator.py | 1 - 15 files changed, 392 insertions(+), 45 deletions(-) create mode 100644 parsing/linkhandler.py create mode 100644 parsing/nodehandler.py create mode 100644 parsing/porthandler.py create mode 100644 parsing/servicehandler.py create mode 100644 test/p2p.json diff --git a/models/connection.py b/models/connection.py index 65396fb..6cf67fb 100644 --- a/models/connection.py +++ b/models/connection.py @@ -12,9 +12,11 @@ import pprint import re # noqa: F401 - import six +from .port import Port +from parsing.porthandler import PortHandler + class Connection(object): """NOTE: This class is auto generated by the swagger code generator program. @@ -140,7 +142,8 @@ def ingress_port(self, ingress_port): if ingress_port is None: raise ValueError("Invalid value for `ingress_port`, must not be `None`") # noqa: E501 - self._ingress_port = ingress_port + port_handler = PortHandler() + self._ingress_port = port_handler.import_port_data(self, ingress_port) @property def egress_port(self): @@ -163,7 +166,8 @@ def egress_port(self, egress_port): if egress_port is None: raise ValueError("Invalid value for `egress_port`, must not be `None`") # noqa: E501 - self._egress_port = egress_port + port_handler = PortHandler() + self._egress_port = port_handler.import_port_data(self, egress_port) @property def quantity(self): diff --git a/models/link.py b/models/link.py index 55464e1..2394eec 100644 --- a/models/link.py +++ b/models/link.py @@ -71,28 +71,27 @@ def __init__(self, id=None, name=None, short_name=None, ports=None, total_bandwi self._private_attributes = None self._time_stamp = None self._measurement_period = None - self.discriminator = None - self.id = id - self.name = name + self._id = id + self._name = name if short_name is not None: - self.short_name = short_name + self._short_name = short_name self.ports = ports if total_bandwidth is not None: - self.total_bandwidth = total_bandwidth + self._total_bandwidth = total_bandwidth if available_bandwidth is not None: - self.available_bandwidth = available_bandwidth + self._available_bandwidth = available_bandwidth if latency is not None: - self.latency = latency + self._latency = latency if packet_loss is not None: - self.packet_loss = packet_loss + self._packet_loss = packet_loss if availability is not None: - self.availability = availability + self._availability = availability if private_attributes is not None: - self.private_attributes = private_attributes + self._private_attributes = private_attributes if time_stamp is not None: - self.time_stamp = time_stamp + self._time_stamp = time_stamp if measurement_period is not None: - self.measurement_period = measurement_period + self._measurement_period = measurement_period @property def id(self): diff --git a/models/node.py b/models/node.py index 2dd8025..b299c44 100644 --- a/models/node.py +++ b/models/node.py @@ -53,15 +53,14 @@ def __init__(self, id=None, name=None, short_name=None, location=None, ports=Non self._location = None self._ports = None self._private_attributes = None - self.discriminator = None - self.id = id - self.name = name + self._id = id + self._name = name if short_name is not None: - self.short_name = short_name - self.location = location - self.ports = ports + self._short_name = short_name + self._location = location + self._ports = ports if private_attributes is not None: - self.private_attributes = private_attributes + self._private_attributes = private_attributes @property def id(self): diff --git a/models/port.py b/models/port.py index c2697aa..d0e16ba 100644 --- a/models/port.py +++ b/models/port.py @@ -56,17 +56,20 @@ def __init__(self, id=None, name=None, short_name=None, node=None, label_range=N self._label_range = None self._status = None self._private_attributes = None - self.discriminator = None - self.id = id - self.name = name + self._id = id + self._name = name if short_name is not None: - self.short_name = short_name - self.node = node + self._short_name = short_name + self._node = node if label_range is not None: - self.label_range = label_range - self.status = status + self._label_range = label_range + self._status = status if private_attributes is not None: - self.private_attributes = private_attributes + self._private_attributes = private_attributes + + def __init__(self, port:dict): + self.__init__(id=id, name=port['name'],short_name=port['short_name'], node=port['node'], + label_range=port['label_range'], status=None, private_attributes=port['private_attributes']) @property def id(self): diff --git a/parsing/connectionhandler.py b/parsing/connectionhandler.py index a09d8dc..ca49e50 100644 --- a/parsing/connectionhandler.py +++ b/parsing/connectionhandler.py @@ -2,29 +2,24 @@ from models.connection import Connection from .exceptions import MissingAttributeException -MANIFEST_FILE = None - class ConnectionHandler(): """" Handler for parsing the connection request descritpion in json """ - def __init__(self,connection_data): + def __init__(self): super().__init__() self.connection = None - self._id = None - self._name = None - self._ingress_port = None - self._egress_port = None - def import_connection_data(self, data): try: id = data['id'] name=data['name'] - start=data['start_time'] - end=data['end_time'] + if 'start_time' in data.keys(): + start=data['start_time'] + if 'end_time' in data.keys(): + end=data['end_time'] ingress=data['ingress_port'] egress=data['egress_port'] except KeyError as e: diff --git a/parsing/linkhandler.py b/parsing/linkhandler.py new file mode 100644 index 0000000..2b7b78c --- /dev/null +++ b/parsing/linkhandler.py @@ -0,0 +1,44 @@ +import json +from models.link import Link +from .exceptions import MissingAttributeException + +class LinkHandler(): + + """" + Handler for parsing the connection request descritpion in json + """ + + def __init__(self): + super().__init__() + self.link = None + + def import_link_data(self, data): + try: + id = data['id'] + name=data['name'] + short_name=data['short_name'] + ports=data['ports'] + timestamp=data['time_stamp'] + if 'total_bandwidth' in data.keys(): + t_b=data['total_bandwidth'] + a_b=data['available_bandwidth'] + latency=data['latency'] + p_l=data['packet_loss'] + p_a=data['private_attributes'] + avai=data['availability'] + m_p=data['measurement_period'] + except KeyError as e: + raise MissingAttributeException(e.args[0],e.args[0]) + + link=Link(id=id, name=name, short_name=short_name, ports=ports, total_bandwidth=t_b, available_bandwidth=a_b, + latency=latency, packet_loss=p_l, availability=avai, private_attributes=p_a, time_stamp=timestamp, measurement_period=None) + + return link + + def import_link(self,file): + with open(file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.link = self.import_link_data(data) + + def get_link(): + return self.link diff --git a/parsing/nodehandler.py b/parsing/nodehandler.py new file mode 100644 index 0000000..0c6db9d --- /dev/null +++ b/parsing/nodehandler.py @@ -0,0 +1,36 @@ +import json +from models.node import Node +from .exceptions import MissingAttributeException + +class NodeHandler(): + + """" + Handler for parsing the connection request descritpion in json + """ + + def __init__(self): + super().__init__() + self.node = None + + def import_node_data(self, data): + try: + id = data['id'] + name=data['name'] + short_name=data['short_name'] + location=data['location'] + ports=data['ports'] + private_attributes=data['private_attributes'] + except KeyError as e: + raise MissingAttributeException(e.args[0],e.args[0]) + + node=Node(id=id, name=name, short_name=short_name, location=location, ports=ports, private_attributes=private_attributes) + + return node + + def import_node(self,file): + with open(file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.node = self.import_node_data(data) + + def get_node(): + return self.node diff --git a/parsing/porthandler.py b/parsing/porthandler.py new file mode 100644 index 0000000..55fac7a --- /dev/null +++ b/parsing/porthandler.py @@ -0,0 +1,36 @@ +import json +from models.port import Port +from .exceptions import MissingAttributeException + +class PortHandler(): + + """" + Handler for parsing the connection request descritpion in json + """ + + def __init__(self): + super().__init__() + self.port = None + + def import_port_data(self, data): + try: + id = data['id'] + name=data['name'] + short_name=data['short_name'] + node=data['node'] + l_r=data['label_range'] + p_a=data['private_attributes'] + except KeyError as e: + raise MissingAttributeException(e.args[0],e.args[0]) + + port=Port(id=id, name=name,short_name=short_name, node=node, label_range=l_r, status=None, private_attributes=p_a) + + return port + + def import_port(self,file): + with open(file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.port = self.import_port_data(data) + + def get_port(): + return self.port diff --git a/parsing/servicehandler.py b/parsing/servicehandler.py new file mode 100644 index 0000000..a7eb80c --- /dev/null +++ b/parsing/servicehandler.py @@ -0,0 +1,36 @@ +import json +from models.service import Service +from .exceptions import MissingAttributeException + +class ServiceHandler(): + + """" + Handler for parsing the connection request descritpion in json + """ + + def __init__(self): + super().__init__() + self.service = None + + def import_service_data(self, data): + try: + id = data['id'] + name=data['name'] + start=data['start_time'] + end=data['end_time'] + ingress=data['ingress_port'] + egress=data['egress_port'] + except KeyError as e: + raise MissingAttributeException(e.args[0],e.args[0]) + + service=Service(id=id, name=name, start_time = start, end_time = end, ingress_port = ingress, egress_port = egress) + + return service + + def import_service(self,file): + with open(file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.service = self.import_service_data(data) + + def get_service(): + return self.service diff --git a/test/p2p.json b/test/p2p.json new file mode 100644 index 0000000..51bf1ad --- /dev/null +++ b/test/p2p.json @@ -0,0 +1,22 @@ +{ + "id": "id", + "name": "AMLight", + "start_time": "2000-01-23T04:56:07.000Z", + "end_time": "2000-01-23T04:56:07.000Z", + "egress_port": + { + "id": "urn:sdx:port:amlight.net:Novi07:10", + "name": "10", + "short_name": "10", + "node": "urn:sdx:port:amlight.net:Novi07", + "status": "up" + }, + "ingress_port": + { + "id": "urn:sdx:port:amlight.net:Novi06:9", + "name": "Novi06:9", + "short_name": "9", + "node": "urn:sdx:port:amlight.net:Novi06", + "status": "up" + } +} \ No newline at end of file diff --git a/test/test_connection_handler.py b/test/test_connection_handler.py index 71861d4..8fc20f5 100644 --- a/test/test_connection_handler.py +++ b/test/test_connection_handler.py @@ -10,7 +10,7 @@ class TestConnectionHandler(unittest.TestCase): def setUp(self): - self.handler = ConnectionHandler(CONNECTION_P2P) # noqa: E501 + self.handler = ConnectionHandler() # noqa: E501 def tearDown(self): pass diff --git a/test/test_connection_validator.py b/test/test_connection_validator.py index e69de29..50973c3 100644 --- a/test/test_connection_validator.py +++ b/test/test_connection_validator.py @@ -0,0 +1,34 @@ +import unittest + +import parsing + +from validation.connectionvalidator import ConnectionValidator +from parsing.connectionhandler import ConnectionHandler +from parsing.exceptions import DataModelException + +CONNECTION_P2P = './test/p2p.json' + +class TestConnectionValidator(unittest.TestCase): + + def setUp(self): + self.handler = ConnectionHandler() + print("Import Connection:") + self.handler.import_connection(CONNECTION_P2P) + conn = self.handler.connection + print(conn) + validator = ConnectionValidator() + validator.set_connection(conn) + + def tearDown(self): + pass + + def testConnection(self): + try: + self.validator.is_valid() + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index f098857..28c3f44 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -8,11 +8,11 @@ TOPOLOGY_AMLIGHT = './test/amlight.json' -class TopologyValidator(unittest.TestCase): +class TestTopologyValidator(unittest.TestCase): def setUp(self): self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) - self.validator = TopologyValidator() # noqa: E501 + self.validator = TopologyValidator() def tearDown(self): pass @@ -22,7 +22,7 @@ def testTopology(self): print("Import Topology:") self.handler.import_topology() self.validator.topology(self.handler.topology) - self.validator.isValid() + self.validator.is_valid() print(self.handler.topology) except DataModelException as e: print(e) diff --git a/validation/connectionvalidator.py b/validation/connectionvalidator.py index e69de29..13c7706 100644 --- a/validation/connectionvalidator.py +++ b/validation/connectionvalidator.py @@ -0,0 +1,140 @@ +""""" +-------------------------------------------------------------------- +Synopsis: A validation class to evaluate that the supplied Connection object contains expected data format +""" +from collections.abc import Iterable +from datetime import * +from models import Connection, Port +from re import match + +ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' + +class ConnectionValidator: + """ + The validation class made to validate a Connection request + """ + def __init__(self): + super().__init__() + self._connection = None + + @property + def connection(self): + return self._connection + + @connection.setter + def set_connection(self, conn): + if not isinstance(conn, Connection): + raise ValueError('The Validator must be passed a Connection object') + self._connection = conn + + @property + def is_valid(self): + errors = self.validate(self._connection, raise_error=True) + for error in errors: + print(error) + return not bool(errors) + + def validate(self, conn=None, raise_error=True): + if not conn and self._connection: + conn = self._connection + errors = self._validate_connection(conn) + if errors and raise_error: + raise ValueError('\n'.join(errors)) + return errors + + def _validate_connection(self, conn: Connection): + """ + Validate that the connection provided meets the JSON schema. + A connection must have the following: + - It must meet object standard + - It must have the default fields: id, name, ingress_port, and egress_port + :param connection: The connection being evaluated + :return: A list of any issues in the data. + """ + errors = [] + errors += self._validate_object_defaults(conn) + + for ingress_port in conn.ingress_port: + errors += self._validate_port(ingress_port, conn) + for egress_port in conn.egress_port: + errors += self._validate_port(egress_port, conn) + for start_time in conn.start_time: + errors += self._validate_time(start_time, conn) + for end_time in conn.end_time: + errors += self._validate_time(end_time, conn) + return errors + + def _validate_port(self, port: Port, conn: Connection): + """ + Validate that the port provided meets the XSD standards. + A port must have the following: + - It must meet object default standards. + - A port must belong to a topology + - The node is optional + :param port: The port being evaluated. + :param topology: The Topology. + :return: A list of any issues in the data. + """ + errors = [] + errors += self._validate_object_defaults(port) + + if not port: + errors.append('{} must exist'.format(port.__class__.__name__)) + + """ + node = find_node(port,topology) + if topology and node not in topology.nodes: + errors.append( + 'listed node id {} does not exist in parent Topology {}'.format( + node.id, node, topology.id + ) + ) + """ + return errors + + def _validate_time(self, time: datetime, conn: Connection): + """ + Validate that the time provided meets the XSD standards. + A port must have the following: + - It must meet object default standards. + - A link can only connect to 2 nodes + - The nodes that a link is connected to must be in the parent Topology's nodes list + :param time: time being validated + :return: A list of any issues in the data. + """ + errors = [] + pass + + def _validate_object_defaults(self, sdx_object): + """ + Validate that the object provided default fields meets the XSD standards. + The object must have the following: + - The object must have an ID + - The object ID must be a string + - The object must have a name + - The object name must be a string + - If the object has a short name, it must be a string + - If the object has a version, it must be a string in ISO format + - All the additional properties on the object are proper + :param sdx_object: The sdx Model Object being evaluated. + :return: A list of any issues in the data. + """ + errors = [] + if not sdx_object._id: + errors.append('{} must have an ID'.format(sdx_object.__class__.__name__)) + if not isinstance(sdx_object._id, str): + errors.append('{} ID must be a string'.format(sdx_object.__class__.__name__)) + if not sdx_object._name: + errors.append( + '{} {} must have a name'.format( + sdx_object.__class__.__name__, sdx_object._name, + ) + ) + if not isinstance(sdx_object._name, str): + errors.append( + '{} {} name must be a String'.format( + sdx_object.__class__.__name__, sdx_object._name, + ) + ) + + return errors \ No newline at end of file diff --git a/validation/topologyvalidator.py b/validation/topologyvalidator.py index a784c92..b7115e6 100644 --- a/validation/topologyvalidator.py +++ b/validation/topologyvalidator.py @@ -46,7 +46,6 @@ def _validate_topology(self, topology: Topology): - It must have its primary owner in its institution list - It must have the global institution in the institution list :param topology: The topology being evaluated - :param parent: The Parent Topology. If this topology is the top level, then the parent should be None. :return: A list of any issues in the data. """ errors = [] From ceb072d4672ae6ecf9a24c0bfd5f07e02d1919fc Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 16 Aug 2021 00:10:15 -0400 Subject: [PATCH 09/67] more on validation --- models/connection.py | 47 +++++++++---------------------- models/port.py | 6 ++-- parsing/porthandler.py | 8 ++++-- test/test_connection_validator.py | 5 ++-- test/test_topology_validator.py | 2 +- validation/connectionvalidator.py | 47 ++++++++++++++++--------------- validation/topologyvalidator.py | 38 ++++++++++--------------- 7 files changed, 65 insertions(+), 88 deletions(-) diff --git a/models/connection.py b/models/connection.py index 6cf67fb..324ff4f 100644 --- a/models/connection.py +++ b/models/connection.py @@ -38,7 +38,6 @@ class Connection(object): 'start_time': 'datetime', 'end_time': 'datetime', 'status': 'str', - 'complete': 'bool' } attribute_map = { @@ -50,7 +49,6 @@ class Connection(object): 'start_time': 'start_time', 'end_time': 'end_time', 'status': 'status', - 'complete': 'complete' } def __init__(self, id=None, name=None, ingress_port=None, egress_port=None, quantity=None, start_time=None, end_time=None, status=None, complete=False): # noqa: E501 @@ -59,11 +57,10 @@ def __init__(self, id=None, name=None, ingress_port=None, egress_port=None, quan self._start_time = None self._end_time = None self._status = None - self._complete = None self._id = id self._name = name - self._ingress_port = ingress_port - self._egress_port = egress_port + self.set_ingress_port(ingress_port) + self.set_egress_port(egress_port) if quantity is not None: self._quantity = quantity if start_time is not None: @@ -72,8 +69,6 @@ def __init__(self, id=None, name=None, ingress_port=None, egress_port=None, quan self._end_time = end_time if status is not None: self._status = status - if complete is not None: - self._complete = complete @property def id(self): @@ -131,8 +126,8 @@ def ingress_port(self): """ return self._ingress_port - @ingress_port.setter - def ingress_port(self, ingress_port): + #setter + def set_ingress_port(self, ingress_port): """Sets the ingress_port of this Connection. @@ -143,7 +138,9 @@ def ingress_port(self, ingress_port): raise ValueError("Invalid value for `ingress_port`, must not be `None`") # noqa: E501 port_handler = PortHandler() - self._ingress_port = port_handler.import_port_data(self, ingress_port) + self._ingress_port = port_handler.import_port_data(ingress_port) + + return self.ingress_port @property def egress_port(self): @@ -155,8 +152,8 @@ def egress_port(self): """ return self._egress_port - @egress_port.setter - def egress_port(self, egress_port): + #setter + def set_egress_port(self, egress_port): """Sets the egress_port of this Connection. @@ -167,7 +164,10 @@ def egress_port(self, egress_port): raise ValueError("Invalid value for `egress_port`, must not be `None`") # noqa: E501 port_handler = PortHandler() - self._egress_port = port_handler.import_port_data(self, egress_port) + self._egress_port = port_handler.import_port_data(egress_port) + + return self.egress_port + @property def quantity(self): @@ -261,27 +261,6 @@ def status(self, status): self._status = status - @property - def complete(self): - """Gets the complete of this Connection. # noqa: E501 - - - :return: The complete of this Connection. # noqa: E501 - :rtype: bool - """ - return self._complete - - @complete.setter - def complete(self, complete): - """Sets the complete of this Connection. - - - :param complete: The complete of this Connection. # noqa: E501 - :type: bool - """ - - self._complete = complete - def to_dict(self): """Returns the model properties as a dict""" result = {} diff --git a/models/port.py b/models/port.py index d0e16ba..ea5027d 100644 --- a/models/port.py +++ b/models/port.py @@ -67,9 +67,9 @@ def __init__(self, id=None, name=None, short_name=None, node=None, label_range=N if private_attributes is not None: self._private_attributes = private_attributes - def __init__(self, port:dict): - self.__init__(id=id, name=port['name'],short_name=port['short_name'], node=port['node'], - label_range=port['label_range'], status=None, private_attributes=port['private_attributes']) + #def __init__(self, port:dict): + # self.__init__(id=id, name=port['name'],short_name=port['short_name'], node=port['node'], + # label_range=port['label_range'], status=None, private_attributes=port['private_attributes']) @property def id(self): diff --git a/parsing/porthandler.py b/parsing/porthandler.py index 55fac7a..8d54b17 100644 --- a/parsing/porthandler.py +++ b/parsing/porthandler.py @@ -18,8 +18,12 @@ def import_port_data(self, data): name=data['name'] short_name=data['short_name'] node=data['node'] - l_r=data['label_range'] - p_a=data['private_attributes'] + l_r=None + if 'label_range' in data.keys(): + l_r=data['label_range'] + p_a=None + if 'private_attributes' in data.keys(): + p_a=data['private_attributes'] except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) diff --git a/test/test_connection_validator.py b/test/test_connection_validator.py index 50973c3..976616f 100644 --- a/test/test_connection_validator.py +++ b/test/test_connection_validator.py @@ -15,9 +15,8 @@ def setUp(self): print("Import Connection:") self.handler.import_connection(CONNECTION_P2P) conn = self.handler.connection - print(conn) - validator = ConnectionValidator() - validator.set_connection(conn) + self.validator = ConnectionValidator() + self.validator.set_connection(conn) def tearDown(self): pass diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index 28c3f44..710d4bf 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -21,7 +21,7 @@ def testTopology(self): try: print("Import Topology:") self.handler.import_topology() - self.validator.topology(self.handler.topology) + self.validator.set_topology(self.handler.topology) self.validator.is_valid() print(self.handler.topology) except DataModelException as e: diff --git a/validation/connectionvalidator.py b/validation/connectionvalidator.py index 13c7706..aa3e420 100644 --- a/validation/connectionvalidator.py +++ b/validation/connectionvalidator.py @@ -9,34 +9,31 @@ ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' -class ConnectionValidator: +class ConnectionValidator(): """ The validation class made to validate a Connection request """ def __init__(self): super().__init__() - self._connection = None + self.connection = None - @property - def connection(self): - return self._connection + def get_connection(self): + return self.connection - @connection.setter def set_connection(self, conn): if not isinstance(conn, Connection): raise ValueError('The Validator must be passed a Connection object') - self._connection = conn + self.connection = conn - @property def is_valid(self): - errors = self.validate(self._connection, raise_error=True) + errors = self.validate(self.connection, raise_error=True) for error in errors: print(error) return not bool(errors) def validate(self, conn=None, raise_error=True): if not conn and self._connection: - conn = self._connection + conn = self.connection errors = self._validate_connection(conn) if errors and raise_error: raise ValueError('\n'.join(errors)) @@ -54,14 +51,13 @@ def _validate_connection(self, conn: Connection): errors = [] errors += self._validate_object_defaults(conn) - for ingress_port in conn.ingress_port: - errors += self._validate_port(ingress_port, conn) - for egress_port in conn.egress_port: - errors += self._validate_port(egress_port, conn) - for start_time in conn.start_time: - errors += self._validate_time(start_time, conn) - for end_time in conn.end_time: - errors += self._validate_time(end_time, conn) + errors += self._validate_port(conn.ingress_port, conn) + + errors += self._validate_port(conn.egress_port, conn) + + errors += self._validate_time(conn.start_time, conn) + + errors += self._validate_time(conn.end_time, conn) return errors def _validate_port(self, port: Port, conn: Connection): @@ -75,11 +71,12 @@ def _validate_port(self, port: Port, conn: Connection): :param topology: The Topology. :return: A list of any issues in the data. """ + errors = [] - errors += self._validate_object_defaults(port) - if not port: errors.append('{} must exist'.format(port.__class__.__name__)) + + errors += self._validate_object_defaults(port) """ node = find_node(port,topology) @@ -92,7 +89,7 @@ def _validate_port(self, port: Port, conn: Connection): """ return errors - def _validate_time(self, time: datetime, conn: Connection): + def _validate_time(self, time: str, conn: Connection): """ Validate that the time provided meets the XSD standards. A port must have the following: @@ -103,7 +100,13 @@ def _validate_time(self, time: datetime, conn: Connection): :return: A list of any issues in the data. """ errors = [] - pass + if not match(ISO_FORMAT, time): + errors.append( + '{} time needs to be in full ISO format'.format( + time, + ) + ) + return errors def _validate_object_defaults(self, sdx_object): """ diff --git a/validation/topologyvalidator.py b/validation/topologyvalidator.py index b7115e6..88bfed1 100644 --- a/validation/topologyvalidator.py +++ b/validation/topologyvalidator.py @@ -14,16 +14,16 @@ class TopologyValidator: The validation class made to validate a Topology """ def __init__(self): - self._topology = None - @property - def topology(self): - return self._topology - @topology.setter - def topology(self, topology): + self.topology = None + + def get_topology(self): + return self.topology + + def set_topology(self, topology): if not isinstance(topology, Topology): raise ValueError('The Validator must be passed a Topology object') - self._topology = topology - @property + self.topology = topology + def is_valid(self): errors = self.validate(self.topology, raise_error=False) for error in errors: @@ -76,7 +76,7 @@ def _validate_service(self, service: Service, topology: Topology): :return: A list of any issues in the data. """ errors = [] - errors += self._validate_object_defaults(service, topology) + errors += self._validate_object_defaults(service) return errors @@ -181,31 +181,23 @@ def _validate_object_defaults(self, sdx_object): :return: A list of any issues in the data. """ errors = [] - if not sdx_object.id: + if not sdx_object._id: errors.append('{} must have an ID'.format(sdx_object.__class__.__name__)) - if not isinstance(sdx_object.id, str): + if not isinstance(sdx_object._id, str): errors.append('{} ID must be a string'.format(sdx_object.__class__.__name__)) - if not sdx_object.name: + if not sdx_object._name: errors.append( '{} {} must have a name'.format( - sdx_object.__class__.__name__, sdx_object.id, + sdx_object.__class__.__name__, sdx_object._name, ) ) - if not isinstance(sdx_object.name, str): + if not isinstance(sdx_object._name, str): errors.append( '{} {} name must be a String'.format( - sdx_object.__class__.__name__, sdx_object.id, + sdx_object.__class__.__name__, sdx_object._name, ) ) - if sdx_object.short_name: - if not isinstance(sdx_object.short_name, str): - errors.append( - '{} {} Short name must be a string'.format( - sdx_object.__class__.__name__, sdx_object.id, - ) - ) - errors += self._validate_additional_properties(sdx_object) return errors def _validate_location(self, location: Location, enforce_coordinates=True): From a0a3b1455dc4adbef348d5ab6dc9ca06ca59bbee Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 16 Aug 2021 23:39:56 -0400 Subject: [PATCH 10/67] service handler --- models/service.py | 16 +++++++++------- models/topology.py | 25 ++++++++++++++----------- parsing/servicehandler.py | 22 ++++++++++++++-------- validation/connectionvalidator.py | 4 +++- validation/topologyvalidator.py | 15 ++++++++------- 5 files changed, 48 insertions(+), 34 deletions(-) diff --git a/models/service.py b/models/service.py index 9663c9f..ed6fcd8 100644 --- a/models/service.py +++ b/models/service.py @@ -45,8 +45,10 @@ class Service(object): 'vendor': 'vendor' } - def __init__(self, monitoring_capability=None, owner=None, private_attributes=None, provisioning_system=None, provisioning_url=None, vendor=None): # noqa: E501 + def __init__(self, monitoring_capability=None, owner=None, private_attributes=None, provisioning_system=None, provisioning_url=None, vendor=None): """Service - a model defined in Swagger""" # noqa: E501 + self._id="service" + self._name="service" self._monitoring_capability = None self._owner = None self._private_attributes = None @@ -55,17 +57,17 @@ def __init__(self, monitoring_capability=None, owner=None, private_attributes=No self._vendor = None self.discriminator = None if monitoring_capability is not None: - self.monitoring_capability = monitoring_capability + self._monitoring_capability = monitoring_capability if owner is not None: - self.owner = owner + self._owner = owner if private_attributes is not None: - self.private_attributes = private_attributes + self._private_attributes = private_attributes if provisioning_system is not None: - self.provisioning_system = provisioning_system + self._provisioning_system = provisioning_system if provisioning_url is not None: - self.provisioning_url = provisioning_url + self._provisioning_url = provisioning_url if vendor is not None: - self.vendor = vendor + self._vendor = vendor @property def monitoring_capability(self): diff --git a/models/topology.py b/models/topology.py index 54a7f1e..5c46ec5 100644 --- a/models/topology.py +++ b/models/topology.py @@ -12,9 +12,10 @@ import pprint import re # noqa: F401 - import six +from parsing.servicehandler import ServiceHandler + GLOBAL_INSTITUTION_ID = 'urn:ogf:networking:global' class Topology(object): @@ -59,7 +60,7 @@ def __init__(self, id=None, name=None, domain_service=None, version=None, time_s self._id = id self._name = name if domain_service is not None: - self._domain_service = domain_service + self._domain_service = self.set_domain_service(domain_service) self._version = version self._time_stamp = time_stamp self._nodes = nodes @@ -113,8 +114,7 @@ def name(self, name): self._name = name - @property - def domain_service(self): + def get_domain_service(self): """Gets the domain_service of this Topology. # noqa: E501 @@ -123,16 +123,21 @@ def domain_service(self): """ return self._domain_service - @domain_service.setter - def domain_service(self, domain_service): + def set_domain_service(self, domain_service): """Sets the domain_service of this Topology. :param domain_service: The domain_service of this Topology. # noqa: E501 :type: Service """ + if domain_service is None: + raise ValueError("Invalid value for `domain_service`, must not be `None`") # noqa: E501 + + service_handler = ServiceHandler() + self._domain_service = service_handler.import_service_data(domain_service) + + return self.get_domain_service() - self._domain_service = domain_service @property def version(self): @@ -180,8 +185,7 @@ def time_stamp(self, time_stamp): self._time_stamp = time_stamp - @property - def nodes(self): + def get_nodes(self): """Gets the nodes of this Topology. # noqa: E501 @@ -190,8 +194,7 @@ def nodes(self): """ return self._nodes - @nodes.setter - def nodes(self, nodes): + def set_nodes(self, nodes): """Sets the nodes of this Topology. diff --git a/parsing/servicehandler.py b/parsing/servicehandler.py index a7eb80c..14258d0 100644 --- a/parsing/servicehandler.py +++ b/parsing/servicehandler.py @@ -14,17 +14,23 @@ def __init__(self): def import_service_data(self, data): try: - id = data['id'] - name=data['name'] - start=data['start_time'] - end=data['end_time'] - ingress=data['ingress_port'] - egress=data['egress_port'] + owner=data['owner'] + m_c=None;p_s=None;p_url=None;vendor=None;p_a=None + if 'monitoring_capability' in data.keys(): + m_c = data['monitoring_capability'] + if 'provisioning_system' in data.keys(): + p_s=data['provisioning_system'] + if 'provisioning_url' in data.keys(): + p_url=data['provisioning_url'] + if 'vendor' in data.keys(): + vendor=data['vendor'] + if 'private_attributes' in data.keys(): + p_a=data['private_attributes'] except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) - service=Service(id=id, name=name, start_time = start, end_time = end, ingress_port = ingress, egress_port = egress) - + service=Service(monitoring_capability=m_c, owner=owner, private_attributes=p_a, provisioning_system=p_s, provisioning_url=p_url, vendor=vendor) + return service def import_service(self,file): diff --git a/validation/connectionvalidator.py b/validation/connectionvalidator.py index aa3e420..d93d679 100644 --- a/validation/connectionvalidator.py +++ b/validation/connectionvalidator.py @@ -9,6 +9,8 @@ ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' +ISO_TIME_FORMAT = r"(^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:Z|[+-][01]\d:[0-5]\d)$)" + class ConnectionValidator(): """ The validation class made to validate a Connection request @@ -100,7 +102,7 @@ def _validate_time(self, time: str, conn: Connection): :return: A list of any issues in the data. """ errors = [] - if not match(ISO_FORMAT, time): + if not match(ISO_TIME_FORMAT, time): errors.append( '{} time needs to be in full ISO format'.format( time, diff --git a/validation/topologyvalidator.py b/validation/topologyvalidator.py index 88bfed1..e5f3810 100644 --- a/validation/topologyvalidator.py +++ b/validation/topologyvalidator.py @@ -54,9 +54,9 @@ def _validate_topology(self, topology: Topology): if GLOBAL_INSTITUTION_ID not in topology.id: errors.append('Global Institution must be in Topology {}'.format(topology.id)) - for service in topology.domain_service: - errors += self._validate_service(service, topology) - for node in topology.nodes: + service = topology.get_domain_service() + errors += self._validate_service(service, topology) + for node in topology.get_nodes(): errors += self._validate_node(node, topology) for link in topology.links: errors += self._validate_link(link, topology) @@ -82,12 +82,12 @@ def _validate_service(self, service: Service, topology: Topology): def _validate_version(self, version, time_stamp, topology: Topology): """ - Validate that the institution provided meets the XSD standards. - A institution must have the following: + Validate that the version and time_stamp provided meets the ISO standards. - It must meet object default standards. - It's Location values are valid - The Institution Type must be in the list of valid Institution types - :param institution: The Institution being evaluated. + :param version: The topology version. + :param time_stamp: The topology time stamp. :param topology: The Parent Topology. :return: A list of any issues in the data. """ @@ -126,7 +126,8 @@ def _validate_node(self, node: Node, topology: Topology): :return: A list of any issues in the data. """ errors = [] - errors += self._validate_object_defaults(node, topology) + print(node) + errors += self._validate_object_defaults(node) errors += self._validate_location(node) return errors From 8b562fc03c8f45cb13189ddb3ee2d75a5edcd600 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Tue, 17 Aug 2021 12:01:52 -0400 Subject: [PATCH 11/67] added location validation and completed nodes and links validation --- models/location.py | 8 +++-- models/node.py | 24 +++++++++---- models/topology.py | 57 ++++++++++++++++++++++++++----- parsing/linkhandler.py | 25 +++++++++----- parsing/locationhandler.py | 37 ++++++++++++++++++++ parsing/nodehandler.py | 6 ++-- test/test_connection_validator.py | 1 + validation/connectionvalidator.py | 4 +-- validation/topologyvalidator.py | 56 +++++++++++++----------------- 9 files changed, 155 insertions(+), 63 deletions(-) create mode 100644 parsing/locationhandler.py diff --git a/models/location.py b/models/location.py index a9667df..e44556e 100644 --- a/models/location.py +++ b/models/location.py @@ -41,16 +41,18 @@ class Location(object): def __init__(self, address=None, latitude=None, longitude=None): # noqa: E501 """Location - a model defined in Swagger""" # noqa: E501 + self._id="location" + self._name="location" self._address = None self._latitude = None self._longitude = None self.discriminator = None if address is not None: - self.address = address + self._address = address if latitude is not None: - self.latitude = latitude + self._latitude = latitude if longitude is not None: - self.longitude = longitude + self._longitude = longitude @property def address(self): diff --git a/models/node.py b/models/node.py index b299c44..2c66763 100644 --- a/models/node.py +++ b/models/node.py @@ -12,9 +12,10 @@ import pprint import re # noqa: F401 - import six +from parsing.locationhandler import LocationHandler + class Node(object): """NOTE: This class is auto generated by the swagger code generator program. @@ -50,14 +51,13 @@ def __init__(self, id=None, name=None, short_name=None, location=None, ports=Non self._id = None self._name = None self._short_name = None - self._location = None self._ports = None self._private_attributes = None self._id = id self._name = name if short_name is not None: self._short_name = short_name - self._location = location + self._location = self.set_location(location) self._ports = ports if private_attributes is not None: self._private_attributes = private_attributes @@ -131,6 +131,15 @@ def short_name(self, short_name): @property def location(self): + """Gets the name of this Node. # noqa: E501 + + + :return: The name of this Node. # noqa: E501 + :rtype: str + """ + return self._location + + def get_location(self): """Gets the location of this Node. # noqa: E501 @@ -139,18 +148,19 @@ def location(self): """ return self._location - @location.setter - def location(self, location): + def set_location(self, location): """Sets the location of this Node. - :param location: The location of this Node. # noqa: E501 :type: Location """ if location is None: raise ValueError("Invalid value for `location`, must not be `None`") # noqa: E501 - self._location = location + location_handler = LocationHandler() + self._location = location_handler.import_location_data(location) + + return self.get_location() @property def ports(self): diff --git a/models/topology.py b/models/topology.py index 5c46ec5..7e47c36 100644 --- a/models/topology.py +++ b/models/topology.py @@ -15,6 +15,8 @@ import six from parsing.servicehandler import ServiceHandler +from parsing.nodehandler import NodeHandler +from parsing.linkhandler import LinkHandler GLOBAL_INSTITUTION_ID = 'urn:ogf:networking:global' @@ -63,8 +65,10 @@ def __init__(self, id=None, name=None, domain_service=None, version=None, time_s self._domain_service = self.set_domain_service(domain_service) self._version = version self._time_stamp = time_stamp - self._nodes = nodes - self._links = links + self._nodes=[] + self._links=[] + self._nodes = self.set_nodes(nodes) + self._links = self.set_links(links) if private_attributes is not None: self._private_attributes = private_attributes @@ -114,6 +118,16 @@ def name(self, name): self._name = name + @property + def domain_service(self): + """Gets the name of this Topology. # noqa: E501 + + + :return: The name of this Topology. # noqa: E501 + :rtype: str + """ + return self._domain_service + def get_domain_service(self): """Gets the domain_service of this Topology. # noqa: E501 @@ -185,6 +199,16 @@ def time_stamp(self, time_stamp): self._time_stamp = time_stamp + @property + def nodes(self): + """Gets the name of this Topology. # noqa: E501 + + + :return: The name of this Topology. # noqa: E501 + :rtype: str + """ + return self._nodes + def get_nodes(self): """Gets the nodes of this Topology. # noqa: E501 @@ -204,10 +228,24 @@ def set_nodes(self, nodes): if nodes is None: raise ValueError("Invalid value for `nodes`, must not be `None`") # noqa: E501 - self._nodes = nodes + for node in nodes: + node_handler = NodeHandler() + node_obj = node_handler.import_node_data(node) + self._nodes.append(node_obj) + + return self.get_nodes() @property def links(self): + """Gets the name of this Topology. # noqa: E501 + + + :return: The name of this Topology. # noqa: E501 + :rtype: str + """ + return self._links + + def get_links(self): """Gets the links of this Topology. # noqa: E501 @@ -216,18 +254,21 @@ def links(self): """ return self._links - @links.setter - def links(self, links): + def set_links(self, links): """Sets the links of this Topology. - - :param links: The links of this Topology. # noqa: E501 + :param links: The links of this Topology, in list of JSON str. # noqa: E501 :type: list[Link] """ if links is None: raise ValueError("Invalid value for `links`, must not be `None`") # noqa: E501 - self._links = links + for link in links: + link_handler = LinkHandler() + link_obj = link_handler.import_link_data(link) + self._links.append(link_obj) + + return self.get_links() @property def private_attributes(self): diff --git a/parsing/linkhandler.py b/parsing/linkhandler.py index 2b7b78c..1ffc931 100644 --- a/parsing/linkhandler.py +++ b/parsing/linkhandler.py @@ -18,20 +18,29 @@ def import_link_data(self, data): name=data['name'] short_name=data['short_name'] ports=data['ports'] - timestamp=data['time_stamp'] + + timestamp=None;t_b=None;a_b=None;latency=None;p_l=None;p_a=None;avai=None;m_p=None + if 'time_stamp' in data.keys(): + timestamp=data['time_stamp'] if 'total_bandwidth' in data.keys(): t_b=data['total_bandwidth'] - a_b=data['available_bandwidth'] - latency=data['latency'] - p_l=data['packet_loss'] - p_a=data['private_attributes'] - avai=data['availability'] - m_p=data['measurement_period'] + if 'available_bandwidth' in data.keys(): + a_b=data['available_bandwidth'] + if 'latency' in data.keys(): + latency=data['latency'] + if 'packet_loss' in data.keys(): + p_l=data['packet_loss'] + if 'private_attributes' in data.keys(): + p_a=data['private_attributes'] + if 'availability' in data.keys(): + avai=data['availability'] + if 'measurement_period' in data.keys(): + m_p=data['measurement_period'] except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) link=Link(id=id, name=name, short_name=short_name, ports=ports, total_bandwidth=t_b, available_bandwidth=a_b, - latency=latency, packet_loss=p_l, availability=avai, private_attributes=p_a, time_stamp=timestamp, measurement_period=None) + latency=latency, packet_loss=p_l, availability=avai, private_attributes=p_a, time_stamp=timestamp, measurement_period=m_p) return link diff --git a/parsing/locationhandler.py b/parsing/locationhandler.py new file mode 100644 index 0000000..e1d6e67 --- /dev/null +++ b/parsing/locationhandler.py @@ -0,0 +1,37 @@ +import json +from models.location import Location +from .exceptions import MissingAttributeException + +class LocationHandler(): + + """" + Handler for parsing the connection request descritpion in json + """ + + def __init__(self): + super().__init__() + self.location = None + + def import_location_data(self, data): + try: + addr=None;long=None;lat=None + if 'address' in data.keys(): + addr = data['address'] + if 'latitude' in data.keys(): + long=data['latitude'] + if 'longitude' in data.keys(): + lat=data['longitude'] + except KeyError as e: + raise MissingAttributeException(e.args[0],e.args[0]) + + location=Location(address=addr, longitude=long, latitude=lat) + + return location + + def import_location(self,file): + with open(file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.location = self.import_location_data(data) + + def get_location(): + return self.location diff --git a/parsing/nodehandler.py b/parsing/nodehandler.py index 0c6db9d..e7ce39f 100644 --- a/parsing/nodehandler.py +++ b/parsing/nodehandler.py @@ -19,11 +19,13 @@ def import_node_data(self, data): short_name=data['short_name'] location=data['location'] ports=data['ports'] - private_attributes=data['private_attributes'] + p_a=None + if 'private_attributes' in data.keys(): + p_a=data['private_attributes'] except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) - node=Node(id=id, name=name, short_name=short_name, location=location, ports=ports, private_attributes=private_attributes) + node=Node(id=id, name=name, short_name=short_name, location=location, ports=ports, private_attributes=p_a) return node diff --git a/test/test_connection_validator.py b/test/test_connection_validator.py index 976616f..191cfbb 100644 --- a/test/test_connection_validator.py +++ b/test/test_connection_validator.py @@ -24,6 +24,7 @@ def tearDown(self): def testConnection(self): try: self.validator.is_valid() + print(self.validator.get_connection()) except DataModelException as e: print(e) return False diff --git a/validation/connectionvalidator.py b/validation/connectionvalidator.py index d93d679..ba16407 100644 --- a/validation/connectionvalidator.py +++ b/validation/connectionvalidator.py @@ -57,9 +57,9 @@ def _validate_connection(self, conn: Connection): errors += self._validate_port(conn.egress_port, conn) - errors += self._validate_time(conn.start_time, conn) + #errors += self._validate_time(conn.start_time, conn) - errors += self._validate_time(conn.end_time, conn) + #errors += self._validate_time(conn.end_time, conn) return errors def _validate_port(self, port: Port, conn: Connection): diff --git a/validation/topologyvalidator.py b/validation/topologyvalidator.py index e5f3810..a096de0 100644 --- a/validation/topologyvalidator.py +++ b/validation/topologyvalidator.py @@ -58,10 +58,9 @@ def _validate_topology(self, topology: Topology): errors += self._validate_service(service, topology) for node in topology.get_nodes(): errors += self._validate_node(node, topology) - for link in topology.links: + for link in topology.get_links(): errors += self._validate_link(link, topology) - for sub_topology in topology.topologies: - errors += self._validate_topology(sub_topology, topology) + return errors def _validate_service(self, service: Service, topology: Topology): @@ -128,7 +127,7 @@ def _validate_node(self, node: Node, topology: Topology): errors = [] print(node) errors += self._validate_object_defaults(node) - errors += self._validate_location(node) + errors += self._validate_location(node.get_location()) return errors @@ -144,27 +143,23 @@ def _validate_link(self, link: Link, topology: Topology): :return: A list of any issues in the data. """ errors = [] - errors += self._validate_object_defaults(link, topology) + errors += self._validate_object_defaults(link) - if len(link._nodes) != 2: + if len(link._ports) != 2: errors.append( - 'Link {} must connect between 2 Nodes. Currently {}'.format( - link.id, str(link._nodes) + 'Link {} must connect between 2 ports. Currently {}'.format( + link.id, str(link._ports) ) ) - for node in link._nodes: - if not isinstance(node, str): + for port in link._ports: + if not isinstance(port, str): errors.append( - 'Link {} Node {} should be a string. Not {}'.format( - link.id, node, node.__class__.__name__ - ) - ) - if topology and node not in topology.nodes: - errors.append( - 'Link {} listed node id {} does not exist in parent Topology {}'.format( - link.id, node, topology.id + 'Link {} Port {} should be a string. Not {}'.format( + link.id, port, port.__class__.__name__ ) ) + #TODO: Check ports are in the current topology + return errors def _validate_object_defaults(self, sdx_object): @@ -262,8 +257,8 @@ def _validate_location(self, location: Location, enforce_coordinates=True): ) try: - if location.altitude: - float(location.altitude) + if location.latitude: + float(location.latitude) except ValueError: errors.append( '{} {} Altitude must be a value that coordinates to a Floating point value'.format( @@ -271,22 +266,17 @@ def _validate_location(self, location: Location, enforce_coordinates=True): ) ) - if location.unlocode and not isinstance(location.unlocode, str): + if not location.address: errors.append( - '{} {} UN/LOCODE must be a string value'.format(location.__class__.__name__, location.id) + '{} {} Address must exist'.format( + location.__class__.__name__, location._id + ) ) - if isinstance(location.addresses, Iterable): - for address in location.addresses: - if not type(address) == str: - errors.append( - '{} {} Address {} must be a string'.format( - location.__class__.__name__, location.id, address - ) - ) - else: + if not type(location.address) == str: errors.append( - '{} {} Addresses should be a list of strings'.format( - location.__class__.__name__, location.id + '{} {} Address {} must be a string'.format( + location.__class__.__name__, location._id, location.address ) ) + return errors \ No newline at end of file From 485ca22a373c3021f4ac0c56bd8ad12de87d6ba9 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Tue, 17 Aug 2021 23:45:22 -0400 Subject: [PATCH 12/67] add more unittest cases --- models/__init__.py | 10 --------- test/{ => data}/amlight.json | 0 test/data/link.json | 34 +++++++++++++++++++++++++++++++ test/data/location.json | 5 +++++ test/data/node.json | 26 +++++++++++++++++++++++ test/{ => data}/p2p.json | 0 test/data/port.json | 11 ++++++++++ test/data/service.json | 3 +++ test/test_connection_handler.py | 2 +- test/test_connection_validator.py | 2 +- test/test_link_handler.py | 28 +++++++++++++++++++++++++ test/test_location_handler.py | 28 +++++++++++++++++++++++++ test/test_node_handler.py | 28 +++++++++++++++++++++++++ test/test_port_handler.py | 28 +++++++++++++++++++++++++ test/test_service_handler.py | 28 +++++++++++++++++++++++++ test/test_topology_handler.py | 2 +- test/test_topology_validator.py | 2 +- 17 files changed, 223 insertions(+), 14 deletions(-) rename test/{ => data}/amlight.json (100%) create mode 100644 test/data/link.json create mode 100644 test/data/location.json create mode 100644 test/data/node.json rename test/{ => data}/p2p.json (100%) create mode 100644 test/data/port.json create mode 100644 test/data/service.json create mode 100644 test/test_link_handler.py create mode 100644 test/test_location_handler.py create mode 100644 test/test_node_handler.py create mode 100644 test/test_port_handler.py create mode 100644 test/test_service_handler.py diff --git a/models/__init__.py b/models/__init__.py index a179732..f7fff13 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -12,13 +12,3 @@ """ from __future__ import absolute_import - -# import models into model package -from .connection import Connection -from .link import Link -from .link_measurement_period import LinkMeasurementPeriod -from .location import Location -from .node import Node -from .port import Port -from .service import Service -from .topology import Topology diff --git a/test/amlight.json b/test/data/amlight.json similarity index 100% rename from test/amlight.json rename to test/data/amlight.json diff --git a/test/data/link.json b/test/data/link.json new file mode 100644 index 0000000..912531c --- /dev/null +++ b/test/data/link.json @@ -0,0 +1,34 @@ +{ + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "id", + "latency": 146582.15146899645, + "name": "miami-Boca.amLight.sdx", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:10", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "10", + "status": "up" + } + ], + "short_name": "Miami-BocaRaton", + "total_bandwidth": 80083.7389632821 + } \ No newline at end of file diff --git a/test/data/location.json b/test/data/location.json new file mode 100644 index 0000000..104fbf1 --- /dev/null +++ b/test/data/location.json @@ -0,0 +1,5 @@ +{ + "address": "Miami", + "latitude": -28.51107891831147, + "longitude": -79.57947854792273 + } \ No newline at end of file diff --git a/test/data/node.json b/test/data/node.json new file mode 100644 index 0000000..ffc4665 --- /dev/null +++ b/test/data/node.json @@ -0,0 +1,26 @@ +{ + "id": "id", + "location": { + "address": "Miami", + "latitude": -28.51107891831147, + "longitude": -79.57947854792273 + }, + "name": "urn:sdx:node:amlight.net:Novi06", + "ports": [ + { + "id": "id", + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "id", + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + } + ], + "short_name": "Novi06" + } \ No newline at end of file diff --git a/test/p2p.json b/test/data/p2p.json similarity index 100% rename from test/p2p.json rename to test/data/p2p.json diff --git a/test/data/port.json b/test/data/port.json new file mode 100644 index 0000000..2604a60 --- /dev/null +++ b/test/data/port.json @@ -0,0 +1,11 @@ +{ + "id": "id", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + } \ No newline at end of file diff --git a/test/data/service.json b/test/data/service.json new file mode 100644 index 0000000..d796683 --- /dev/null +++ b/test/data/service.json @@ -0,0 +1,3 @@ +{ + "owner":"FIU" +} \ No newline at end of file diff --git a/test/test_connection_handler.py b/test/test_connection_handler.py index 8fc20f5..60768a6 100644 --- a/test/test_connection_handler.py +++ b/test/test_connection_handler.py @@ -5,7 +5,7 @@ from parsing.connectionhandler import ConnectionHandler from parsing.exceptions import DataModelException -CONNECTION_P2P = './test/p2p.json' +CONNECTION_P2P = './test/data/p2p.json' class TestConnectionHandler(unittest.TestCase): diff --git a/test/test_connection_validator.py b/test/test_connection_validator.py index 191cfbb..9c25c35 100644 --- a/test/test_connection_validator.py +++ b/test/test_connection_validator.py @@ -6,7 +6,7 @@ from parsing.connectionhandler import ConnectionHandler from parsing.exceptions import DataModelException -CONNECTION_P2P = './test/p2p.json' +CONNECTION_P2P = './test/data/p2p.json' class TestConnectionValidator(unittest.TestCase): diff --git a/test/test_link_handler.py b/test/test_link_handler.py new file mode 100644 index 0000000..6a59d5e --- /dev/null +++ b/test/test_link_handler.py @@ -0,0 +1,28 @@ +import unittest + +import parsing + +from parsing.linkhandler import LinkHandler +from parsing.exceptions import DataModelException + +link = './test/data/link.json' + +class TestLinkHandler(unittest.TestCase): + + def setUp(self): + self.handler = LinkHandler() # noqa: E501 + def tearDown(self): + pass + + def testImportLink(self): + try: + print("Test Link") + self.handler.import_link(link) + print(self.handler.link) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_location_handler.py b/test/test_location_handler.py new file mode 100644 index 0000000..1644dc3 --- /dev/null +++ b/test/test_location_handler.py @@ -0,0 +1,28 @@ +import unittest + +import parsing + +from parsing.locationhandler import LocationHandler +from parsing.exceptions import DataModelException + +location = './test/data/location.json' + +class TestPortHandler(unittest.TestCase): + + def setUp(self): + self.handler = LocationHandler() # noqa: E501 + def tearDown(self): + pass + + def testImportLocation(self): + try: + print("Test Location") + self.handler.import_location(location) + print(self.handler.location) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_node_handler.py b/test/test_node_handler.py new file mode 100644 index 0000000..4f2b3b0 --- /dev/null +++ b/test/test_node_handler.py @@ -0,0 +1,28 @@ +import unittest + +import parsing + +from parsing.nodehandler import NodeHandler +from parsing.exceptions import DataModelException + +node = './test/data/node.json' + +class TestNodeHandler(unittest.TestCase): + + def setUp(self): + self.handler = NodeHandler() # noqa: E501 + def tearDown(self): + pass + + def testImportNode(self): + try: + print("Test node") + self.handler.import_node(node) + print(self.handler.node) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_port_handler.py b/test/test_port_handler.py new file mode 100644 index 0000000..e6d53d5 --- /dev/null +++ b/test/test_port_handler.py @@ -0,0 +1,28 @@ +import unittest + +import parsing + +from parsing.porthandler import PortHandler +from parsing.exceptions import DataModelException + +port = './test/data/port.json' + +class TestPortHandler(unittest.TestCase): + + def setUp(self): + self.handler = PortHandler() # noqa: E501 + def tearDown(self): + pass + + def testImportPort(self): + try: + print("Test Port") + self.handler.import_port(port) + print(self.handler.port) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_service_handler.py b/test/test_service_handler.py new file mode 100644 index 0000000..6c9b28c --- /dev/null +++ b/test/test_service_handler.py @@ -0,0 +1,28 @@ +import unittest + +import parsing + +from parsing.servicehandler import ServiceHandler +from parsing.exceptions import DataModelException + +service = './test/data/service.json' + +class TestServiceHandler(unittest.TestCase): + + def setUp(self): + self.handler = ServiceHandler() # noqa: E501 + def tearDown(self): + pass + + def testImportService(self): + try: + print("Test Service") + self.handler.import_service(service) + print(self.handler.service) + except DataModelException as e: + print(e) + return False + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_topology_handler.py b/test/test_topology_handler.py index becf473..d708d5e 100644 --- a/test/test_topology_handler.py +++ b/test/test_topology_handler.py @@ -5,7 +5,7 @@ from parsing.topologyhandler import TopologyHandler from parsing.exceptions import DataModelException -TOPOLOGY_AMLIGHT = './test/amlight.json' +TOPOLOGY_AMLIGHT = './test/data/amlight.json' class TestTopologyHandler(unittest.TestCase): diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index 710d4bf..00b7866 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -6,7 +6,7 @@ from parsing.topologyhandler import TopologyHandler from parsing.exceptions import DataModelException -TOPOLOGY_AMLIGHT = './test/amlight.json' +TOPOLOGY_AMLIGHT = './test/data/amlight.json' class TestTopologyValidator(unittest.TestCase): From 23e7a5ce5911fa9780d7bab1ac807da276db20c5 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Thu, 2 Sep 2021 16:06:26 -0400 Subject: [PATCH 13/67] grenml converter --- doc/requirements.txt | 101 ++++++++++++++++++++++++++++ models/link.py | 4 -- models/node.py | 24 ++++++- models/topology.py | 36 ++++++---- parsing/topologyhandler.py | 5 +- requirements.txt | 102 +---------------------------- test/data/ampath.json | 45 +++++++++++++ test/test_topology_manager.py | 39 +++++++++++ test/test_topology_validator.py | 3 +- topologymanager/__init__.py | 0 topologymanager/grenmlconverter.py | 45 +++++++++++++ topologymanager/manager.py | 51 +++++++++++++++ validation/topologyvalidator.py | 12 +++- 13 files changed, 345 insertions(+), 122 deletions(-) create mode 100644 doc/requirements.txt create mode 100644 test/data/ampath.json create mode 100644 test/test_topology_manager.py create mode 100644 topologymanager/__init__.py create mode 100644 topologymanager/grenmlconverter.py create mode 100644 topologymanager/manager.py diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..9b65feb --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,101 @@ +1. topology.json +{ + "type": "object", + "properties": { + "id": { // must be provided by SDX, not by OXPO// source IP of the OXPO? + "type": "string" + }, + "links": ["repository://"], // urn:sdx:link:amlight:novi01_2_novi02_2 + "name": "AmLight", + "nodes": ["repository://Node_novi01", "repository://Node_novi02", "repository://Node_novi03"], + "time_stamp": "06-31-2021-11:23:59", // is residual bandwidth going to change the timestamp? + "version": "1", // is residual bandwidth going to change the version? + "domain_service": { + "$ref": "repository://Service" + } + }, + "title": "Topology" +} + +Are "Links" for inter-domain as well? +how to specify the inter-domain connections? + +Each attribute must have a qualifier for private or public +how to push for updates (full topology, just the change, or just a notification of a change) + +Future: +topology capabilities: INT/JTI, counters + + +2. port.json +{ + "type": "object", + "properties": { + "id": "2", // ID used by the equipment (of_port or ifIndex) maybe URN? + "short_name": "???? What fields are mandatory or optional", + "name": "2", // "Examples: Juniper: xe-1/1/0, Brocade: eth2/5, Dell Hu-1/2" + "node": "repository://Node_novi01", + "type": "100GbpsE", + "encapsulation": "VLAN", // what's the difference between label and encapsulation? + "label": "vlan", + "swapping_capability": { + "type": "string", + "enum": [ + "vlan" // ? + ] + }, + "label_range": [[1, 5], [100, 200], [34, 34]], + // mtu is desired because of INT and VXLAN + "inter-domain": "SAX:Switch1:Port2", // full URN + "inter-domain": "sfsadfsadfdsfgsdfgfdsgfsdgfdsgfsd", // shared string + // description + "status": "?????? // state and status could be multivalue. Boolean not enough [provisioning|decom|maintenance|up|down|optimal|suboptimal]" + }, + "title": "Port" +} + +3. link.json +{ + "type": "object", + "properties": { + "id": "urn:sdx:link:amlight:novi01_2_novi02_2", + "short_name": "", + "name": "novi01_2_novi02_2", + "ports": ["repository://Port_novi01_02", "repository://Port_novi02_02"], + "total_bandwidth": // no an SDX issue. I suggest removing it. + { //unit bps? minimum and maximum must be conditioned to the interface.type (10GE for instance) + "type": "number", + "minimum": 1, + "maximum": 1000000 + }, + "bandwidth": { // 10Mbps - 1Tbps + "type": "number", + "minimum": 0, + "maximum": 1000000 // no max, just > 0 + }, + "latency": { // ns or ms? + "type": "number", + "minimum": 0, + "maximum": 1000000 // no max, just > 0 + }, + "packet loss": { // % + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "availability": { // % + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "residual_bw": { // based on the last X time? what is X? // array with last 30s, last 5min, last hour, last day, last week + "type": "number", + "minimum": 0, + "maximum": 100 + } + }, + "title": "Link" +} + +4. node.json + diff --git a/models/link.py b/models/link.py index 2394eec..91407ce 100644 --- a/models/link.py +++ b/models/link.py @@ -16,10 +16,6 @@ import six class Link(object): - """NOTE: This class is auto generated by the swagger code generator program. - - Do not edit the class manually. - """ """ Attributes: swagger_types (dict): The key is attribute name diff --git a/models/node.py b/models/node.py index 2c66763..d5b677f 100644 --- a/models/node.py +++ b/models/node.py @@ -15,6 +15,7 @@ import six from parsing.locationhandler import LocationHandler +from parsing.porthandler import PortHandler class Node(object): """NOTE: This class is auto generated by the swagger code generator program. @@ -58,7 +59,7 @@ def __init__(self, id=None, name=None, short_name=None, location=None, ports=Non if short_name is not None: self._short_name = short_name self._location = self.set_location(location) - self._ports = ports + self._ports = self.set_ports(ports) if private_attributes is not None: self._private_attributes = private_attributes @@ -172,6 +173,26 @@ def ports(self): """ return self._ports + def set_ports(self, ports): + """Sets the ports of this Node. + + + :param ports: The ports of this Node. # noqa: E501 + :type: list[port] + """ + if ports is None: + raise ValueError("Invalid value for `ports`, must not be `None`") # noqa: E501 + + if self.ports is None: + self._ports=[] + + for port in ports: + port_handler = PortHandler() + port_obj = port_handler.import_port_data(port) + self._ports.append(port_obj) + + return self.ports + @ports.setter def ports(self, ports): """Sets the ports of this Node. @@ -185,6 +206,7 @@ def ports(self, ports): self._ports = ports + @property def private_attributes(self): """Gets the private_attributes of this Node. # noqa: E501 diff --git a/models/topology.py b/models/topology.py index 7e47c36..ce762d8 100644 --- a/models/topology.py +++ b/models/topology.py @@ -21,17 +21,7 @@ GLOBAL_INSTITUTION_ID = 'urn:ogf:networking:global' class Topology(object): - """NOTE: This class is auto generated by the swagger code generator program. - - Do not edit the class manually. - """ - """ - Attributes: - swagger_types (dict): The key is attribute name - and the value is attribute type. - attribute_map (dict): The key is attribute name - and the value is json key in definition. - """ + swagger_types = { 'id': 'str', 'name': 'str', @@ -235,6 +225,23 @@ def set_nodes(self, nodes): return self.get_nodes() + def add_nodes(self, node_objects): + """add the nodes to this Topology. + :param node_objects: a list of node objects + """ + + self._nodes.extend(node_objects) + + def get_node_by_port(self,port): + for node in self.get_nodes: + ports = node.ports + for port in ports: + if port.id == port: + return node + + return None + + @property def links(self): """Gets the name of this Topology. # noqa: E501 @@ -270,6 +277,13 @@ def set_links(self, links): return self.get_links() + def add_links(self, link_objects): + """add the links to this Topology. + :param link_objects: a list of link objects + """ + + self._nodes.extend(link_objects) + @property def private_attributes(self): """Gets the private_attributes of this Topology. # noqa: E501 diff --git a/parsing/topologyhandler.py b/parsing/topologyhandler.py index dc3a4cb..52e3c38 100644 --- a/parsing/topologyhandler.py +++ b/parsing/topologyhandler.py @@ -10,11 +10,14 @@ class TopologyHandler(): Handler for parsing the topology descritpion in json """ - def __init__(self,topology_filename): + def __init__(self,topology_filename=None): super().__init__() self.topology_file = topology_filename self.topology = None + def topology_file(self,topology_filename=None): + self.topology_file = topology_filename + def import_topology(self): with open(self.topology_file, 'r', encoding='utf-8') as data_file: data = json.load(data_file) diff --git a/requirements.txt b/requirements.txt index 9b65feb..2fd0d25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,101 +1 @@ -1. topology.json -{ - "type": "object", - "properties": { - "id": { // must be provided by SDX, not by OXPO// source IP of the OXPO? - "type": "string" - }, - "links": ["repository://"], // urn:sdx:link:amlight:novi01_2_novi02_2 - "name": "AmLight", - "nodes": ["repository://Node_novi01", "repository://Node_novi02", "repository://Node_novi03"], - "time_stamp": "06-31-2021-11:23:59", // is residual bandwidth going to change the timestamp? - "version": "1", // is residual bandwidth going to change the version? - "domain_service": { - "$ref": "repository://Service" - } - }, - "title": "Topology" -} - -Are "Links" for inter-domain as well? -how to specify the inter-domain connections? - -Each attribute must have a qualifier for private or public -how to push for updates (full topology, just the change, or just a notification of a change) - -Future: -topology capabilities: INT/JTI, counters - - -2. port.json -{ - "type": "object", - "properties": { - "id": "2", // ID used by the equipment (of_port or ifIndex) maybe URN? - "short_name": "???? What fields are mandatory or optional", - "name": "2", // "Examples: Juniper: xe-1/1/0, Brocade: eth2/5, Dell Hu-1/2" - "node": "repository://Node_novi01", - "type": "100GbpsE", - "encapsulation": "VLAN", // what's the difference between label and encapsulation? - "label": "vlan", - "swapping_capability": { - "type": "string", - "enum": [ - "vlan" // ? - ] - }, - "label_range": [[1, 5], [100, 200], [34, 34]], - // mtu is desired because of INT and VXLAN - "inter-domain": "SAX:Switch1:Port2", // full URN - "inter-domain": "sfsadfsadfdsfgsdfgfdsgfsdgfdsgfsd", // shared string - // description - "status": "?????? // state and status could be multivalue. Boolean not enough [provisioning|decom|maintenance|up|down|optimal|suboptimal]" - }, - "title": "Port" -} - -3. link.json -{ - "type": "object", - "properties": { - "id": "urn:sdx:link:amlight:novi01_2_novi02_2", - "short_name": "", - "name": "novi01_2_novi02_2", - "ports": ["repository://Port_novi01_02", "repository://Port_novi02_02"], - "total_bandwidth": // no an SDX issue. I suggest removing it. - { //unit bps? minimum and maximum must be conditioned to the interface.type (10GE for instance) - "type": "number", - "minimum": 1, - "maximum": 1000000 - }, - "bandwidth": { // 10Mbps - 1Tbps - "type": "number", - "minimum": 0, - "maximum": 1000000 // no max, just > 0 - }, - "latency": { // ns or ms? - "type": "number", - "minimum": 0, - "maximum": 1000000 // no max, just > 0 - }, - "packet loss": { // % - "type": "number", - "minimum": 0, - "maximum": 100 - }, - "availability": { // % - "type": "number", - "minimum": 0, - "maximum": 100 - }, - "residual_bw": { // based on the last X time? what is X? // array with last 30s, last 5min, last hour, last day, last week - "type": "number", - "minimum": 0, - "maximum": 100 - } - }, - "title": "Link" -} - -4. node.json - +grenml diff --git a/test/data/ampath.json b/test/data/ampath.json new file mode 100644 index 0000000..5133e44 --- /dev/null +++ b/test/data/ampath.json @@ -0,0 +1,45 @@ +{ + "id": "urn:sdx:topology:", + "links":[ + { + "id": "urn:sdx:link::Ampath5/177_JAX/249", + "name": "Ampath5/177_JAX/249", + "ports": [ + { + "id" : "urn:sdx:port::Ampath5:177" + }, + { + "id" : "urn:sdx:port::JAX:249" + } + ] + } + ], + "model_version": "1.0.0", + "name": "AmLight-OXP", + "nodes": [ + { + "id": "urn:sdx:node::Ampath3", + "location": { + "address": "", + "latitude": "0.0", + "longitude": "-20.0" + }, + "name": "Ampath3", + "ports": [ + { + "id": "urn:sdx:port::Ampath3:149", + "mtu": "1500", + "name": "Ampath3-eth149", + "nni": "False", + "node": "urn:sdx:node::Ampath3", + "services": "l2vpn", + "state": "enabled", + "status": "up", + "type": "10GE" + } + ] + } + ], + "timestamp": "20210811-214909", + "version": 2 +} diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py new file mode 100644 index 0000000..e59f07d --- /dev/null +++ b/test/test_topology_manager.py @@ -0,0 +1,39 @@ +import unittest + +import parsing +import topologymanager + +from topologymanager.manager import TopologyManager +from topologymanager.grenmlconverter import GrenmlConverter +from parsing.exceptions import DataModelException + + +TOPOLOGY_AMLIGHT = './test/data/amlight.json' + +class TestTopologyHandler(unittest.TestCase): + + def setUp(self): + self.manager = TopologyManager() # noqa: E501 + self.manager.handler.topology_file(TOPOLOGY_AMLIGHT) + self.manager.handler.import_topology() + + def tearDown(self): + pass + + def testMergeTopology(self): + try: + print("Test Topology") + print(self.handler.topology) + except DataModelException as e: + print(e) + return False + return True + + def testGrenmlConverter(self): + try: + print("Test Topology") + print(self.handler.topology) + except DataModelException as e: + print(e) + return False + return True \ No newline at end of file diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index 00b7866..f7610d1 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -7,11 +7,12 @@ from parsing.exceptions import DataModelException TOPOLOGY_AMLIGHT = './test/data/amlight.json' +TOPOLOGY_AMPATH = './test/data/ampath.json' class TestTopologyValidator(unittest.TestCase): def setUp(self): - self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) + self.handler = TopologyHandler(TOPOLOGY_AMPATH) self.validator = TopologyValidator() def tearDown(self): diff --git a/topologymanager/__init__.py b/topologymanager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/topologymanager/grenmlconverter.py b/topologymanager/grenmlconverter.py new file mode 100644 index 0000000..0174755 --- /dev/null +++ b/topologymanager/grenmlconverter.py @@ -0,0 +1,45 @@ +import grenml + +from models.topology import Topology +from models.node import Node +from models.location import Location + +class GrenmlConverter(object): + + def __init__(self, topology:Topology): + self.topology=topology + self.grenml_manager = grenml.GRENMLManager(topology.name()) + + def read_topology(self): + domain_service = self.topology.get_domain_service() + owner = domain_service._owner + self.grenml_manager.set_primary_owner(owner) + + self.grenml_manager.add_institution(owner) + + self.add_nodes(self.topology.get_nodes()) + + self.add_links(self.topology.get_links()) + + self.topology_str = self.grenml_manager.write_to_string() + + def add_nodes(self,nodes): + for node in nodes: + location = node.get_location() + self.grenml_manager.add_node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address()) + + def add_links(self,links): + for link in links: + ports=link.ports + end_nodes=[] + for port in ports: + node=self.topology.find_node_by_port(port) + if node is not None: + end_nodes.append(node) + else: + print("This port doesn't belong to any node in the topology!") + + self.grenml_manager.add_link(link.id, link.name,link.short_name, nodes=end_nodes) + + def get_xml_str(self): + return self.topology_str \ No newline at end of file diff --git a/topologymanager/manager.py b/topologymanager/manager.py new file mode 100644 index 0000000..a3a57f2 --- /dev/null +++ b/topologymanager/manager.py @@ -0,0 +1,51 @@ + +import json +from models.topology import Topology + +import parsing +from parsing.topologyhandler import TopologyHandler +from parsing.exceptions import DataModelException + +class TopologyManager(): + + """" + Manager for topology operations: merge multiple topologies, convert to grenml (XML). + """ + + def __init__(self): + super().__init__() + + self.handler = TopologyHandler() + self.topology=Topology() + + def topology_id(self, id): + self.topology._id(id) + + def add_topology(self, data): + topology = self.handler.import_topology_data(data) + ##generate a new topology id + ## update the version and timestamp to be the latest + ##check the inter-domain links first. + self.inter_domain_check(topology) + ##nodes + nodes = topology.get_nodes() + self.topology.add_nodes(nodes) + ##links + links = topology.get_links() + self.topology.add_links(nodes) + + def remove_topology(self, data): + pass + + def update_topology(self,data): + pass + + def get_topology(): + return self.topology + + def inter_domain_check(topology): + pass + + + def generate_grenml(): + pass \ No newline at end of file diff --git a/validation/topologyvalidator.py b/validation/topologyvalidator.py index a096de0..7e8364b 100644 --- a/validation/topologyvalidator.py +++ b/validation/topologyvalidator.py @@ -3,10 +3,16 @@ Synopsis: A validation class to evaluate that the supplied Topology object contains expected data format """ from collections.abc import Iterable -from models import Topology, Service, Node, Link, Location -from models.topology import GLOBAL_INSTITUTION_ID from re import match +from models.topology import GLOBAL_INSTITUTION_ID, Topology +from models.service import Service +from models.node import Node +from models.link import Link +from models.port import Port +from models.location import Location + + ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' class TopologyValidator: @@ -125,7 +131,7 @@ def _validate_node(self, node: Node, topology: Topology): :return: A list of any issues in the data. """ errors = [] - print(node) + errors += self._validate_object_defaults(node) errors += self._validate_location(node.get_location()) From e757f8a7846635bc4212eb76ea36cfb6865bb224 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Thu, 2 Sep 2021 16:55:42 -0400 Subject: [PATCH 14/67] adding unittest for grenmlconverter --- models/topology.py | 6 ++-- parsing/topologyhandler.py | 2 +- test/data/amlight.json | 24 +++++++-------- test/data/ampath.json | 48 +++++++++++++++--------------- test/test_topology_manager.py | 16 ++++++---- test/test_topology_validator.py | 2 +- topologymanager/grenmlconverter.py | 23 +++++++++----- topologymanager/manager.py | 7 +++-- 8 files changed, 72 insertions(+), 56 deletions(-) diff --git a/models/topology.py b/models/topology.py index ce762d8..1d26115 100644 --- a/models/topology.py +++ b/models/topology.py @@ -232,11 +232,11 @@ def add_nodes(self, node_objects): self._nodes.extend(node_objects) - def get_node_by_port(self,port): - for node in self.get_nodes: + def get_node_by_port(self,aPort): + for node in self.nodes: ports = node.ports for port in ports: - if port.id == port: + if port.id == aPort['id']: return node return None diff --git a/parsing/topologyhandler.py b/parsing/topologyhandler.py index 52e3c38..ba8f98e 100644 --- a/parsing/topologyhandler.py +++ b/parsing/topologyhandler.py @@ -15,7 +15,7 @@ def __init__(self,topology_filename=None): self.topology_file = topology_filename self.topology = None - def topology_file(self,topology_filename=None): + def topology_file_name(self,topology_filename=None): self.topology_file = topology_filename def import_topology(self): diff --git a/test/data/amlight.json b/test/data/amlight.json index 173068c..1ae4011 100644 --- a/test/data/amlight.json +++ b/test/data/amlight.json @@ -4,13 +4,13 @@ { "availability": 56.37376656633328, "available_bandwidth": 602746.015561422, - "id": "id", + "id": "l-1", "latency": 146582.15146899645, "name": "miami-Boca.amLight.sdx", "packet_loss": 59.621339166831824, "ports": [ { - "id": "id", + "id": "p-1", "label_range": [ "label_range", "label_range" @@ -21,7 +21,7 @@ "status": "up" }, { - "id": "id", + "id": "p-2", "label_range": [ "label_range", "label_range" @@ -38,13 +38,13 @@ { "availability": 56.37376656633328, "available_bandwidth": 602746.015561422, - "id": "id", + "id": "l-2", "latency": 146582.15146899645, "name": "miami-Boca.amLight.sdx", "packet_loss": 59.621339166831824, "ports": [ { - "id": "id", + "id": "p-3", "label_range": [ "label_range", "label_range" @@ -55,7 +55,7 @@ "status": "up" }, { - "id": "id", + "id": "p-4", "label_range": [ "label_range", "label_range" @@ -73,7 +73,7 @@ "name": "amLight", "nodes": [ { - "id": "id", + "id": "1", "location": { "address": "Miami", "latitude": -28.51107891831147, @@ -82,14 +82,14 @@ "name": "urn:sdx:node:amlight.net:Novi06", "ports": [ { - "id": "id", + "id": "p-1", "name": "urn:sdx:port:amlight.net:Novi06:9", "node": "urn:sdx:port:amlight.net:Novi06", "short_name": "9", "status": "up" }, { - "id": "id", + "id": "p-3", "name": "urn:sdx:port:amlight.net:Novi06:9", "node": "urn:sdx:port:amlight.net:Novi06", "short_name": "9", @@ -99,7 +99,7 @@ "short_name": "Novi06" }, { - "id": "id", + "id": "2", "location": { "address": "Miami", "latitude": -28.51107891831147, @@ -108,7 +108,7 @@ "name": "urn:sdx:node:amlight.net:Novi06", "ports": [ { - "id": "id", + "id": "p-2", "label_range": [ "label_range", "label_range" @@ -119,7 +119,7 @@ "status": "up" }, { - "id": "id", + "id": "p-4", "label_range": [ "label_range", "label_range" diff --git a/test/data/ampath.json b/test/data/ampath.json index 5133e44..d2fbc94 100644 --- a/test/data/ampath.json +++ b/test/data/ampath.json @@ -2,9 +2,9 @@ "id": "urn:sdx:topology:", "links":[ { - "id": "urn:sdx:link::Ampath5/177_JAX/249", - "name": "Ampath5/177_JAX/249", - "ports": [ + "id":"urn:sdx:link::Ampath5/177_JAX/249", + "name":"Ampath5/177_JAX/249", + "ports":[ { "id" : "urn:sdx:port::Ampath5:177" }, @@ -14,32 +14,32 @@ ] } ], - "model_version": "1.0.0", - "name": "AmLight-OXP", - "nodes": [ + "model_version":"1.0.0", + "name":"AmLight-OXP", + "nodes":[ { - "id": "urn:sdx:node::Ampath3", - "location": { - "address": "", - "latitude": "0.0", - "longitude": "-20.0" + "id":"urn:sdx:node::Ampath3", + "location":{ + "address":"", + "latitude":"0.0", + "longitude":"-20.0" }, - "name": "Ampath3", - "ports": [ + "name":"Ampath3", + "ports":[ { - "id": "urn:sdx:port::Ampath3:149", - "mtu": "1500", - "name": "Ampath3-eth149", - "nni": "False", - "node": "urn:sdx:node::Ampath3", - "services": "l2vpn", - "state": "enabled", - "status": "up", - "type": "10GE" + "id":"urn:sdx:port::Ampath3:149", + "mtu":"1500", + "name":"Ampath3-eth149", + "nni":"False", + "node":"urn:sdx:node::Ampath3", + "services":"l2vpn", + "state":"enabled", + "status":"up", + "type":"10GE" } ] } ], - "timestamp": "20210811-214909", - "version": 2 + "timestamp":"20210811-214909", + "version":2 } diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py index e59f07d..d2587db 100644 --- a/test/test_topology_manager.py +++ b/test/test_topology_manager.py @@ -3,6 +3,8 @@ import parsing import topologymanager +from validation.topologyvalidator import TopologyValidator +from parsing.topologyhandler import TopologyHandler from topologymanager.manager import TopologyManager from topologymanager.grenmlconverter import GrenmlConverter from parsing.exceptions import DataModelException @@ -10,19 +12,20 @@ TOPOLOGY_AMLIGHT = './test/data/amlight.json' -class TestTopologyHandler(unittest.TestCase): +class TestTopologyManager(unittest.TestCase): def setUp(self): self.manager = TopologyManager() # noqa: E501 - self.manager.handler.topology_file(TOPOLOGY_AMLIGHT) - self.manager.handler.import_topology() + self.handler = self.manager.handler + self.handler.topology_file_name(TOPOLOGY_AMLIGHT) + self.handler.import_topology() def tearDown(self): pass def testMergeTopology(self): try: - print("Test Topology") + print("Test Topology Merge") print(self.handler.topology) except DataModelException as e: print(e) @@ -31,8 +34,11 @@ def testMergeTopology(self): def testGrenmlConverter(self): try: - print("Test Topology") + print("Test Topology Converter") print(self.handler.topology) + converter = GrenmlConverter(self.handler.topology) + converter.read_topology() + print(converter.get_xml_str) except DataModelException as e: print(e) return False diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index f7610d1..3110f22 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -12,7 +12,7 @@ class TestTopologyValidator(unittest.TestCase): def setUp(self): - self.handler = TopologyHandler(TOPOLOGY_AMPATH) + self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) self.validator = TopologyValidator() def tearDown(self): diff --git a/topologymanager/grenmlconverter.py b/topologymanager/grenmlconverter.py index 0174755..f3bee67 100644 --- a/topologymanager/grenmlconverter.py +++ b/topologymanager/grenmlconverter.py @@ -1,21 +1,23 @@ -import grenml +from grenml import GRENMLManager +from grenml.models.nodes import Node +from grenml.models.links import Link from models.topology import Topology -from models.node import Node +#from models.node import Node from models.location import Location class GrenmlConverter(object): def __init__(self, topology:Topology): self.topology=topology - self.grenml_manager = grenml.GRENMLManager(topology.name()) + self.grenml_manager = GRENMLManager(topology.name) def read_topology(self): domain_service = self.topology.get_domain_service() - owner = domain_service._owner + owner = domain_service.owner self.grenml_manager.set_primary_owner(owner) - self.grenml_manager.add_institution(owner) + self.grenml_manager.add_institution(owner,owner) self.add_nodes(self.topology.get_nodes()) @@ -23,19 +25,24 @@ def read_topology(self): self.topology_str = self.grenml_manager.write_to_string() + print(self.topology_str) + def add_nodes(self,nodes): for node in nodes: location = node.get_location() - self.grenml_manager.add_node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address()) + + self.grenml_manager.add_node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address) def add_links(self,links): for link in links: ports=link.ports end_nodes=[] for port in ports: - node=self.topology.find_node_by_port(port) + node=self.topology.get_node_by_port(port) if node is not None: - end_nodes.append(node) + location = node.get_location() + grenml_node = Node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address) + end_nodes.append(grenml_node ) else: print("This port doesn't belong to any node in the topology!") diff --git a/topologymanager/manager.py b/topologymanager/manager.py index a3a57f2..2ff3ad0 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -16,7 +16,10 @@ def __init__(self): super().__init__() self.handler = TopologyHandler() - self.topology=Topology() + #self.topology=Topology() + + def get_handler(self): + return self.handler def topology_id(self, id): self.topology._id(id) @@ -33,7 +36,7 @@ def add_topology(self, data): ##links links = topology.get_links() self.topology.add_links(nodes) - + def remove_topology(self, data): pass From 4a8dc0c1b083fba84220733de1fcd69ce5cf1512 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 3 Sep 2021 09:47:26 -0400 Subject: [PATCH 15/67] check in the topology merge --- topologymanager/manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/topologymanager/manager.py b/topologymanager/manager.py index 2ff3ad0..3572fb8 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -43,12 +43,12 @@ def remove_topology(self, data): def update_topology(self,data): pass - def get_topology(): + def get_topology(self): return self.topology - def inter_domain_check(topology): + def inter_domain_check(self,topology): pass - def generate_grenml(): + def generate_grenml(self): pass \ No newline at end of file From 80e46e45dc36a43cf4fa5be535d1d4c775975976 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Thu, 23 Sep 2021 17:48:51 -0400 Subject: [PATCH 16/67] inter-domain topology functions --- models/topology.py | 4 ++- parsing/topologyhandler.py | 8 ++--- topologymanager/manager.py | 61 ++++++++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/models/topology.py b/models/topology.py index 1d26115..932f796 100644 --- a/models/topology.py +++ b/models/topology.py @@ -18,7 +18,9 @@ from parsing.nodehandler import NodeHandler from parsing.linkhandler import LinkHandler -GLOBAL_INSTITUTION_ID = 'urn:ogf:networking:global' +SDX_INSTITUTION_ID = 'urn:ogf:network:sdx' +SDX_TOPOLOGY_ID_prefix = "urn:ogf:network:sdx" +TOPOLOGY_INITIAL_VERSION="0.0" class Topology(object): diff --git a/parsing/topologyhandler.py b/parsing/topologyhandler.py index ba8f98e..6b568a4 100644 --- a/parsing/topologyhandler.py +++ b/parsing/topologyhandler.py @@ -21,7 +21,7 @@ def topology_file_name(self,topology_filename=None): def import_topology(self): with open(self.topology_file, 'r', encoding='utf-8') as data_file: data = json.load(data_file) - self.topology = self.import_topology_data(data) + self.import_topology_data(data) def import_topology_data(self, data): try: @@ -35,9 +35,9 @@ def import_topology_data(self, data): except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) - topology=Topology(id=id, name=name, domain_service = service, version=version, time_stamp=time_stamp, nodes=nodes, links=links) + self.topology=Topology(id=id, name=name, domain_service = service, version=version, time_stamp=time_stamp, nodes=nodes, links=links) - return topology + return self.topology - def get_topology(): + def get_topology(self): return self.topology diff --git a/topologymanager/manager.py b/topologymanager/manager.py index 3572fb8..0d23d71 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -1,11 +1,15 @@ import json +import copy + from models.topology import Topology import parsing from parsing.topologyhandler import TopologyHandler from parsing.exceptions import DataModelException + + class TopologyManager(): """" @@ -16,17 +20,25 @@ def __init__(self): super().__init__() self.handler = TopologyHandler() - #self.topology=Topology() + self.topology=None + self.topology_list={} def get_handler(self): return self.handler - def topology_id(self, id): + def topology_id(self, id): self.topology._id(id) def add_topology(self, data): + topology = self.handler.import_topology_data(data) - ##generate a new topology id + self.topology_list[topology.id] = topology + + if self.topology is None: + self.topology = copy.deepcopy(topology) + ##generate a new topology id + self.generate_id() + ## update the version and timestamp to be the latest ##check the inter-domain links first. self.inter_domain_check(topology) @@ -35,20 +47,59 @@ def add_topology(self, data): self.topology.add_nodes(nodes) ##links links = topology.get_links() - self.topology.add_links(nodes) + self.topology.add_links(links) + self.update_version(False) + + def generate_id(self): + id=Topology.SDX_TOPOLOGY_ID_prefix + self.topology.id(id) + self.topology.version(Topology.TOPOLOGY_INITIAL_VERSION) + return id + def remove_topology(self, data): pass def update_topology(self,data): - pass + #likely adding new iner-domain links + self.update_version(True) def get_topology(self): return self.topology + def update_version(self,sub:bool): + [ver, sub_ver] = self.topology.version.split('.') + if not sub: + ver=str(int(ver)+1) + else: + sub_ver=str(int(sub_ver)+1) + def inter_domain_check(self,topology): + interdomain_port_list=[] + links = topology.get_links() + for link in links: + for port in link.ports: + if port.inter_domain: + interdomain_port_list.append(port) + if len(interdomain_port_list)==0: + return False + + #match any ports in the existing topology + # + # + # remove the duplicated inter-domain links? + # + + return True + + def update_timestamp(self): + pass + + def add_domain_service(self): pass + def update_private_properties(self): + pass def generate_grenml(self): pass \ No newline at end of file From f7f29fd5c8478cbd5268795d1c432db224ae8c1f Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 1 Oct 2021 13:58:21 -0400 Subject: [PATCH 17/67] adding inter-domain link checks --- test/test_topology_manager.py | 22 ++++++++++++++-------- topologymanager/manager.py | 16 +++++++++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py index d2587db..9d6f617 100644 --- a/test/test_topology_manager.py +++ b/test/test_topology_manager.py @@ -1,4 +1,5 @@ import unittest +import json import parsing import topologymanager @@ -11,22 +12,28 @@ TOPOLOGY_AMLIGHT = './test/data/amlight.json' +TOPOLOGY_SAX = './test/data/sax.json' +TOPOLOGY_ZHAOXI = './test/data/zhaoxi.json' + +topology_file_list = [TOPOLOGY_AMLIGHT,TOPOLOGY_SAX, TOPOLOGY_ZHAOXI] class TestTopologyManager(unittest.TestCase): def setUp(self): self.manager = TopologyManager() # noqa: E501 - self.handler = self.manager.handler - self.handler.topology_file_name(TOPOLOGY_AMLIGHT) - self.handler.import_topology() def tearDown(self): pass def testMergeTopology(self): + print("Test Topology Merge!") try: - print("Test Topology Merge") - print(self.handler.topology) + for topology_file in topology_file_list: + with open(self.topology_file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + print("Adding Topology:" + topology_file) + self.manager.add_topology(self, data) + except DataModelException as e: print(e) return False @@ -34,9 +41,8 @@ def testMergeTopology(self): def testGrenmlConverter(self): try: - print("Test Topology Converter") - print(self.handler.topology) - converter = GrenmlConverter(self.handler.topology) + print("Test Topology GRENML Converter") + converter = GrenmlConverter(self.topology) converter.read_topology() print(converter.get_xml_str) except DataModelException as e: diff --git a/topologymanager/manager.py b/topologymanager/manager.py index 0d23d71..4fd960f 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -38,6 +38,8 @@ def add_topology(self, data): self.topology = copy.deepcopy(topology) ##generate a new topology id self.generate_id() + else: + self.update_version(False) ## update the version and timestamp to be the latest ##check the inter-domain links first. @@ -57,11 +59,12 @@ def generate_id(self): self.topology.version(Topology.TOPOLOGY_INITIAL_VERSION) return id - def remove_topology(self, data): - pass + def remove_topology(self, topology_id): + self.topology_list.pop(topology_id, None) + self.update_version(False) def update_topology(self,data): - #likely adding new iner-domain links + #likely adding new inter-domain links self.update_version(True) def get_topology(self): @@ -81,12 +84,15 @@ def inter_domain_check(self,topology): for port in link.ports: if port.inter_domain: interdomain_port_list.append(port) + + #ToDo: raise an warning or exception if len(interdomain_port_list)==0: return False #match any ports in the existing topology - # - # + for port in interdomain_port_list: + print("checking port:" + port.id) + # remove the duplicated inter-domain links? # From 02107d869005a8b688b1287249d7c5934d538f92 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Thu, 7 Oct 2021 19:33:28 -0400 Subject: [PATCH 18/67] added unittest to combine three mocked domain topologies, tested/imporved the topology manager, validator, and grenmlconverter --- models/link.py | 20 +- models/node.py | 4 +- models/topology.py | 23 +- test/data/amlight.json | 234 +++++++++++++------- test/data/amlight_origin.json | 141 +++++++++++++ test/data/ampath-sax-zaoxi.pdf | Bin 0 -> 31287 bytes test/data/sax.json | 293 ++++++++++++++++++++++++++ test/data/zaoxi.json | 191 +++++++++++++++++ test/test_topology_grenmlconverter.py | 36 ++++ test/test_topology_manager.py | 14 +- test/test_topology_validator.py | 4 +- topologymanager/grenmlconverter.py | 13 +- topologymanager/manager.py | 106 +++++++--- validation/topologyvalidator.py | 4 +- 14 files changed, 958 insertions(+), 125 deletions(-) create mode 100644 test/data/amlight_origin.json create mode 100644 test/data/ampath-sax-zaoxi.pdf create mode 100644 test/data/sax.json create mode 100644 test/data/zaoxi.json create mode 100644 test/test_topology_grenmlconverter.py diff --git a/models/link.py b/models/link.py index 91407ce..0e29a96 100644 --- a/models/link.py +++ b/models/link.py @@ -71,7 +71,8 @@ def __init__(self, id=None, name=None, short_name=None, ports=None, total_bandwi self._name = name if short_name is not None: self._short_name = short_name - self.ports = ports + if ports is not None: + self._ports = self.set_ports(ports) if total_bandwidth is not None: self._total_bandwidth = total_bandwidth if available_bandwidth is not None: @@ -166,6 +167,23 @@ def ports(self): """ return self._ports + def set_ports(self, ports): + """Sets the ports of this Node. + + + :param ports: The ports of this Node. # noqa: E501 + :type: list[port] + """ + if ports is None: + raise ValueError("Invalid value for `ports`, must not be `None`") # noqa: E501 + + if self._ports is None: + self._ports=[] + + for port in ports: + self._ports.append(port['id']) + + return self.ports @ports.setter def ports(self, ports): """Sets the ports of this Link. diff --git a/models/node.py b/models/node.py index d5b677f..ee92d2c 100644 --- a/models/node.py +++ b/models/node.py @@ -183,7 +183,7 @@ def set_ports(self, ports): if ports is None: raise ValueError("Invalid value for `ports`, must not be `None`") # noqa: E501 - if self.ports is None: + if self._ports is None: self._ports=[] for port in ports: @@ -191,7 +191,7 @@ def set_ports(self, ports): port_obj = port_handler.import_port_data(port) self._ports.append(port_obj) - return self.ports + return self._ports @ports.setter def ports(self, ports): diff --git a/models/topology.py b/models/topology.py index 932f796..23a29f3 100644 --- a/models/topology.py +++ b/models/topology.py @@ -74,8 +74,7 @@ def id(self): """ return self._id - @id.setter - def id(self, id): + def set_id(self, id): """Sets the id of this Topology. @@ -97,8 +96,7 @@ def name(self): """ return self._name - @name.setter - def name(self, name): + def set_name(self, name): """Sets the name of this Topology. @@ -155,8 +153,7 @@ def version(self): """ return self._version - @version.setter - def version(self, version): + def set_version(self, version): """Sets the version of this Topology. @@ -227,6 +224,11 @@ def set_nodes(self, nodes): return self.get_nodes() + def remove_node(self,node_id): + for node in list(self._nodes): + if node.id == node_id: + self._nodes.remove(node) + def add_nodes(self, node_objects): """add the nodes to this Topology. :param node_objects: a list of node objects @@ -238,7 +240,7 @@ def get_node_by_port(self,aPort): for node in self.nodes: ports = node.ports for port in ports: - if port.id == aPort['id']: + if port.id == aPort: return node return None @@ -279,12 +281,17 @@ def set_links(self, links): return self.get_links() + def remove_link(self,link_id): + for link in list(self._links): + if link.id == link_id: + self._links.remove(link) + def add_links(self, link_objects): """add the links to this Topology. :param link_objects: a list of link objects """ - self._nodes.extend(link_objects) + self._links.extend(link_objects) @property def private_attributes(self): diff --git a/test/data/amlight.json b/test/data/amlight.json index 1ae4011..d4bada7 100644 --- a/test/data/amlight.json +++ b/test/data/amlight.json @@ -1,35 +1,19 @@ { - "id": "id", + "id": "urn:ogf:network:sdx:topology:amlight", "links": [ { "availability": 56.37376656633328, "available_bandwidth": 602746.015561422, - "id": "l-1", + "id": "urn:ogf:network:sdx:link:amlight:B1-B2", "latency": 146582.15146899645, - "name": "miami-Boca.amLight.sdx", + "name": "amlight:B1-B2", "packet_loss": 59.621339166831824, "ports": [ { - "id": "p-1", - "label_range": [ - "label_range", - "label_range" - ], - "name": "urn:sdx:port:amlight.net:Novi06:9", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "9", - "status": "up" + "id": "urn:ogf:network:sdx:port:amlight:B1:2" }, { - "id": "p-2", - "label_range": [ - "label_range", - "label_range" - ], - "name": "urn:sdx:port:amlight.net:Novi06:10", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "10", - "status": "up" + "id": "urn:ogf:network:sdx:port:amlight:B2:2" } ], "short_name": "Miami-BocaRaton", @@ -38,99 +22,203 @@ { "availability": 56.37376656633328, "available_bandwidth": 602746.015561422, - "id": "l-2", + "id": "urn:ogf:network:sdx:link:amlight:A1-B1", "latency": 146582.15146899645, - "name": "miami-Boca.amLight.sdx", + "name": "amlight:A1-B1", "packet_loss": 59.621339166831824, "ports": [ { - "id": "p-3", - "label_range": [ - "label_range", - "label_range" - ], - "name": "urn:sdx:port:amlight.net:Novi06:9", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "9", - "status": "up" + "id": "urn:ogf:network:sdx:port:amlight:A1:1" }, { - "id": "p-4", - "label_range": [ - "label_range", - "label_range" - ], - "name": "urn:sdx:port:amlight.net:Novi06:10", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "10", - "status": "up" + "id": "urn:ogf:network:sdx:port:amlight:B1:3" } ], - "short_name": "Miami-BocaRaton", + "short_name": "redclara-miami", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:amlight:A1-B2", + "latency": 146582.15146899645, + "name": "amlight:A1-B2", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:amlight:A1:2" + }, + { + "id": "urn:ogf:network:sdx:port:amlight:B2:3" + } + ], + "short_name": "redclara-BocaRaton", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", + "latency": 146582.15146899645, + "name": "nni:Miami-Sanpaolo", + "packet_loss": 59.621339166831824, + "nni": "True", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:amlight:B1:1" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:1" + } + ], + "short_name": "Miami-Sanpaolo", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", + "latency": 146582.15146899645, + "name": "nni:BocaRaton-Fortaleza", + "packet_loss": 59.621339166831824, + "nni": "True", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:amlight:B2:1" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:1" + } + ], + "short_name": "BocaRaton-Fortaleza", "total_bandwidth": 80083.7389632821 } ], "name": "amLight", "nodes": [ { - "id": "1", + "id": "urn:ogf:network:sdx:node:amlight:B1", "location": { "address": "Miami", - "latitude": -28.51107891831147, - "longitude": -79.57947854792273 + "latitude": 25.75633040531146, + "longitude": -80.37676058477908 }, - "name": "urn:sdx:node:amlight.net:Novi06", + "name": "amlight:Novi01", "ports": [ { - "id": "p-1", - "name": "urn:sdx:port:amlight.net:Novi06:9", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "9", + "id": "urn:ogf:network:sdx:port:amlight:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:node:amlight:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:amlight:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:node:amlight:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], "status": "up" }, { - "id": "p-3", - "name": "urn:sdx:port:amlight.net:Novi06:9", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "9", + "id": "urn:ogf:network:sdx:port:amlight:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:amlight:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], "status": "up" } ], - "short_name": "Novi06" + "short_name": "B1" }, { - "id": "2", + "id": "urn:ogf:network:sdx:node:amlight:B2", "location": { - "address": "Miami", - "latitude": -28.51107891831147, - "longitude": -79.57947854792273 + "address": "BocaRaton", + "latitude": 26.381437356374075, + "longitude": -80.10225977485742 + }, + "name": "amlight:Novi02", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:amlight:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:ogf:network:sdx:node:amlight:B2", + "short_name": "B2:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:amlight:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:amlight:B2", + "short_name": "B2:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:amlight:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:amlight:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "short_name": "B2" + }, + { + "id": "urn:ogf:network:sdx:node:amlight:A1", + "location": { + "address": "redclara", + "latitude": 30.34943181039702, + "longitude": -81.66666016473143 }, - "name": "urn:sdx:node:amlight.net:Novi06", + "name": "amlight:Novi100", "ports": [ { - "id": "p-2", + "id": "urn:ogf:network:sdx:port:amlight:A1:1", "label_range": [ - "label_range", - "label_range" + "100-200", + "1000" ], - "name": "urn:sdx:port:amlight.net:Novi06:9", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "9", + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:amlight:A1", + "short_name": "A1:1", "status": "up" }, { - "id": "p-4", + "id": "urn:ogf:network:sdx:port:amlight:A1:2", "label_range": [ - "label_range", - "label_range" + "100-200", + "1000" ], - "name": "urn:sdx:port:amlight.net:Novi06:9", - "node": "urn:sdx:port:amlight.net:Novi06", - "short_name": "9", + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:amlight:A1", + "short_name": "A1:2", "status": "up" } ], - "short_name": "Novi06" + "short_name": "A1" } ], "time_stamp": "2000-01-23T04:56:07+00:00", diff --git a/test/data/amlight_origin.json b/test/data/amlight_origin.json new file mode 100644 index 0000000..1ae4011 --- /dev/null +++ b/test/data/amlight_origin.json @@ -0,0 +1,141 @@ +{ + "id": "id", + "links": [ + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "l-1", + "latency": 146582.15146899645, + "name": "miami-Boca.amLight.sdx", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "p-1", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "p-2", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:10", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "10", + "status": "up" + } + ], + "short_name": "Miami-BocaRaton", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "l-2", + "latency": 146582.15146899645, + "name": "miami-Boca.amLight.sdx", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "p-3", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "p-4", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:10", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "10", + "status": "up" + } + ], + "short_name": "Miami-BocaRaton", + "total_bandwidth": 80083.7389632821 + } + ], + "name": "amLight", + "nodes": [ + { + "id": "1", + "location": { + "address": "Miami", + "latitude": -28.51107891831147, + "longitude": -79.57947854792273 + }, + "name": "urn:sdx:node:amlight.net:Novi06", + "ports": [ + { + "id": "p-1", + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "p-3", + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + } + ], + "short_name": "Novi06" + }, + { + "id": "2", + "location": { + "address": "Miami", + "latitude": -28.51107891831147, + "longitude": -79.57947854792273 + }, + "name": "urn:sdx:node:amlight.net:Novi06", + "ports": [ + { + "id": "p-2", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + }, + { + "id": "p-4", + "label_range": [ + "label_range", + "label_range" + ], + "name": "urn:sdx:port:amlight.net:Novi06:9", + "node": "urn:sdx:port:amlight.net:Novi06", + "short_name": "9", + "status": "up" + } + ], + "short_name": "Novi06" + } + ], + "time_stamp": "2000-01-23T04:56:07+00:00", + "version": 1, + "domain_service": { + "owner":"FIU" + } + } \ No newline at end of file diff --git a/test/data/ampath-sax-zaoxi.pdf b/test/data/ampath-sax-zaoxi.pdf new file mode 100644 index 0000000000000000000000000000000000000000..99c9359b3e5ef9ff219c702f57afb82049db47b6 GIT binary patch literal 31287 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 I{R;cP0DaN~yZ`_I literal 0 HcmV?d00001 diff --git a/test/data/sax.json b/test/data/sax.json new file mode 100644 index 0000000..898931d --- /dev/null +++ b/test/data/sax.json @@ -0,0 +1,293 @@ +{ + "id": "urn:ogf:network:sdx:topology:sax", + "links": [ + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:sax:B1-B2", + "latency": 146582.15146899645, + "name": "sax:B1-B2", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B1:2" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:2" + } + ], + "short_name": "SaoPaulo-Fortaleza", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:sax:A1-B1", + "latency": 146582.15146899645, + "name": "sax:A1-B1", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:1" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:3" + } + ], + "short_name": "redclara-SaoPaulo", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:sax:A1-B2", + "latency": 146582.15146899645, + "name": "sax:A1-B2", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:2" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:3" + } + ], + "short_name": "redclara-Fortaleza", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", + "latency": 146582.15146899645, + "name": "nni:Miami-Sanpaolo", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:amlight:B1:1" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:1" + } + ], + "short_name": "Miami-Sanpaolo", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", + "latency": 146582.15146899645, + "name": "nni:BocaRaton-Fortaleza", + "packet_loss": 59.621339166831824, + "nni": "True", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:amlight:B2:1" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:1" + } + ], + "short_name": "BocaRaton-Fortaleza", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", + "latency": 146582.15146899645, + "name": "nni:Fortaleza-Sangano", + "packet_loss": 59.621339166831824, + "nni": "True", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:1" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1" + } + ], + "short_name": "Fortaleza-Sangano", + "total_bandwidth": 80083.7389632821 + } + ], + "name": "sax", + "nodes": [ + { + "id": "urn:ogf:network:sdx:node:sax:B1", + "location": { + "address": "SaoPaulo", + "latitude": -23.5311561958366, + "longitude": -46.650271781410524 + }, + "name": "sax:Novi01", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "short_name": "B1" + }, + { + "id": "urn:ogf:network:sdx:node:sax:B2", + "location": { + "address": "Fortaleza", + "latitude": -3.73163824920348, + "longitude": -38.52443289673026 + }, + "name": "sax:Novi02", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "short_name": "B2" + }, + { + "id": "urn:ogf:network:sdx:node:sax:B3", + + "location": { + "address": "Fortaleza", + "latitude": -3.73163824920348, + "longitude": -38.52443289673026 + }, + "name": "sax:Novi02", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B3:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B3:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi03:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:3", + "status": "up" + } + ], + "short_name": "B3" + }, + { + "id": "urn:ogf:network:sdx:node:sax:A1", + "location": { + "address": "Santiago", + "latitude": -33.4507049233331, + "longitude": -70.64634765264213 + }, + "name": "sax:Novi100", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:sax:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:sax:A1", + "short_name": "A1:2", + "status": "up" + } + ], + "short_name": "A1" + } + ], + "time_stamp": "2000-01-23T04:56:07+00:00", + "version": 1, + "domain_service": { + "owner":"RNP" + } +} \ No newline at end of file diff --git a/test/data/zaoxi.json b/test/data/zaoxi.json new file mode 100644 index 0000000..03ad7f3 --- /dev/null +++ b/test/data/zaoxi.json @@ -0,0 +1,191 @@ +{ + "id": "urn:ogf:network:sdx:topology:zaoxi", + "links": [ + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", + "latency": 146582.15146899645, + "name": "zaoxi:B1-B2", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2" + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:B2:2" + } + ], + "short_name": "Sangano-Capetown", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", + "latency": 146582.15146899645, + "name": "zaoxi:A1-B1", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:node:zaoxi:A1:1" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3" + } + ], + "short_name": "Karoo-Sangano", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", + "latency": 146582.15146899645, + "name": "zaoxi:A1-B2", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:node:zaoxi:A1:2" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3" + } + ], + "short_name": "Karoo-Capetown", + "total_bandwidth": 80083.7389632821 + } + ], + "name": "zaoxi", + "nodes": [ + { + "id": "urn:ogf:network:sdx:node:zaoxi:B1", + "location": { + "address": "Sangano", + "latitude": -9.533459658700743, + "longitude": 13.216709879405311 + }, + "name": "zaoxi:Novi01", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "short_name": "B1" + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:B2", + "location": { + "address": "CapeTown", + "latitude": -3.73163824920348, + "longitude": -38.52443289673026 + }, + "name": "zaoxi:Novi02", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "short_name": "B2" + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:A1", + "location": { + "address": "Karoo", + "latitude": -32.3632301851245, + "longitude": 22.541224555821298 + }, + "name": "zaoxi:Novi100", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:2", + "status": "up" + } + ], + "short_name": "A1" + } + ], + "time_stamp": "2000-01-23T04:56:07+00:00", + "version": 1, + "domain_service": { + "owner":"FIU" + } +} \ No newline at end of file diff --git a/test/test_topology_grenmlconverter.py b/test/test_topology_grenmlconverter.py new file mode 100644 index 0000000..9fc2152 --- /dev/null +++ b/test/test_topology_grenmlconverter.py @@ -0,0 +1,36 @@ +import unittest + +import parsing +import topologymanager + +from validation.topologyvalidator import TopologyValidator +from parsing.topologyhandler import TopologyHandler +from topologymanager.manager import TopologyManager +from topologymanager.grenmlconverter import GrenmlConverter +from parsing.exceptions import DataModelException + + +TOPOLOGY_AMLIGHT = './test/data/amlight.json' + +class TestTopologyManager(unittest.TestCase): + + def setUp(self): + self.manager = TopologyManager() # noqa: E501 + self.handler = self.manager.handler + self.handler.topology_file_name(TOPOLOGY_AMLIGHT) + self.handler.import_topology() + + def tearDown(self): + pass + + def testGrenmlConverter(self): + try: + print("Test Topology Converter") + print(self.handler.topology) + converter = GrenmlConverter(self.handler.topology) + converter.read_topology() + print(converter.get_xml_str) + except DataModelException as e: + print(e) + return False + return True \ No newline at end of file diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py index 9d6f617..cc030d3 100644 --- a/test/test_topology_manager.py +++ b/test/test_topology_manager.py @@ -13,7 +13,7 @@ TOPOLOGY_AMLIGHT = './test/data/amlight.json' TOPOLOGY_SAX = './test/data/sax.json' -TOPOLOGY_ZHAOXI = './test/data/zhaoxi.json' +TOPOLOGY_ZHAOXI = './test/data/zaoxi.json' topology_file_list = [TOPOLOGY_AMLIGHT,TOPOLOGY_SAX, TOPOLOGY_ZHAOXI] @@ -29,10 +29,10 @@ def testMergeTopology(self): print("Test Topology Merge!") try: for topology_file in topology_file_list: - with open(self.topology_file, 'r', encoding='utf-8') as data_file: + with open(topology_file, 'r', encoding='utf-8') as data_file: data = json.load(data_file) print("Adding Topology:" + topology_file) - self.manager.add_topology(self, data) + self.manager.add_topology(data) except DataModelException as e: print(e) @@ -42,10 +42,14 @@ def testMergeTopology(self): def testGrenmlConverter(self): try: print("Test Topology GRENML Converter") - converter = GrenmlConverter(self.topology) + self.testMergeTopology() + converter = GrenmlConverter(self.manager.get_topology()) converter.read_topology() print(converter.get_xml_str) except DataModelException as e: print(e) return False - return True \ No newline at end of file + return True + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index 3110f22..ee16c73 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -8,11 +8,13 @@ TOPOLOGY_AMLIGHT = './test/data/amlight.json' TOPOLOGY_AMPATH = './test/data/ampath.json' +TOPOLOGY_SAX = './test/data/sax.json' +TOPOLOGY_ZAOXI = './test/data/zaoxi.json' class TestTopologyValidator(unittest.TestCase): def setUp(self): - self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) + self.handler = TopologyHandler(TOPOLOGY_ZAOXI) self.validator = TopologyValidator() def tearDown(self): diff --git a/topologymanager/grenmlconverter.py b/topologymanager/grenmlconverter.py index f3bee67..096eb21 100644 --- a/topologymanager/grenmlconverter.py +++ b/topologymanager/grenmlconverter.py @@ -12,6 +12,9 @@ def __init__(self, topology:Topology): self.topology=topology self.grenml_manager = GRENMLManager(topology.name) + def set_topology(self, topology:Topology): + self.topology=topology + def read_topology(self): domain_service = self.topology.get_domain_service() owner = domain_service.owner @@ -30,11 +33,12 @@ def read_topology(self): def add_nodes(self,nodes): for node in nodes: location = node.get_location() - + print("adding node:"+node.id) self.grenml_manager.add_node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address) def add_links(self,links): for link in links: + inter_domain_link = False ports=link.ports end_nodes=[] for port in ports: @@ -44,9 +48,10 @@ def add_links(self,links): grenml_node = Node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address) end_nodes.append(grenml_node ) else: - print("This port doesn't belong to any node in the topology!") - - self.grenml_manager.add_link(link.id, link.name,link.short_name, nodes=end_nodes) + print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port) + inter_domain_link = True + if not inter_domain_link: + self.grenml_manager.add_link(link.id, link.name,link.short_name, nodes=end_nodes) def get_xml_str(self): return self.topology_str \ No newline at end of file diff --git a/topologymanager/manager.py b/topologymanager/manager.py index 4fd960f..b195e97 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -2,13 +2,15 @@ import json import copy -from models.topology import Topology +from models.topology import Topology, SDX_TOPOLOGY_ID_prefix,TOPOLOGY_INITIAL_VERSION +from models.link import Link +from models.port import Port + -import parsing from parsing.topologyhandler import TopologyHandler from parsing.exceptions import DataModelException - +from .grenmlconverter import GrenmlConverter class TopologyManager(): @@ -20,8 +22,12 @@ def __init__(self): super().__init__() self.handler = TopologyHandler() + self.topology=None self.topology_list={} + self.port_list={} #{port, link} + + self.num_interdomain_link=0 def get_handler(self): return self.handler @@ -38,34 +44,67 @@ def add_topology(self, data): self.topology = copy.deepcopy(topology) ##generate a new topology id self.generate_id() + #Addding to the port list + links = topology.get_links() + for link in links: + for port in link.ports: + self.port_list[port] = link else: - self.update_version(False) - ## update the version and timestamp to be the latest - ##check the inter-domain links first. - self.inter_domain_check(topology) - ##nodes - nodes = topology.get_nodes() - self.topology.add_nodes(nodes) - ##links - links = topology.get_links() - self.topology.add_links(links) + self.update_version(False) + self.update_timestamp() + + ##check the inter-domain links first. + self.num_interdomain_link+=self.inter_domain_check(topology) + if self.num_interdomain_link==0: + print("Warning: no interdomain links detected!") + ##nodes + nodes = topology.get_nodes() + self.topology.add_nodes(nodes) + ##links + links = topology.get_links() + self.topology.add_links(links) - self.update_version(False) + self.update_version(False) def generate_id(self): - id=Topology.SDX_TOPOLOGY_ID_prefix - self.topology.id(id) - self.topology.version(Topology.TOPOLOGY_INITIAL_VERSION) + self.topology.set_id(SDX_TOPOLOGY_ID_prefix) + self.topology.set_version(TOPOLOGY_INITIAL_VERSION) return id def remove_topology(self, topology_id): self.topology_list.pop(topology_id, None) self.update_version(False) + self.update_timestamp() def update_topology(self,data): #likely adding new inter-domain links + topology = self.handler.import_topology_data(data) + self.topology_list[topology.id] = topology + + ##nodes + nodes = topology.get_nodes() + for node in nodes: + self.topology.remove_node(node.id) + ##links + links = topology.get_links() + for link in links: + self.topology.remove_links(link.id) + + ##check the inter-domain links first. + num_interdomain_link=self.inter_domain_check(topology) + if num_interdomain_link==0: + print("Warning: no interdomain links detected!") + + ##nodes + nodes = topology.get_nodes() + self.topology.add_nodes(nodes) + ##links + links = topology.get_links() + self.topology.add_links(links) + self.update_version(True) + self.update_timestamp() def get_topology(self): return self.topology @@ -78,25 +117,29 @@ def update_version(self,sub:bool): sub_ver=str(int(sub_ver)+1) def inter_domain_check(self,topology): - interdomain_port_list=[] + interdomain_port_dict={} + num_interdomain_link=0 links = topology.get_links() + link_dict ={} for link in links: + link_dict[link.id] = link for port in link.ports: - if port.inter_domain: - interdomain_port_list.append(port) + interdomain_port_dict[port]=link #ToDo: raise an warning or exception - if len(interdomain_port_list)==0: + if len(interdomain_port_dict)==0: return False #match any ports in the existing topology - for port in interdomain_port_list: - print("checking port:" + port.id) - - # remove the duplicated inter-domain links? - # - - return True + for port_id in interdomain_port_dict: + print("checking port:" + port_id) + for existing_port, existing_link in self.port_list.items(): + if port_id == existing_port: + #remove redundant link between two domains + self.topology.remove_link(existing_link.id) + num_interdomain_link=+1 + + return num_interdomain_link def update_timestamp(self): pass @@ -107,5 +150,10 @@ def add_domain_service(self): def update_private_properties(self): pass + def generate_graph(self): + pass + def generate_grenml(self): - pass \ No newline at end of file + self.converter = GrenmlConverter(self.topology) + + return self.converter.read_topology() \ No newline at end of file diff --git a/validation/topologyvalidator.py b/validation/topologyvalidator.py index 7e8364b..d80513a 100644 --- a/validation/topologyvalidator.py +++ b/validation/topologyvalidator.py @@ -5,7 +5,7 @@ from collections.abc import Iterable from re import match -from models.topology import GLOBAL_INSTITUTION_ID, Topology +from models.topology import SDX_INSTITUTION_ID, Topology from models.service import Service from models.node import Node from models.link import Link @@ -57,7 +57,7 @@ def _validate_topology(self, topology: Topology): errors = [] errors += self._validate_object_defaults(topology) - if GLOBAL_INSTITUTION_ID not in topology.id: + if SDX_INSTITUTION_ID not in topology.id: errors.append('Global Institution must be in Topology {}'.format(topology.id)) service = topology.get_domain_service() From 590d4a7720cb7ca33133ec691755079979e2915b Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 8 Oct 2021 14:25:03 -0400 Subject: [PATCH 19/67] unittest for generating a graph (networkx) --- test/test_topology_graph.py | 41 +++++++++++++++++++++++++++ test/test_topology_grenmlconverter.py | 2 +- topologymanager/grenmlconverter.py | 2 +- topologymanager/manager.py | 36 ++++++++++++++++++++++- 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 test/test_topology_graph.py diff --git a/test/test_topology_graph.py b/test/test_topology_graph.py new file mode 100644 index 0000000..fe5d1e8 --- /dev/null +++ b/test/test_topology_graph.py @@ -0,0 +1,41 @@ +import unittest + +from networkx import MultiGraph, Graph +import matplotlib.pyplot as plt +import networkx as nx + +import parsing +import topologymanager + +from validation.topologyvalidator import TopologyValidator +from parsing.topologyhandler import TopologyHandler +from topologymanager.manager import TopologyManager +from topologymanager.grenmlconverter import GrenmlConverter +from parsing.exceptions import DataModelException + + +TOPOLOGY_AMLIGHT = './test/data/amlight.json' + +class TestTopologyGrpah(unittest.TestCase): + + def setUp(self): + self.manager = TopologyManager() # noqa: E501 + self.handler = self.manager.handler + self.handler.topology_file_name(TOPOLOGY_AMLIGHT) + self.handler.import_topology() + self.manager.set_topology(self.handler.get_topology()) + + def tearDown(self): + pass + + def testGenerateGraph(self): + try: + print("Test Topology Graph") + graph = self.manager.generate_graph() + #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout + nx.draw(graph) + plt.savefig("amlight.png") + except DataModelException as e: + print(e) + return False + return True \ No newline at end of file diff --git a/test/test_topology_grenmlconverter.py b/test/test_topology_grenmlconverter.py index 9fc2152..b90a4a7 100644 --- a/test/test_topology_grenmlconverter.py +++ b/test/test_topology_grenmlconverter.py @@ -12,7 +12,7 @@ TOPOLOGY_AMLIGHT = './test/data/amlight.json' -class TestTopologyManager(unittest.TestCase): +class TestTopologyGRENMLConverter(unittest.TestCase): def setUp(self): self.manager = TopologyManager() # noqa: E501 diff --git a/topologymanager/grenmlconverter.py b/topologymanager/grenmlconverter.py index 096eb21..ec7c6d8 100644 --- a/topologymanager/grenmlconverter.py +++ b/topologymanager/grenmlconverter.py @@ -46,7 +46,7 @@ def add_links(self,links): if node is not None: location = node.get_location() grenml_node = Node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address) - end_nodes.append(grenml_node ) + end_nodes.append(grenml_node) else: print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port) inter_domain_link = True diff --git a/topologymanager/manager.py b/topologymanager/manager.py index b195e97..a451edb 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -2,6 +2,8 @@ import json import copy +import networkx as nx + from models.topology import Topology, SDX_TOPOLOGY_ID_prefix,TOPOLOGY_INITIAL_VERSION from models.link import Link from models.port import Port @@ -35,6 +37,9 @@ def get_handler(self): def topology_id(self, id): self.topology._id(id) + def set_topology(self, topology): + self.topology = topology + def add_topology(self, data): topology = self.handler.import_topology_data(data) @@ -147,11 +152,40 @@ def update_timestamp(self): def add_domain_service(self): pass + #may need to read from a configuration file. def update_private_properties(self): pass + #adjacent matrix of the graph, in jason? def generate_graph(self): - pass + G = nx.Graph() + links = self.topology.links + for link in links: + inter_domain_link = False + ports=link.ports + end_nodes=[] + for port in ports: + node=self.topology.get_node_by_port(port) + if node is None: + print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port) + inter_domain_link = True + break + else: + end_nodes.append(node) + if not inter_domain_link: + G.add_edge(end_nodes[0].id,end_nodes[1].id) + edge = G.edges[end_nodes[0].id,end_nodes[1].id] + edge['id'] = link.id + edge['latency'] = link.latency + edge['total_bandwidth'] = link.total_bandwidth + edge['available_bandwidth'] = link.available_bandwidth + edge['latency'] = link.latency + edge['packet_loss'] = link.packet_loss + edge['availability'] = link.availability + + return G + + def generate_grenml(self): self.converter = GrenmlConverter(self.topology) From 68057e426886b20b82fe78bce455e72744f9a9a6 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Fri, 8 Oct 2021 14:39:23 -0400 Subject: [PATCH 20/67] adding an unittest in topology manager --- test/test_topology_manager.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py index cc030d3..5b8ed70 100644 --- a/test/test_topology_manager.py +++ b/test/test_topology_manager.py @@ -1,5 +1,8 @@ import unittest import json +from networkx import MultiGraph, Graph +import matplotlib.pyplot as plt +import networkx as nx import parsing import topologymanager @@ -51,5 +54,18 @@ def testGrenmlConverter(self): return False return True + def testGenerateGraph(self): + try: + print("Test Topology Graph") + self.testMergeTopology() + graph = self.manager.generate_graph() + #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout + nx.draw(graph) + plt.savefig("amlight.png") + except DataModelException as e: + print(e) + return False + return True + if __name__ == '__main__': unittest.main() \ No newline at end of file From fbe57e9ceb54a41964c88d74c2230badb4b5e078 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 11 Oct 2021 14:20:33 -0400 Subject: [PATCH 21/67] adjust the 3-domain SDX topology (1-connected) --- test/data/sax.json | 64 +++++++++++++++++++++++++++++++++-- test/data/zaoxi.json | 25 ++++++++++++-- test/test_topology_graph.py | 2 ++ test/test_topology_manager.py | 7 ++-- topologymanager/manager.py | 13 +++++-- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/test/data/sax.json b/test/data/sax.json index 898931d..c55939b 100644 --- a/test/data/sax.json +++ b/test/data/sax.json @@ -19,6 +19,42 @@ "short_name": "SaoPaulo-Fortaleza", "total_bandwidth": 80083.7389632821 }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:sax:Panama-Fortaleza", + "latency": 146582.15146899645, + "name": "sax:Panama-Fortaleza", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:2" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:4" + } + ], + "short_name": "Panama-Fortaleza", + "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:sax:SanPaolo-Fortaleza", + "latency": 146582.15146899645, + "name": "nni:SanPaolo-Fortaleza", + "packet_loss": 59.621339166831824, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:3" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4" + } + ], + "short_name": "BocaRaton-Fortaleza", + "total_bandwidth": 80083.7389632821 + }, { "availability": 56.37376656633328, "available_bandwidth": 602746.015561422, @@ -155,6 +191,17 @@ "10001" ], "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:4", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" } ], "short_name": "B1" @@ -162,9 +209,9 @@ { "id": "urn:ogf:network:sdx:node:sax:B2", "location": { - "address": "Fortaleza", - "latitude": -3.73163824920348, - "longitude": -38.52443289673026 + "address": "PanamaCity", + "latitude": 8.993040465928525, + "longitude": -79.4947050137491 }, "name": "sax:Novi02", "ports": [ @@ -200,6 +247,17 @@ "node": "urn:ogf:network:sdx:node:sax:B2", "short_name": "B2:3", "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:4", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:4", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:3", + "status": "up" } ], "short_name": "B2" diff --git a/test/data/zaoxi.json b/test/data/zaoxi.json index 03ad7f3..a2eb470 100644 --- a/test/data/zaoxi.json +++ b/test/data/zaoxi.json @@ -13,7 +13,7 @@ "id": "urn:ogf:network:sdx:port:zaoxi:B1:2" }, { - "id": "urn:ogf:network:sdx:node:zaoxi:B2:2" + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2" } ], "short_name": "Sangano-Capetown", @@ -28,7 +28,7 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:ogf:network:sdx:node:zaoxi:A1:1" + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1" }, { "id": "urn:ogf:network:sdx:port:zaoxi:B1:3" @@ -46,7 +46,7 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:ogf:network:sdx:node:zaoxi:A1:2" + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2" }, { "id": "urn:ogf:network:sdx:port:zaoxi:B2:3" @@ -54,6 +54,25 @@ ], "short_name": "Karoo-Capetown", "total_bandwidth": 80083.7389632821 + }, + { + "availability": 56.37376656633328, + "available_bandwidth": 602746.015561422, + "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", + "latency": 146582.15146899645, + "name": "nni:Fortaleza-Sangano", + "packet_loss": 59.621339166831824, + "nni": "True", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:1" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1" + } + ], + "short_name": "Fortaleza-Sangano", + "total_bandwidth": 80083.7389632821 } ], "name": "zaoxi", diff --git a/test/test_topology_graph.py b/test/test_topology_graph.py index fe5d1e8..5455734 100644 --- a/test/test_topology_graph.py +++ b/test/test_topology_graph.py @@ -15,6 +15,8 @@ TOPOLOGY_AMLIGHT = './test/data/amlight.json' +TOPOLOGY_SAX = './test/data/sax.json' +TOPOLOGY_ZAOXI = './test/data/zaoxi.json' class TestTopologyGrpah(unittest.TestCase): diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py index 5b8ed70..6b4be59 100644 --- a/test/test_topology_manager.py +++ b/test/test_topology_manager.py @@ -16,9 +16,10 @@ TOPOLOGY_AMLIGHT = './test/data/amlight.json' TOPOLOGY_SAX = './test/data/sax.json' -TOPOLOGY_ZHAOXI = './test/data/zaoxi.json' +TOPOLOGY_ZAOXI = './test/data/zaoxi.json' -topology_file_list = [TOPOLOGY_AMLIGHT,TOPOLOGY_SAX, TOPOLOGY_ZHAOXI] +topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_SAX, TOPOLOGY_ZAOXI] +topology_file_list_2 = [TOPOLOGY_SAX, TOPOLOGY_ZAOXI] class TestTopologyManager(unittest.TestCase): @@ -31,7 +32,7 @@ def tearDown(self): def testMergeTopology(self): print("Test Topology Merge!") try: - for topology_file in topology_file_list: + for topology_file in topology_file_list_3: with open(topology_file, 'r', encoding='utf-8') as data_file: data = json.load(data_file) print("Adding Topology:" + topology_file) diff --git a/topologymanager/manager.py b/topologymanager/manager.py index a451edb..cfd61d5 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -39,6 +39,11 @@ def topology_id(self, id): def set_topology(self, topology): self.topology = topology + + def clear_topology(self): + self.topology=None + self.topology_list={} + self.port_list={} def add_topology(self, data): @@ -129,7 +134,7 @@ def inter_domain_check(self,topology): for link in links: link_dict[link.id] = link for port in link.ports: - interdomain_port_dict[port]=link + interdomain_port_dict[port]=link #ToDo: raise an warning or exception if len(interdomain_port_dict)==0: @@ -137,12 +142,13 @@ def inter_domain_check(self,topology): #match any ports in the existing topology for port_id in interdomain_port_dict: - print("checking port:" + port_id) for existing_port, existing_link in self.port_list.items(): if port_id == existing_port: + print("Interdomain port:" + port_id) #remove redundant link between two domains self.topology.remove_link(existing_link.id) - num_interdomain_link=+1 + num_interdomain_link=+1 + self.port_list[port_id] = interdomain_port_dict[port_id] return num_interdomain_link @@ -172,6 +178,7 @@ def generate_graph(self): break else: end_nodes.append(node) + print("graph node:"+node.id) if not inter_domain_link: G.add_edge(end_nodes[0].id,end_nodes[1].id) edge = G.edges[end_nodes[0].id,end_nodes[1].id] From f1fc6bb4dd351c053194f925f31f96a60c013eb5 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 11 Oct 2021 14:29:50 -0400 Subject: [PATCH 22/67] using node name in graph drawing --- test/data/sax.json | 2 +- test/test_topology_manager.py | 2 +- topologymanager/manager.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/data/sax.json b/test/data/sax.json index c55939b..4a9cc73 100644 --- a/test/data/sax.json +++ b/test/data/sax.json @@ -270,7 +270,7 @@ "latitude": -3.73163824920348, "longitude": -38.52443289673026 }, - "name": "sax:Novi02", + "name": "sax:Novi03", "ports": [ { "id": "urn:ogf:network:sdx:port:sax:B3:1", diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py index 6b4be59..75615c4 100644 --- a/test/test_topology_manager.py +++ b/test/test_topology_manager.py @@ -61,7 +61,7 @@ def testGenerateGraph(self): self.testMergeTopology() graph = self.manager.generate_graph() #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout - nx.draw(graph) + nx.draw(graph,with_labels = True) plt.savefig("amlight.png") except DataModelException as e: print(e) diff --git a/topologymanager/manager.py b/topologymanager/manager.py index cfd61d5..082d17c 100644 --- a/topologymanager/manager.py +++ b/topologymanager/manager.py @@ -180,8 +180,8 @@ def generate_graph(self): end_nodes.append(node) print("graph node:"+node.id) if not inter_domain_link: - G.add_edge(end_nodes[0].id,end_nodes[1].id) - edge = G.edges[end_nodes[0].id,end_nodes[1].id] + G.add_edge(end_nodes[0].name,end_nodes[1].name) + edge = G.edges[end_nodes[0].name,end_nodes[1].name] edge['id'] = link.id edge['latency'] = link.latency edge['total_bandwidth'] = link.total_bandwidth From 71eba9dd19156a38a97f905df0581c3fa84a4dbb Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sun, 7 Nov 2021 15:38:35 -0500 Subject: [PATCH 23/67] only port and link objects need private property list --- schemas/Link.json | 4 ++-- schemas/Node.json | 6 ------ schemas/Port.json | 25 ++++++++++++++++++------- schemas/Service.json | 6 ------ schemas/Topology.json | 6 ------ 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/schemas/Link.json b/schemas/Link.json index a4178f1..ccad7a3 100644 --- a/schemas/Link.json +++ b/schemas/Link.json @@ -27,12 +27,12 @@ "type": "string", "format": "date-time" }, - "total_bandwidth": { + "bandwidth": { "type": "number", "minimum": 1, "maximum": 1000000 }, - "available_bandwidth": { + "residual_bandwidth": { "type": "number", "minimum": 1, "maximum": 1000000 diff --git a/schemas/Node.json b/schemas/Node.json index 937ec9a..26acd2c 100644 --- a/schemas/Node.json +++ b/schemas/Node.json @@ -22,12 +22,6 @@ "$ref": "port.json" } ] - }, - "private_attibutes": { - "type": "array", - "items": { - "type": "string" - } } }, "required": ["id", "location", "name", "ports"] diff --git a/schemas/Port.json b/schemas/Port.json index 55aedc3..7f3f733 100644 --- a/schemas/Port.json +++ b/schemas/Port.json @@ -18,18 +18,20 @@ "node": { "type": "string" }, - "inter_domain": { + "nni": { "type": "boolean" }, - "link_to": { - "$ref": "port.json" - }, "type": { "type": "string", "enum": [ - "10GE", + "100FE", "1GE", - "100GE" + "10GE", + "25GE", + "40GE", + "100GE", + "400GE", + "Other" ] }, "encapsulation": { @@ -73,7 +75,16 @@ "type": "string", "enum": [ "up", - "down" + "down", + "error" + ] + }, + "state": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "maintenance" ] }, "private_attributes": { diff --git a/schemas/Service.json b/schemas/Service.json index 571a5c1..2f18041 100644 --- a/schemas/Service.json +++ b/schemas/Service.json @@ -22,12 +22,6 @@ } ] }, - "private_attributes": { - "type": "array", - "items": { - "type": "string" - } - }, "monitoring_capability": { "anyOf": [ { diff --git a/schemas/Topology.json b/schemas/Topology.json index 0d2aa26..b79aa71 100644 --- a/schemas/Topology.json +++ b/schemas/Topology.json @@ -23,12 +23,6 @@ "domain_service": { "$ref": "Service" }, - "private_attributes": { - "type": "array", - "items": { - "type": "string" - } - }, "nodes": { "type": "array", "items": [ From 52a3c6c59e6f48452d0f321302f2ea386815e2da Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 8 Nov 2021 08:10:15 -0500 Subject: [PATCH 24/67] added status and state property to the link object --- schemas/Link.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/schemas/Link.json b/schemas/Link.json index ccad7a3..725e95a 100644 --- a/schemas/Link.json +++ b/schemas/Link.json @@ -52,6 +52,22 @@ "minimum": 0, "maximum": 100 }, + "status": { + "type": "string", + "enum": [ + "up", + "down", + "error" + ] + }, + "state": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "maintenance" + ] + }, "private_attributes": { "description": "This property is used to define a list of private properties that will not be exposed to outside users of SDX", "type": "array", From 0f5bfed6659c93b7b6de8872b2f3b8a5820ca9b1 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 8 Nov 2021 12:51:34 -0500 Subject: [PATCH 25/67] packaging for setup --- README.md | 10 +++ models/__init__.py | 14 ---- sdxdatamodel/__init__.py | 6 ++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 557 bytes sdxdatamodel/models/__init__.py | 8 ++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 564 bytes .../__pycache__/connection.cpython-38.pyc | Bin 0 -> 8062 bytes .../models/__pycache__/link.cpython-38.pyc | Bin 0 -> 10327 bytes .../link_measurement_period.cpython-38.pyc | Bin 0 -> 3967 bytes .../__pycache__/location.cpython-38.pyc | Bin 0 -> 4335 bytes .../models/__pycache__/node.cpython-38.pyc | Bin 0 -> 6940 bytes .../models/__pycache__/port.cpython-38.pyc | Bin 0 -> 6673 bytes .../models/__pycache__/service.cpython-38.pyc | Bin 0 -> 6341 bytes .../__pycache__/topology.cpython-38.pyc | Bin 0 -> 9921 bytes {models => sdxdatamodel/models}/connection.py | 2 +- {models => sdxdatamodel/models}/link.py | 0 .../models}/link_measurement_period.py | 0 {models => sdxdatamodel/models}/location.py | 0 {models => sdxdatamodel/models}/node.py | 4 +- {models => sdxdatamodel/models}/port.py | 0 {models => sdxdatamodel/models}/service.py | 0 {models => sdxdatamodel/models}/topology.py | 6 +- sdxdatamodel/parsing/__init__.py | 6 ++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 565 bytes .../connectionhandler.cpython-38.pyc | Bin 0 -> 1667 bytes .../__pycache__/exceptions.cpython-38.pyc | Bin 0 -> 1034 bytes .../__pycache__/linkhandler.cpython-38.pyc | Bin 0 -> 1977 bytes .../locationhandler.cpython-38.pyc | Bin 0 -> 1593 bytes .../__pycache__/nodehandler.cpython-38.pyc | Bin 0 -> 1586 bytes .../__pycache__/porthandler.cpython-38.pyc | Bin 0 -> 1618 bytes .../__pycache__/servicehandler.cpython-38.pyc | Bin 0 -> 1751 bytes .../topologyhandler.cpython-38.pyc | Bin 0 -> 1861 bytes .../parsing}/connectionhandler.py | 2 +- .../parsing}/exceptions.py | 0 .../parsing}/linkhandler.py | 2 +- .../parsing}/locationhandler.py | 2 +- .../parsing}/nodehandler.py | 2 +- .../parsing}/porthandler.py | 2 +- .../parsing}/servicehandler.py | 2 +- .../parsing}/topologyhandler.py | 2 +- sdxdatamodel/sdxdatamodel.egg-info/PKG-INFO | 70 ++++++++++++++++++ .../sdxdatamodel.egg-info/SOURCES.txt | 44 +++++++++++ .../dependency_links.txt | 0 .../sdxdatamodel.egg-info/requires.txt | 1 + .../sdxdatamodel.egg-info/top_level.txt | 4 + sdxdatamodel/topologymanager/__init__.py | 6 ++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 573 bytes .../grenmlconverter.cpython-38.pyc | Bin 0 -> 2417 bytes .../__pycache__/manager.cpython-38.pyc | Bin 0 -> 5678 bytes .../topologymanager}/grenmlconverter.py | 4 +- .../topologymanager}/manager.py | 10 +-- sdxdatamodel/validation/__init__.py | 6 ++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 568 bytes .../connectionvalidator.cpython-38.pyc | Bin 0 -> 5269 bytes .../topologyvalidator.cpython-38.pyc | Bin 0 -> 9673 bytes .../validation}/connectionvalidator.py | 4 +- .../validation}/topologyvalidator.py | 15 ++-- setup.cfg | 30 ++++++++ setup.py | 4 + test/__init__.py | 4 + test/test_connection_validator.py | 3 +- test/test_topology_grenmlconverter.py | 4 +- test/test_topology_handler.py | 6 +- test/test_topology_manager.py | 17 +++-- test/test_topology_validator.py | 8 +- topologymanager/__init__.py | 0 validation/__init__.py | 0 67 files changed, 251 insertions(+), 59 deletions(-) delete mode 100644 models/__init__.py create mode 100644 sdxdatamodel/__init__.py create mode 100644 sdxdatamodel/__pycache__/__init__.cpython-38.pyc create mode 100644 sdxdatamodel/models/__init__.py create mode 100644 sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/connection.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/link.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/location.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/node.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/port.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/service.cpython-38.pyc create mode 100644 sdxdatamodel/models/__pycache__/topology.cpython-38.pyc rename {models => sdxdatamodel/models}/connection.py (99%) rename {models => sdxdatamodel/models}/link.py (100%) rename {models => sdxdatamodel/models}/link_measurement_period.py (100%) rename {models => sdxdatamodel/models}/location.py (100%) rename {models => sdxdatamodel/models}/node.py (98%) rename {models => sdxdatamodel/models}/port.py (100%) rename {models => sdxdatamodel/models}/service.py (100%) rename {models => sdxdatamodel/models}/topology.py (98%) create mode 100644 sdxdatamodel/parsing/__init__.py create mode 100644 sdxdatamodel/parsing/__pycache__/__init__.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/connectionhandler.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/locationhandler.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/servicehandler.cpython-38.pyc create mode 100644 sdxdatamodel/parsing/__pycache__/topologyhandler.cpython-38.pyc rename {parsing => sdxdatamodel/parsing}/connectionhandler.py (97%) rename {parsing => sdxdatamodel/parsing}/exceptions.py (100%) rename {parsing => sdxdatamodel/parsing}/linkhandler.py (97%) rename {parsing => sdxdatamodel/parsing}/locationhandler.py (95%) rename {parsing => sdxdatamodel/parsing}/nodehandler.py (96%) rename {parsing => sdxdatamodel/parsing}/porthandler.py (96%) rename {parsing => sdxdatamodel/parsing}/servicehandler.py (96%) rename {parsing => sdxdatamodel/parsing}/topologyhandler.py (96%) create mode 100644 sdxdatamodel/sdxdatamodel.egg-info/PKG-INFO create mode 100644 sdxdatamodel/sdxdatamodel.egg-info/SOURCES.txt rename parsing/__init__.py => sdxdatamodel/sdxdatamodel.egg-info/dependency_links.txt (100%) create mode 100644 sdxdatamodel/sdxdatamodel.egg-info/requires.txt create mode 100644 sdxdatamodel/sdxdatamodel.egg-info/top_level.txt create mode 100644 sdxdatamodel/topologymanager/__init__.py create mode 100644 sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc create mode 100644 sdxdatamodel/topologymanager/__pycache__/grenmlconverter.cpython-38.pyc create mode 100644 sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc rename {topologymanager => sdxdatamodel/topologymanager}/grenmlconverter.py (95%) rename {topologymanager => sdxdatamodel/topologymanager}/manager.py (95%) create mode 100644 sdxdatamodel/validation/__init__.py create mode 100644 sdxdatamodel/validation/__pycache__/__init__.cpython-38.pyc create mode 100644 sdxdatamodel/validation/__pycache__/connectionvalidator.cpython-38.pyc create mode 100644 sdxdatamodel/validation/__pycache__/topologyvalidator.cpython-38.pyc rename {validation => sdxdatamodel/validation}/connectionvalidator.py (98%) rename {validation => sdxdatamodel/validation}/topologyvalidator.py (96%) create mode 100644 setup.cfg create mode 100644 setup.py delete mode 100644 topologymanager/__init__.py delete mode 100644 validation/__init__.py diff --git a/README.md b/README.md index 5ec4932..de05324 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ - [How to Contribute](#contrib) - [AW-SDX Data Model](#datamodel) +- [How to test and use](#usage) - [AW-SDX Accompanying Projectsl](#accompany) ## How to Contribute @@ -43,6 +44,7 @@ This set of updates mainly come from the domain monitoring system which is suppo ## Topology description schemas There are defined in the *schema* subfolder. Some attributes of each objects are requied (Can be found in the API definition) while some are optional. Two attributes are worth of mentioning: (1) In the *service* object, there is a *vender* attribute for the domain to list device vendors that are NOT in its domain, (2) in the topology, link, node, and port objects, there is an *private* attibute for the domain to list attributes that need to kept private.:wq +## Usage ## Unittest ``` python -m unittest -v test.test_topology_handler @@ -50,5 +52,13 @@ python -m unittest -v test.test_topology_handler ``` python -m unittest -v test.test_topology_validator ``` +## Install +``` +pip install -r requirements.txt +``` +``` +python setup.py install +``` + ## Accompanying AW-SDX Projects diff --git a/models/__init__.py b/models/__init__.py deleted file mode 100644 index f7fff13..0000000 --- a/models/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# coding: utf-8 - -# flake8: noqa -""" - SDX LC - - You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). # noqa: E501 - - OpenAPI spec version: 1.0.0 - Contact: yxin@renci.org - Generated by: https://github.com/swagger-api/swagger-codegen.git -""" - -from __future__ import absolute_import diff --git a/sdxdatamodel/__init__.py b/sdxdatamodel/__init__.py new file mode 100644 index 0000000..3339748 --- /dev/null +++ b/sdxdatamodel/__init__.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import + +from os.path import dirname, basename, isfile, join +import glob +modules = glob.glob(join(dirname(__file__), "*.py")) +__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] diff --git a/sdxdatamodel/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c4eefc5cc59fec9a1d492f49e2db112a596df84 GIT binary patch literal 557 zcmYjN&2AGh5cYVT-R-6k7Y^_Ol@^IC4^S#35XT%^^|DfBIki)a?A>5H>W|V>;J}eL zXpg*-ublb{MJOFFNMxk>`9_{^KF{f7QV}$F{9XLU{40T78zFdy?vF7@BEb*~EkLTN zp*CwX%UWhf?a1bBKJ+tFw1q0}3YDu&l^c+3L+_#8lOvgbC2H>j*+5&#?1a?C1M-p@ z;xjS#y;%jtwO#fu)GQWKyG~dY%enAssB-NW+Nj7r_qv2Qe9$* v?85Z(SmF$xjPqEaf|XG|7Mu%XkR$2d3HxcPJpB9YD4lgsu}@z>3D4mP>%)rl literal 0 HcmV?d00001 diff --git a/sdxdatamodel/models/__init__.py b/sdxdatamodel/models/__init__.py new file mode 100644 index 0000000..329323c --- /dev/null +++ b/sdxdatamodel/models/__init__.py @@ -0,0 +1,8 @@ +# coding: utf-8 + +from __future__ import absolute_import + +from os.path import dirname, basename, isfile, join +import glob +modules = glob.glob(join(dirname(__file__), "*.py")) +__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] diff --git a/sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30cf8be92f3f5e9a7ebc39483a4e59032d0ce231 GIT binary patch literal 564 zcmYjN&2AGh5T5Zm>+L2Hmmc5+kQRw74^UM|RNTmcs+X0JwW*zA-RdNc2cpzOU zFg&XlzO(CIOVg}6*B3kpWn5b~I%JF5>!C`GUmB|e|I(Rue#2kQR@dj``zaVz(epb> zKO?w+4Sag}4SU$&79HQ<9V)m-&wCa-3Kjd#UcgWGm6M<3W-O&?O)n)$&maEocDo{R zQ8Hw@RsPcSpB*P!u)t<-0xo}n8x#L?Ql5G3e0hCk+H!vSzN{}_dv#UDRCQlBozgb4 z|72+Sa&KeEkF4?idDpCtGg9>tQecol4sIIvjzccxa^0_;meR!s2hsW2swQIzvBBNo y*roB1U)XLDGMb^ElQIa}y0+Q}A!ThXX+-;bFo{#y(Z6fQQF3rGVb3wcLwp2BeT=LC literal 0 HcmV?d00001 diff --git a/sdxdatamodel/models/__pycache__/connection.cpython-38.pyc b/sdxdatamodel/models/__pycache__/connection.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d89cb9d40e3b2877effefb1ad6f79510c76b05a5 GIT binary patch literal 8062 zcmb_hOKcm*8Qxtk$>oZ&@c<%3I$@LK-)`Opf5BitfPSz=%L8H=iYnRQ-Pj>9t#vjfj0gAS#r4~R}Phy(#+0# z|Hn7~%>VDZ^YgZbU+V|se>N%EU zX47ot>v^5V3Md$2bV#cf`j9#i#LDyL)Xy5jho$D`Oz7kXSc83RFf9Fjn?C^&3 zAs34V)GL6&)RT=nG3S)Wv8LaS$RX16YS**ltHpj|ro*h!HdG=Lym>s#V*9*MJ zjX~C_*yH!LdKv3H!M>u}=Gl|X zLE9m;J;k0@Z3}z>E1XSgMRtxoqk0zkB6^-pYDM-OJFj{U^TX(=B();Dz@Aqaq3B*%d;8|~ zW#{IWA3BYu7lsZBURSmqs3_5`Be%FSfR{u18}9nsztUaK0T=~^2AlMZLTRFmR0 zTVBxhn$4Z6LQRwlr4TvjFv|IXj4TESNx#J_R%G}rG6S#0qp}}t3Lb`T2VjWic&cLE z?s|dr~m3;Vv)|^_Y_}L8&yAJ6wVjN_e zQP4+!wr=Yd>Z8;~alZ$3q!&l;+gdhnw%CB^AuDD;8lTb5Xq8Z++cAL|OuXao1T-p=ZyxwqrpuBh&K z*#6-3%Wyo+aIJUY&EP|?3CSVva%TfJX3b}77oAo&l*+oSb7!sA4)|Kdi0p6EsOv(s zg_2aoh$W@=KH+adT&rjbNz6S4g8h)6dfws+;$HR=X6&&_Q#0g^SE0j$zNTzEI zX^4k~6dvf+{{tevmmp$49;PutBV$zTH)moOC46V$;$j9aXx)8~A)X`wk*J1{P$xl# z#I{Ld2ZqG26G)`(=a+}Z0ar;xH82`wfGhpVY>?$+kdZeLE&46Gsi^CUOGlY-(5!AAm0WC-NZ?UfjoONDx04(ABu0FU z6cSdoB3Y<{UN(AXQmUZ?I!)S)ChS)UUQ?(04020Z`t-?mutmNIt>yHUBNME2dHV@T zW&qX`DT$wb4 zW5TV~JD$dR;&2{7**{N|{b(kp)jb*?D|}R+L6C_M;_K9u3~J&Ta5>bwa*8MPPHlfm zBr&Vrf069$q4zlhvHNKK04pTy4z5H~{9=0_1v8mCr)a>R6Hcu$*KtV00W|*OMB~HB zSt6L$_i*&!;21R zwsKbE$ri_8trZ`Mm;V~ewHle79ff`K3Uh#(_S6KrN)s$*)_obw-D-mMIL zdieA3<_S3;UD>VSNBnn13LJWZ$(? z837?fH+4fZJ~uu$^J8Zhd3}_W?*4#+ukGqmgQm1?V^6=Peb9T~*fn-@GS}Do#yw-# zlxE+Q`Fn=ACi4<8J!ah3`sO`om9+b+c6(7G{>O3{@qXWYVC~VlAGg!Y652nOCCoJA znGfteEj_z$CgbO1S&jcaVu~0AY8F9Fei!RtOdjw0O`mQP`QEeNQbDIUU=;@np^6R{ z(&vch;@@F@LyqxIaWBHIiinV(ED`-;I_Nus_vw4+=u<4rs6|EDc4_`YBPworp(hc) zy?_~r)$4aB>;)mNMChSv;VLm?inR@|RLGG8%; zrzv7(h^Ikq?zE!$w4!$d-(VEQW=+t=OD!mfP4PT3`oQ2z-kP9S(3U-kQE>=BFMobQ zugoJx?ca?`7%yl}7|jpD-!5&4Fe+|xSS%?b8*mQ*>Y%|Rv+0L2%C)?XxHx=&WRl>b ze7p)qhuw7*6c=PODh1^kMg>H=yGutH^wAZ)sH3oYWUj;dC^vxBRyO)Vh zf(XUE2|}*>Fb-`-c}(JB0v1uhb@^@AeLg|50ZfdGF&;5ReQNdn={d;I~paMJ!Wf&vhf)bz#nMxS(8i=_^Nb z&{Kq1+6^e>MH13HZ5)5SE4fSC?+GQifeGAEaq_4Ta*6Gn`XEHnmiVg>dAV7ihGG+< zFhd#^Hz=cT#wjKhyDA({>Y~#s43Js6SvGL;r2hU{m~NS~j~cpNGIbk$$yoHxSQ=N1o6~JRt$)5&Bf|_nkXCcYkKq_NJ_T z@67!_=X~d!J9E#xHa=cd@cY}jZ`tq7D9WFy(EHO-IE~~TN5+(Sg{e%dtMjV3YV(@7 zX67^J6_#Q8ZH4Jvzm=KKGVP*L&e}7oq7+U1E}s30`O=x9xc_Q%&8*oCbIEBiv$^J* zt4)`i_9ES0e8XN|=B{b`=A|pX-I_U7HH+S}^bxKCy$?qsC%Ww8_!*M(ezrpW?tv6=QJC0=84Mj z%5g#cnP$VcYyO;c6p=bRGRLxXgkjv+_inq%*FLNlZf@ez_R0CSzD~snyWD{ z$Ly9P?`xoVnKvrvR{RiRR;o41dw=67oJR6~3wd${;#WxY^BJazKZsubWm%5pneoHY zyv_=&$i`TSjk6tWf=#lWY>Mq-)1o!YUSNCJ-rMSYj_27vw*R(rOPx2ku~K07$x@yj zVE2obA}^xl0a?nk2iZfSWsHxZ#gwHyJIEduEhTn{J#t%_ALk|Xcr>oXdWYGkM2{VO z2U?EEQl6FBQPDENC(!bkEalk@J0@Bt`6OB@vXp1D?6_#z$#LR5`I}@A6bH497i)%jj=7bWNW=C-? zf4#*$^9XZletFJ(^$Ity@^#bkU>%(yX2V|Pol#_F$)61y^D*+eU0>tSmOFL2jsvT`ZZ92V*%jIiik!8De*&$e*ug%f_$x9ip-WbuZ> z{43!u`?~Ga?ZrCpsm#~0N~5+Oj6;7>oX|q~_I5!8wb@P-Nloco=uj`IcR-%mT+ucFg1Z9(y?L=0sQA_-z`wY?< zB=7IYm>MWnl*1w89zFqiM&!E4vm(!lJTG!X)b%)c~Wf7Tm=kL-Pfg z1*PkOvQY~3J7V?#bE58ckVOwYz}zUk7vxaN23P~7jUbOwF2GtS{X8&G$_H2zrQZYv zl#BptqxAcrh*BXK6Qw@}V<;7ak|_NpD4~QFSdG>`NYI8%u<30*+Ots>J~(m zH(Q5nE3Ng=uq?7p%X*4*>?|@x%c;Zv473?)35l-5QhS`RtHg8H;Q8c_?kNeSCQ}KN z6$OB+?e=-@d%|})cuZKr9Xy|yXmHz>4sg!p{+f&JntbIJqheP%=aL_sGvR6EJZ06L z0x5S@_aR(_aHH%X6^Y8rNS=mFG1_@Sl*4MGOm+CtJte`{eYgo=qE`)f(pQ*vOMOcf zMu$4^nrW|I9L8%gdMY7m=UTRFuY#}9_)D@^Z6AHHfeir?infI%JVY%xY+=S+UGscA z68YvLHy5hS245&^Ve!kfc|7mBO;^aJtVMDWp3MYr1HxHGl}C+=VW@_-O}yWfc$4qG z7sR(5w*}_Ah?PT>nKBcIllS(KV#Esqaa!&ZVD3rGlXNFhr#=zx$=(C@Ah}FKekKKZ znsEsB?js}#5=;!b*vU+)6`7RJo2VKc^tUDGU0;1K&~J0J6itfJ!`i2btuzIoo%7)I%vyXjU!rmR;&v6p`ZqG;sy5;_nBPfFVCHY8_8yC}dE%l6@z zL7yzr8R~l)S;B=YE0I$-m$7Z(*-1UCT%Nv(=cpBRLkl#P*{|GDRpr_vsL8<5>+PEo z{eCv9^xB#PUUZ5uPwBro+(Ife>avtd_2XkH<}YSWwSTeImG#I8kUYbj-NO4Z6Hl?_ zOtcMnRQGG7(LLq+(y|5~VDH7oMww#J+QjRbR4I7P zJ;Y*~p^@+V9WmSSn$tqU18WZlKH)mwm&P+NN52S?kPmc^3~HdVe~!)7pkD-4w+tp@ z)QRdrJD|EhNI`7Zmwq4-PkBMvl-wbnb<}CLM1C*vnua{R+Wtv0Y_#_Y6immOAl~myRP@+C)%_J|v|aTZQWH8arJIsC1pVy{ z-4gMA#7-LGbdK>4$(YeD3B)^Vj)3x;66I8SlgM_uMQEmu84bb5z(kCoIGiz2(Ssn> z{R?R{1ivE@>^V%h7ZkTUd`l$v6I*FW(lJvWujH}Uo)k#-tUCglZ%Z_L#`m+GM6{=C z=h3Ho9@js1G-PMMSd6SVaWhE-A`@>hRJT(yzFjZ~yOgQQ5RZUv0Tx**QfO%jpFgf39sGg2_y)rEJZE_4rg zH>GrlrEhbzN@1JRNwb)!dRrffA5bFpD zBvKEP?u3p|9hXAt*AY6mrgR3$`xP=rxvBcf24Y_;+8sLKX}_-B)NW>cM92c|mUdJ3 z^+5Ntw>0;xpY;(BW7=&c&~M?GA18m>N#)w4pGPDugC4I3`i60bP6VTR8d*U7TYdo} z^=RZq@s83vI?!eRF~21G{}K^7^a3@LpeB10^Po=_Ykd%?IBCkZ4}VF-LZS#t6f$eM zO~fR92SH^U|0V^N6X$vvuzZntr`LKP=UPk!eN$LZy@Q5;f-_n*%==A?#^2S#{1w}y zc&K{FutBcG&bpt~GCweLE2gOd*nWS>E-mKYm?-XvLj$*Fiw9%1E zMB9D}r)ciOpcAKUMD2b-=@LFb5wz;n53#5sCUnK!<0yhm-SPSA0HyAeX!{yz2C0Ce zmj1g(rKqCk+z1Qk@6s4A9Pf+JhxujhlK{e^<2emPf*Um+>a;C|8HCf_Coq**Kh#NN zVK$lty}h-?THW?Mmrh8^IdPsA=3MTr)%{QxbE;OD6;YZngAn>4y(ID$(R&0pil%GR zY7u`rVsYAkN_tV<5tX#uze_o4nN*8f=jE&;{DhS0qaUID^uQEp{)To3Cz(KAZC85r z3PCfcv241$1)33$w<)k6Su&5}wB<~gYhmAS_+h?8PGHOXU%}h)HJ#j-JRUgLU?VzK6aucC3CUqUoo4DE4+p% zr;QJw__Z~=eh!DK&k}{i8iiv?VJz!z6mbi)7{uL#*er`*v#jq)y~Qc39*Zt&OHeLF zM`u%TWa?F|6bbLB1b7&w6dx4SMufaf^qMj|h`TUnSq*MkKj;x(MemMkx)789wB#w?tAgwrW9 O0*@?EFVYFWq5Th=%Y|?N literal 0 HcmV?d00001 diff --git a/sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc b/sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79b986aeab03430f68539b00b23a444c8a17c6f6 GIT binary patch literal 3967 zcmb_f-)|eY9p_P})9E5PZrWsZvu?x+6jj$qX|cTsL69M7f&rcS!zr+-DbDnfvd@<8 z6q0IU?Ti7lv4{N)+Ykf8fb3}x`)lrXz<@sYtw7g)zodOpZMj{Go=qNk_a*Pc4?o{8 zzxuG9f7ZX?9D=EJ?e$ z+OqHN$xJG4q+pwSeTHB)EMO}&+q2DXl21<7a*a>Zs+|DhEtz$}Ecz5^CW8d)#j6IL zTTt|$p^^mXCrJIUCMeSW7drGsT?DpO7cYrWG!MODAR8hQ7Y@mRHwPcSwd8#3k`O+*{%WaT)GyaYg(H?knO&u?F{xq5&IxwY>aJn(h8Z zay?UWA~W-zRB0~edhqW3xBKk=cB)x2=2|mo@R`XOthAztnQh6AcZnelCvRa-rz+o4 zeA0EBawmsYoO4swv{4;e22%^AY?^c;<5`v(RR{J| z0M#HtMo%tmQz*Bg=(nH}UO{44gV3Q(3%Kt#6)Uh;D}a0GbqfkTUwFU-oxin|33iQf z(6^9dCL{*MPDj#&}Wg|Yu8O0UQvMo}bsC1HM+~n#R?oO@rwH%M0J$Xvh1wj1y z-QI^9wAw>a_6F;Bd;HNgEgtu<2yVEA(6hDfjbYE?$+Jy6hR~hvm0=vGKvf*ShGeWj zMQGsRUuetxCrg*vJsFmO!&eu!u~=e}_k!#YAj_No?(##b2=Y3Gp_n8VO!K<&#-cnK>r$w| zQ@q@OM~;`~|0#)oR+6av;0IB~_nghDVnG-XR2F3s;*#OSJR=H}`MqlK;ryq|MNy&p z4!ZbzrHg9xw=i^88r4Xrj;i}D6PwZL z70}f8VIHul!?*rcpbOXMuYO?Tk_&Mz{KJ6;k-(%94np{6B2CO0*Qmjg*P-YuP#K7q z!UKGl;CtQ?yeKew(m~lUd5q(q(z3D5H8)17=U_y~E4|03_%6d6V?Nmw{H6el&!CWb z?;KIOS-UsJdBVr~CN$hgJNfxwUxhgx*fkMWZH&*ZT5`*J;B6pFS>|V6SkDF5AZ`M2u?GWn)VkWTcEz zoo3qbERm%@PBo+k6FybX!&LVBrH>qzbvFxG>)EDFEELA(PGBQk87Qe|V^jKePA@L& zpYZWambG2EcN#D5!2>87PftR9`hpk1pASbT{a?$EycO4@!56In->iBOoxl8zyK#ng z%*m~PU>tV7@q`|cBUGL@?=C7295c4B({|S$PT7_1mAVBot}5*Im0v1t1}P7Cy1J$r~&kgcOm23urM|lhhGI74%0@* zQ?G;RvPg@m3N9>;f4_j|-1Nkmo{6^TaF)!MpaB~83^<%#!IKz6PT}p)UHmCS7hGD+ zBuhtZGdJ5T-`tT2q%s_S!N`Y@jo$6l)XNAN*NPZXV{!b{otI@D21#{FH)RmV@?jkR z0fsoKQxMI$>#c6{aoQ=xFKY%*YtswT;ih zgeWe~puf@C%$5ox7w;sx@o`P6F0q*(V|Xx@F%)t!Y%gABA2>L;4j}Lg9=6 literal 0 HcmV?d00001 diff --git a/sdxdatamodel/models/__pycache__/location.cpython-38.pyc b/sdxdatamodel/models/__pycache__/location.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eab38739281f1139ceee41f6f2b4261095606c5a GIT binary patch literal 4335 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 GW&Z`JUU0Yo literal 0 HcmV?d00001 diff --git a/sdxdatamodel/models/__pycache__/port.cpython-38.pyc b/sdxdatamodel/models/__pycache__/port.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd1f889e6a6abc5b2355f7a701ec3e54bab225c0 GIT binary patch literal 6673 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 literal 0 HcmV?d00001 diff --git a/sdxdatamodel/models/__pycache__/service.cpython-38.pyc b/sdxdatamodel/models/__pycache__/service.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2530e324444c70cc845b5be0a130dee5413c5c54 GIT binary patch literal 6341 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 L*|2rnu#Nu#^5_uO literal 0 HcmV?d00001 diff --git a/sdxdatamodel/models/__pycache__/topology.cpython-38.pyc b/sdxdatamodel/models/__pycache__/topology.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d573cb0ae782d6f22fd5a3d1142b65d01cc337c9 GIT binary patch literal 9921 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 JgNm}t{{?07mtX(@ literal 0 HcmV?d00001 diff --git a/sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc5e37a55dae0167d5ee3aa136815ec389e96725 GIT binary patch literal 1034 zcmaJ=&2G~`5Z<*N#|>>&^~P;MNGL+o4?tD5g*HMWK2Y_N%gXgm!IfjDySAF9=?N~p z1sr)LU%BuM91t^`*lGEjXvQ5s#ngLTx z@sFs$KDZ-}v7=m!UEmly$~$5GPJ)^Dgk!XiYPQe-8*#-jmycZKV7^;#zlzIvSSZMj zrU^{Sw8&@AdIIGzHXxu`6z7E&gSSV~@Qs+n+NAkAn-%rFI*>At#~@|hlX6_BDkFbg z%F8OwmOEW3Rgp;fm1!T5So32Lo*K|5yq>0c7@Zu4@zryqrXi_9Tv!V-idMn$?yv=B zb|%-Fx+FD3S-?HluKYm;lDr^8pmU!LqH^RZPj&FyQ9hE>tsf3kL!BO!rB2VP5>_0& zw%s2H4VP7_L5aA@5lK;HO5{Z;&H!nvawV?bL*CFP;#@!mW5`QeBXJQ-Bjgjxm#Fs1 z=6ud1n>%Rn=@1o<`cA#s-e#Gvw_!Sgqy#0~4SZYAKz6Rzu#~1jTM@6j7}B$)d0I-j zPL~mLjQf1{_@7PhGzSvmx)Ji}a0^-S&uq>v5cq}jg{I7jW>1$z@6dE}i?pN}w7l%z z-0mT2A17@bmkY=QzR&do{QM3dC(4vsN*Zhvpkdsj-_O~j|8-!`M15Zq&6@bugkE>Z y5EFWnOgp3tj|kIr2|0!~amu{FvuhEeUTa6gj-FlrTah~0UHn7#?7R|)E&msrm+|ZX literal 0 HcmV?d00001 diff --git a/sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9487b021a5279950c4906cc234ca980ac0f486a7 GIT binary patch literal 1977 zcmZ`)-EQ1O6rLH6yEItM@?*fxV(t;!?SwTlEVU$Qu^1>VOgj4bbk-iL`5E&e?#Fz9I8MI!& zIkGXJj(?q(yDuoLV6pa1ZcJX@|HN9IZ%(YbzL%-7&8srQ{fh%Nz+tWdQzW61Buvu8 zdrG#bWYT*QCR}pqKOt~uAX^{<8Ok=u)>Fbs5`rF)>)`!2(oz;mAAETw>cf~W`&*Un zk5i2r_3gImXH{9M4Atpt^DdD-7FAe(PwXwq!poB^TI<~%Ss`R zA>b%d6Tu|8MV`}*f#-ZP87u8VA@VY}LO5Q4g6Jrtimm6=wS~xv)EFV2lfQ2aZyTk} zaDOi^ha0y(AEuA48o4(_lBrEcl~e_;UM{8v4e_(vr*^N8_jL$bbx=JHjL?W4oL}(F z`b?b+&6tg4nDl@?1u~z3kq3X(RMIta$UriX9!L))2gyPDk{kv=Euavn4HN-&fMTGo z^lN%dPRMSc@4y)!&IB@;V>^HafVE_6jy(pf1z0G0qX!3%Xp6c0ay%J_bw?v;Kb71=x3@gQ*zj&c4(oOu)p6aagSsnw&*HWLfBhP= z6=ay$`gLS)AX`PYhU@~ebI2|N8~Bdzs(s_auhjl^tt+iD=NwPrN z|96#ZqYoMvB%T$UiIs5z1(I`Y;a7V&S1N;IF2buF$twk1{Cu>GMzE9b8&0rnA@nr>M{usBBYI@4>_p zNWP|}J@bCLbjU0{W;HvZP+a8LtGyEn9fV`DeMfGRdysOFM0~?hec(^**41m!JVFS6 zC83X@^Q*Bc9miG$&814Ez5=&24jS;Ok) zH;sgVo;ZQ>5H1$t!6YrFZ(2ggDifmVgr_cf8$PYy0p_~XrkkmvfGsaQfh|*`LfQw0 zWH5C@#(B)ce&{#&Akc3Dnp{}y#OqCSHtT3allCrBv)`R*N}@TtaM^f&q5S?8vDN2=N7f69P5E{YFp*q9@il*?PC{hGz3)dCU7(rZK77Tag(56Ub zcZJ#l)kAC`=ppE}NBii%#A{Fa3q5s)mTXlh&;e$4ceo#K-pugD+FA=xI?sQPcLl&- zv{)7o7f(>_7ibhHmO(@#&)A4ZoPqL`$h?t=1cUEE`6@UE6`b+NS8N}8jSqMZ20d1V zdqonvw8%d&^q^z&r_|asKm67?lfIfb{p=*sW7?6>nPnjqBz6alhKMPMxMGp_4u(u| z<(-F-P(u0VfbRyXfn%UT)x@#!4g^FYVrfpvI~i+Jg;J(@>ZGi~Y)%w%B&^PcSFCDEnPjoGQeMH|d;J$y8{0oUN%Q_- z|GR$t>m#d9`lKp$@u*Nbqt)$V9#nsE_3P{Sx5uX@M06h!OiVujYq9CQ6_&Oa&=Lx1 zAq|{R?L0-bf1;W0DVSkkGY?fLRkn;m@qU5DlVlBM)WBg#q-WMK2=g(S4KzK!636-!kNl`lpBYmaLkv|S zHiy;_6TOBZ0xCf~qaUBB1l?MB_)5jJY2s(Ic4O|)?z`z|To@-8sgQJYdzmi0V2}GTwFLC04l0hx=-ut67=B}<80)+e)CLrLtjv9Ur4GC& z=JX8H4C$sYB~8sIv}1*p^_5T_;2s;G0Sq2%^1=Uvp3v51p`6xH(io11^=)z<;t5+Tr)vkbS~GDPiZ2%PcJkXkCdkPFck;YR4!{$5}{MQ_wu?)>fg oXJEG%)m9g7)zXV!t*wi?KEL$fEgRw2*O7cfeAWq}s%-Or0HLjbzW@LL literal 0 HcmV?d00001 diff --git a/sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b48222f482a0ecdae39067d301cf7fd02791b15c GIT binary patch literal 1586 zcmZ`(OK;pZ5FS#LR=c)aCpFMG4N##EAbW@x2wK30qUbAa(4Y%6Mi56ALKV5TY^}5* zxvjm>_D~xLdI);!**^L&@!C`VLQkEMy~)-=*R0;0I8licUdACp--m+kt-P74Cy!pLsE? zm43x=goDkW3TumM?`!8w@ypcd2S>S{xT3CdTE8sh0g2s4r6FYsQm$AUynqo?Tm`3b zDwI&+Dd3%v>Y$BOth#7BFF-&VqaQuLLiaz+DphJT`|+b*f^s!XMzu*MnIVr7x381D zt}30AJBiWHr`oziX`7qEO=zyDk^_sjJtL=(7D9cFjhwa{s2rTY89N6A3L21H!46!o zVL$Mpoldmzv6MwsI4QlT&_M#q>T-0!d{@f6%&e920{(qCcw)7&gX5#38Vq;7A7sDX zv+8I-O=T_{(c18-VrLY-0(Tt%W-|g|QII5o+IJ zvwx$Seb+F>Het@ua30IfhP(1vJ3w2^{&M@0=ggL62H%r`h6;e6*QRNDd^JM5W; z!8~R#yX|-r9|Wqi2@89||6otya|CZ5{~qtcGpP73IMMJXXhah>QPWZJ%V^$hy2x^O z=y{Rfby7D--6ZuM zDdMvqdU2?at&bn+;|Ipn#t@EPWMLUcOxLz-eufSLlq2Qu{6iC#h?0(TZz0tSp6*;C~yT0t^_F_Wieno70kIAy(T+ zX|sa^e3cA!`@Hq!IsvT{3CYKJwD|;;U%RUIT$d#-d4mMisv(C=h>Dt}{EE2{t0KM;hwXen wG9Th>dTaR!*lJ$|yG41_=BDrI#XD=;ylwX`UA7ec7nf0dN%*zE$CPdSKLVg|dH?_b literal 0 HcmV?d00001 diff --git a/sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15b164a3b59458d22bb03b642a62102c85a40eb5 GIT binary patch literal 1618 zcmZ`(QEwYX5T4!JJD;5dLR$er5wa=}_@S{>6)8dpfl#3WVbO-F;a*nP+jVlu`Oe*4 zLt>qIAdxC0D*gZu$s2j+*X%1#{R=!XbFo_!(Xn=Bcf7Yd^L;b>aBZyvD1Y7mHT^>X z{7s8x32^Zl4tpDw0>uhQ$nt`Xc)}T|K#3w4i9|5?3RI|~Q&7*40ZL|fNY+GZwqWAf!?_rRiU&d4aGh0vViprGvyR1QwyjGY4l1vN;n zUSB1od|S$_NUfFf0{*$ze`vL_{o|v&><@On z?5987vg)W$L!~YqRZ17MdbyY<)Td+jFZ13SADbAXTA}ceZUEL{(;G|3Y%RjcD2;_s zjnMc4hy4@P^!u7AwgEGamZJ^O251FZfi_eyi&R*%GdPFC$b5|c5d9JQTPk|te}Mi7 z{jEn(ZVPbiuw#B>FpC*X-*LQ-4+7QNfQ3EbzpzK}5rR33e~x$I2PpY2I8pODs6`#t zcz)ed@w3*fUAK|<_Q3O8c~Pb#?K^fKv5?JNl!%yLDbmNfkR~nnw7)hs`H!j7GF?=q zHBG&?%~##m z=moL#e}^Uk2IGUVOg+=4jeE`73-ua(x}J}SP}wLthC;LNq5{TQJpT!{iZjxyY@L0` z-{jNHWtOiV@*lP-Xu7&#`wmVVAWKcJJqv!{oN>p_c+JllvJPiK9h@^PZ9%-;hX=5W z>>%=D;F+0*6F0oIja41%GOH9e7@<^+b?F85kQYUjDzk~VH0k!l9HC)eXy2v;B~8r+ z?YM%)%2FCP@qZhk0!$c-`mI-_o6(kKA%>ew>0<{6_-YxN_IMM?RT7#I8YbdBeM!Ry zxZ5`Ea;29R8+vVOb1FM*^Ny6*tdoM~t5QChq{aN7mXxZ>q-W6YouSH=79I)qA$TdA8qs-?R70@^S~Tefs#f z_zxf8Z<;JN4=3NC*!!pyD3(D)JpHgeneD~X`GU>Bnr5_w7x~$S7PiVq zMc!-JVTvp7IE;i6%0C7?Hc&0>0~M+^_N~_-APUiyj=Mf};fFX^nKt7e?{>P_rqk|D zVY+2($f<7iTz8Wq&vil`b&Y;8)VAs>Z4;ALC1Gja-Lu#?dn6RqtkIaGkl*$ustS(a zgq;GW3u=&D!4{mc&7S9cJ1n(vp_FN!R#Li_Jx>#6NLZcioH5syGRb0Vr96Xw9`&D~ zh3$_H)4adA^{^kmyl2&6pG3t~JSddTXm&A~#?+tg{QNxn^>SoF^z9lsDNq6HuoNRA>R;CvHgzulffipFuq;!I^6SAYYk@o z5r4&=!k5UxB>XkphF>7(+fa$xt9jk3MeWyEH*bPTyKZB}v^Sk!yv&iGTZ01WDGZi| zOyV+rmS$-+a;v2&4pNKNKp*VLR(jx8E`dXnx!^$OsxaczZE>ISJ*R6XB`eLb=+e56MYj46yE!kmcBt~JC=uVaW1Cy4cCpU4}?B=SmW zBfpgBMYEGbAa+cfHd>qIxw=k^SJOdRm`YCTMABa5lKmVNFwR0I@G><&wuH0Gvcy){ zXZ$7~-@Q^p|FP|*a-vwspQfC?z(EB_Yjd&Rc)x!z;T1dKH9ut&uZ9z^_D&gIB0)s{ z43A+O=|r-`rekL8538Mf8+ds_ohOAtS}1u%sdFdjwmOj&u`+k?lqLr~F-_b~nrYXj zkR=Vx`($w?LDw%w@(G&Q02N^5cy~Vh-?$SpTx`UA6Eba4!5%)BhK4=fxOA1G#+Qbt zYrA?*QV$o{=5Dg_?JMUmnziYP$>V{>C#EAMz7oTX#!FJZ7{=N3OG`>sBvLjH!+DZE z#sdsR>XxU^P@=O8O)hdnFNh(}4E}=viKSu;xe!YtycPNl-V4k}h(>o7-=_8Exw7li kl51+&b@c3?z&3T>EMGY9A`{_P=Yf1pxooJQqFmzt0$5+GbpQYW literal 0 HcmV?d00001 diff --git a/sdxdatamodel/parsing/__pycache__/topologyhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/topologyhandler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f337b9a0597faa75490a7fa8674b7b6a4cb5566a GIT binary patch literal 1861 zcmah~PjA~c6elUll2toNgAGg4Z1swvms4RU~(V2{u+cNl4c}A%`!S<5u-#pl4s74M;yj9a=$0im5mc38^-@zau5tSrj z@S-EzugQQSj+17@<$`@5kVsGZCj>AwWdPcf*JKN{|C(?TH38S6S*-E=5a%*W)a3rH zpaX3>?F$4fqIL3}F>NOB0IVPv{ zj8Ib1iU=kNNJo-wa!z|8H?AI!5@mcL(mXANFzxzDF-Wt-`1SOh8LpFT5M8fV?1nrZ zCg;>Nh3IFo)O|@8;X#^rd)r&x_}4pH9(1wGxQvH|OfsBZO{O+?>$_iE z(6cc*R6bbIMspe<2n}e1PF8lNyi9r9sNuhtjBoKYMxCVqf@zy5yey=u2})f8pk2@|=mzKpXb-dp+J8p!yBrvN8K{zyqb4PjwUSoN zeJ1@?QtM;(J4OB^?D(y@OMWIf+a;w_vC64<`?)1piCbGE`JpZ$*`1xCj^@r-t2x8B5D0{&*w${Ii~446x-)?RWIj$gpZ zI;Ga$6pC!xlEZQ74;q zrU{bzA4Xe8NA1TCdJnfYw|B(W!$+I;x)}r(i!DL^y$k!kL83L5L7%p0i*fcZq7l4J literal 0 HcmV?d00001 diff --git a/parsing/connectionhandler.py b/sdxdatamodel/parsing/connectionhandler.py similarity index 97% rename from parsing/connectionhandler.py rename to sdxdatamodel/parsing/connectionhandler.py index ca49e50..8ca89f9 100644 --- a/parsing/connectionhandler.py +++ b/sdxdatamodel/parsing/connectionhandler.py @@ -34,5 +34,5 @@ def import_connection(self,file): data = json.load(data_file) self.connection = self.import_connection_data(data) - def get_connection(): + def get_connection(self): return self.connection diff --git a/parsing/exceptions.py b/sdxdatamodel/parsing/exceptions.py similarity index 100% rename from parsing/exceptions.py rename to sdxdatamodel/parsing/exceptions.py diff --git a/parsing/linkhandler.py b/sdxdatamodel/parsing/linkhandler.py similarity index 97% rename from parsing/linkhandler.py rename to sdxdatamodel/parsing/linkhandler.py index 1ffc931..7deef52 100644 --- a/parsing/linkhandler.py +++ b/sdxdatamodel/parsing/linkhandler.py @@ -1,5 +1,5 @@ import json -from models.link import Link +from sdxdatamodel.models.link import Link from .exceptions import MissingAttributeException class LinkHandler(): diff --git a/parsing/locationhandler.py b/sdxdatamodel/parsing/locationhandler.py similarity index 95% rename from parsing/locationhandler.py rename to sdxdatamodel/parsing/locationhandler.py index e1d6e67..e9830ad 100644 --- a/parsing/locationhandler.py +++ b/sdxdatamodel/parsing/locationhandler.py @@ -1,5 +1,5 @@ import json -from models.location import Location +from sdxdatamodel.models.location import Location from .exceptions import MissingAttributeException class LocationHandler(): diff --git a/parsing/nodehandler.py b/sdxdatamodel/parsing/nodehandler.py similarity index 96% rename from parsing/nodehandler.py rename to sdxdatamodel/parsing/nodehandler.py index e7ce39f..5692ae9 100644 --- a/parsing/nodehandler.py +++ b/sdxdatamodel/parsing/nodehandler.py @@ -1,5 +1,5 @@ import json -from models.node import Node +from sdxdatamodel.models.node import Node from .exceptions import MissingAttributeException class NodeHandler(): diff --git a/parsing/porthandler.py b/sdxdatamodel/parsing/porthandler.py similarity index 96% rename from parsing/porthandler.py rename to sdxdatamodel/parsing/porthandler.py index 8d54b17..11f7962 100644 --- a/parsing/porthandler.py +++ b/sdxdatamodel/parsing/porthandler.py @@ -1,5 +1,5 @@ import json -from models.port import Port +from sdxdatamodel.models.port import Port from .exceptions import MissingAttributeException class PortHandler(): diff --git a/parsing/servicehandler.py b/sdxdatamodel/parsing/servicehandler.py similarity index 96% rename from parsing/servicehandler.py rename to sdxdatamodel/parsing/servicehandler.py index 14258d0..486a339 100644 --- a/parsing/servicehandler.py +++ b/sdxdatamodel/parsing/servicehandler.py @@ -1,5 +1,5 @@ import json -from models.service import Service +from sdxdatamodel.models.service import Service from .exceptions import MissingAttributeException class ServiceHandler(): diff --git a/parsing/topologyhandler.py b/sdxdatamodel/parsing/topologyhandler.py similarity index 96% rename from parsing/topologyhandler.py rename to sdxdatamodel/parsing/topologyhandler.py index 6b568a4..a4fc138 100644 --- a/parsing/topologyhandler.py +++ b/sdxdatamodel/parsing/topologyhandler.py @@ -1,5 +1,5 @@ import json -from models.topology import Topology +from sdxdatamodel.models.topology import Topology from .exceptions import MissingAttributeException MANIFEST_FILE = None diff --git a/sdxdatamodel/sdxdatamodel.egg-info/PKG-INFO b/sdxdatamodel/sdxdatamodel.egg-info/PKG-INFO new file mode 100644 index 0000000..71dbeff --- /dev/null +++ b/sdxdatamodel/sdxdatamodel.egg-info/PKG-INFO @@ -0,0 +1,70 @@ +Metadata-Version: 2.1 +Name: sdxdatamodel +Version: 0.0.1 +Summary: UNKNOWN +Home-page: https://github.com/atlanticwave-sdx/datamodel +Author: Y. Xin +Author-email: yxin@renci.org +License: UNKNOWN +Project-URL: Bug Tracker, https://github.com/atlanticwave-sdx/datamodel/issues +Description: ## Table of Contents + + - [How to Contribute](#contrib) + - [AW-SDX Data Model](#datamodel) + - [AW-SDX Accompanying Projectsl](#accompany) + + ## How to Contribute + + 1. Ensure you're able to run the existing code in your own [development environment](#setup). + 2. Create a descriptive [GitHub issue](https://github.com/atlanticwave-sdx/datamodel/issues) that outlines what feature you plan to contribute. + 3. Clone the repository, and start from the most recent version of the [develop branch](https://github.com/atlanticwave-sdx/datamodel/tree/develop). + 4. Name your branch using the Github issue number as a prefix along with a brief name that corresponds to your feature (e.g., `8-how-to-contribute`). + 5. Once satisfied with your completed and tested work, submit a [pull request](https://github.com/atlanticwave-sdx/datamodel/pulls) against the **develop** branch so that your code can be reviewed by the team. + + Notes: + + - Do not create a pull request against the **master** branch. The **master** branch is considered the production branch and must always remain stable. The **master** branch is periodically updated from the contents of the **develop** branch at the conclusion of a development cycle. + - Do not put any content (css, js, images, etc.) in the main `static` directory, instead create a directory named `static` in your app that can be imported into the main `static` directory using the `manage.py collectstatic` call. + - Use clear and concise naming conventions for apps, classes, functions, variables, etc. Ideally others will be able to reuse your work, and the more clear and concise your code is, the easier it is to reuse it. + - Include easy to understand documentation and complete unit/functional tests for each new feature being introduced to the project. ([pytest](https://docs.pytest.org/en/latest/) is the recommended framework to use for testing). + + ## AW-SDX Data Model + + ## System + Each domain, proxied by the customized SDX-LC who communicates between the SDX-controller and the domain (1) provisioning system (eg, Kytos) and (2) monitoring system (BAPM). + + ## Topology Models + In the whole SDX system, two types of topology models are needed: + ### Domain substrate description model + It's used by the intra-domain prvisioning system. + ### Domain declaration/advertisement model + Based on the information from the domain provisioning system, tt's abstracted/generated/passed by the SDX-LC to the SDX-controller for inter-domain topology assembly to support (a) inter-domain path computation; and (b) inter-domain path monitoring and reconfiguration. It would consist of three types of information: (1) Topology abstraction; (2) network resources available for inter-domain connections and their QoS metrics (eg, bw, latency, packet loss, vlan ranges, etc); (3) switching capability (eg, vlan, Q-in-Q, etc). + + There is a *service* attribute in the topology object, which is an object that describes domain service meta information like owner, provisiong system, and security features. + + ## Domain topology and state update + ### Topology update: + On the events of addition, removal, and/or maintenance of ports, nodes, links, an updated domain topology with version and timestamp needs to be sent to the LC and subsequently the SDX controller. A new topology object is supposed to be generated and passed on to the SDX controller. + + ### Topology link state update + This set of updates mainly come from the domain monitoring system which is supposed to stream periodical measurement information on the links, like bandwidth, latency, and packet loss. A new link object is supposed to generated and pass on to the SDX controller. + + ## Topology description schemas + There are defined in the *schema* subfolder. Some attributes of each objects are requied (Can be found in the API definition) while some are optional. Two attributes are worth of mentioning: (1) In the *service* object, there is a *vender* attribute for the domain to list device vendors that are NOT in its domain, (2) in the topology, link, node, and port objects, there is an *private* attibute for the domain to list attributes that need to kept private.:wq + + ## Unittest + ``` + python -m unittest -v test.test_topology_handler + ``` + ``` + python -m unittest -v test.test_topology_validator + ``` + + ## Accompanying AW-SDX Projects + +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown diff --git a/sdxdatamodel/sdxdatamodel.egg-info/SOURCES.txt b/sdxdatamodel/sdxdatamodel.egg-info/SOURCES.txt new file mode 100644 index 0000000..ad95d98 --- /dev/null +++ b/sdxdatamodel/sdxdatamodel.egg-info/SOURCES.txt @@ -0,0 +1,44 @@ +README.md +setup.cfg +setup.py +sdxdatamodel/models/__init__.py +sdxdatamodel/models/connection.py +sdxdatamodel/models/link.py +sdxdatamodel/models/link_measurement_period.py +sdxdatamodel/models/location.py +sdxdatamodel/models/node.py +sdxdatamodel/models/port.py +sdxdatamodel/models/service.py +sdxdatamodel/models/topology.py +sdxdatamodel/parsing/__init__.py +sdxdatamodel/parsing/connectionhandler.py +sdxdatamodel/parsing/exceptions.py +sdxdatamodel/parsing/linkhandler.py +sdxdatamodel/parsing/locationhandler.py +sdxdatamodel/parsing/nodehandler.py +sdxdatamodel/parsing/porthandler.py +sdxdatamodel/parsing/servicehandler.py +sdxdatamodel/parsing/topologyhandler.py +sdxdatamodel/sdxdatamodel.egg-info/PKG-INFO +sdxdatamodel/sdxdatamodel.egg-info/SOURCES.txt +sdxdatamodel/sdxdatamodel.egg-info/dependency_links.txt +sdxdatamodel/sdxdatamodel.egg-info/requires.txt +sdxdatamodel/sdxdatamodel.egg-info/top_level.txt +sdxdatamodel/topologymanager/__init__.py +sdxdatamodel/topologymanager/grenmlconverter.py +sdxdatamodel/topologymanager/manager.py +sdxdatamodel/validation/__init__.py +sdxdatamodel/validation/connectionvalidator.py +sdxdatamodel/validation/topologyvalidator.py +test/test_connection_handler.py +test/test_connection_validator.py +test/test_link_handler.py +test/test_location_handler.py +test/test_node_handler.py +test/test_port_handler.py +test/test_service_handler.py +test/test_topology_graph.py +test/test_topology_grenmlconverter.py +test/test_topology_handler.py +test/test_topology_manager.py +test/test_topology_validator.py \ No newline at end of file diff --git a/parsing/__init__.py b/sdxdatamodel/sdxdatamodel.egg-info/dependency_links.txt similarity index 100% rename from parsing/__init__.py rename to sdxdatamodel/sdxdatamodel.egg-info/dependency_links.txt diff --git a/sdxdatamodel/sdxdatamodel.egg-info/requires.txt b/sdxdatamodel/sdxdatamodel.egg-info/requires.txt new file mode 100644 index 0000000..92b69d7 --- /dev/null +++ b/sdxdatamodel/sdxdatamodel.egg-info/requires.txt @@ -0,0 +1 @@ +python-dateutil~=2.8 diff --git a/sdxdatamodel/sdxdatamodel.egg-info/top_level.txt b/sdxdatamodel/sdxdatamodel.egg-info/top_level.txt new file mode 100644 index 0000000..9ee0aa8 --- /dev/null +++ b/sdxdatamodel/sdxdatamodel.egg-info/top_level.txt @@ -0,0 +1,4 @@ +models +parsing +topologymanager +validation diff --git a/sdxdatamodel/topologymanager/__init__.py b/sdxdatamodel/topologymanager/__init__.py new file mode 100644 index 0000000..9bc366c --- /dev/null +++ b/sdxdatamodel/topologymanager/__init__.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import + +from os.path import dirname, basename, isfile, join +import glob +modules = glob.glob(join(dirname(__file__), "*.py")) +__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] \ No newline at end of file diff --git a/sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40f530ecfc21bb836926303d0a67b6fa7a8d9802 GIT binary patch literal 573 zcmYjN&2AGh5cYVT-EGo{3kP@sq(vgj15_0fhzlZzR=uo*ET?vYk-fXvj@ne=6gY6? z4ca5GD(;b& zR1hDDh40KdD6VbaxlplKNbOo-RV)|6tD(xZUuvTw`_$=ne#4&6`pqRIiho8iC(W;pKImT#6uUCzVO) zKL}mlnQpbQq7^IU>VvQ2_{3-*E<4+w=1A{L3^}2sBxB;HNx&qIIbW{B+9}Rm+B_DW zpY;M2W{3uvgLIb|B3qem5lfuG6LKC4RJ1nA$AWWV405DB4#IJoDo_4CJ56UER2BV*mgE literal 0 HcmV?d00001 diff --git a/sdxdatamodel/topologymanager/__pycache__/grenmlconverter.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/grenmlconverter.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..332b1702e9912ec8e2e5111be087a185584714e6 GIT binary patch literal 2417 zcmZ`)&5zqe6rb_86DPY{K1xdqDFR9)*yV^2swx#)klF~k+lvLV+>Dz#b?n`-`;nD% zYA+mm;oL*^xc|~zIq@%W;`he!b`up_GxOfe^URyy@4e>-yXT z@-cmV{xPB4L@R$rm#~lv78;2u_!}+^VZO3LYo%GjhGsWDg^uuq|B8jK=!h;xPXwZe z(HDJqxuv%5$RwX-w~G8(BCAB6!ij3-(AdCP?L#uPTWIBn=xScGh*iAiQ{yGcreOQ* zl#hm%^sz34bH*Kh{L#j>cU!an zzcWQsb7#7zBvm4=nCwwUS7^FzNGd3kDr()MwNhirkFRVVeBN-(Vc9;!IQgpq7Z7ST zWlMg-78>$}0o`0$wQ+)&)v{e1&}g`ug$XVpidmfI5vX~VjuT}U&+|me0RDtK!ai{_#E!g%d)c4SdN4b9PU09bJE0fF@Wsj2*|$R|Kj zB+AQB7qsNUNG4@D?5Kbc%~mBtUWPZyNQEXxc`fjgB42gN$Atv^$VT}ICgi8o4bUN7 zHK2AwjYR{FYj<4_Z21AcxQ13z=riu&?epVHn*g?lmuuir&QR0fy@8;?`)gqczAym} zTUZmeuxksD{=T3P=1Ze?PWZyDUBRCC@=@(BO~C9`R_y{%7l66|)B~X2)JN+Os0+Wg zm)+W@;6J)t1;VVIh7e&b%%jV-Sz6e=H|?L%Vqh0r*srZKLY&cUc@<6`|90>=EeD6d zZy<`K%&%92hY3+LsER?H&oy6zG#^xt6HGPL{OduMK1s4U3TRwa47vOQ zTcZHmv~SrY+;*EG(ZhK}^OB&JR0-r~)De~PbL!SPR8D+&nB+poK$D!Bf6A+2XT@)m zZx#(|2bq+}Ho0j^euZt7k4;uNYH0_)q{FVG3%ir1hEsfOB$~Vk12QVFprfd;kYE`$rk9DcSnl$Y_0fFA*7}w#eo2}+NuleE@1`(#qEYGs!JHYm}OKlzQcN3ct73@ z(Q|eqZ8~pf3bD5|tb2 zqNAEoQq}Z~zSTFqEzj|G^vY-$gieF4{$+;mHOj93;g!2OEp-fRu4vdKRU~!g{C~0i rvJFM;clb_|vot4+l~X*NCgV!G^{$&J?}rAN8^ldXsT_b}kc;;}MI%;) literal 0 HcmV?d00001 diff --git a/sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd0b69ab7f1d0c85174c42161fb37699f2679110 GIT binary patch literal 5678 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 DMCy-W literal 0 HcmV?d00001 diff --git a/sdxdatamodel/validation/__pycache__/connectionvalidator.cpython-38.pyc b/sdxdatamodel/validation/__pycache__/connectionvalidator.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cf989f089904da5e5ae676a35ab97079134b463 GIT binary patch literal 5269 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 literal 0 HcmV?d00001 diff --git a/validation/connectionvalidator.py b/sdxdatamodel/validation/connectionvalidator.py similarity index 98% rename from validation/connectionvalidator.py rename to sdxdatamodel/validation/connectionvalidator.py index ba16407..a297d32 100644 --- a/validation/connectionvalidator.py +++ b/sdxdatamodel/validation/connectionvalidator.py @@ -4,9 +4,11 @@ """ from collections.abc import Iterable from datetime import * -from models import Connection, Port from re import match +from models.port import Port +from models.connection import Connection + ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' ISO_TIME_FORMAT = r"(^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:Z|[+-][01]\d:[0-5]\d)$)" diff --git a/validation/topologyvalidator.py b/sdxdatamodel/validation/topologyvalidator.py similarity index 96% rename from validation/topologyvalidator.py rename to sdxdatamodel/validation/topologyvalidator.py index d80513a..c5e91f7 100644 --- a/validation/topologyvalidator.py +++ b/sdxdatamodel/validation/topologyvalidator.py @@ -5,12 +5,15 @@ from collections.abc import Iterable from re import match -from models.topology import SDX_INSTITUTION_ID, Topology -from models.service import Service -from models.node import Node -from models.link import Link -from models.port import Port -from models.location import Location +from sdxdatamodel import * +from sdxdatamodel.models import * + +from sdxdatamodel.models.topology import SDX_INSTITUTION_ID, Topology +from sdxdatamodel.models.service import Service +from sdxdatamodel.models.node import Node +from sdxdatamodel.models.link import Link +from sdxdatamodel.models.port import Port +from sdxdatamodel.models.location import Location ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ddee3fb --- /dev/null +++ b/setup.cfg @@ -0,0 +1,30 @@ +[metadata] +name = sdxdatamodel +version = 0.0.1 +author = Y. Xin +author_email = yxin@renci.org +description = +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/atlanticwave-sdx/datamodel +project_urls = + Bug Tracker = https://github.com/atlanticwave-sdx/datamodel/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +include_package_data = True +package_dir = + = sdxdatamodel +packages = find: +install_requires = + python-dateutil~=2.8 +python_requires = >=3.6 + +[options.packages.find] +exclude = + docs + test* +where = sdxdatamodel diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..37c8dba --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ + +import setuptools + +setuptools.setup() diff --git a/test/__init__.py b/test/__init__.py index e69de29..1cb91d4 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -0,0 +1,4 @@ +from os.path import dirname, basename, isfile, join +import glob +modules = glob.glob(join(dirname(__file__), "*.py")) +__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] \ No newline at end of file diff --git a/test/test_connection_validator.py b/test/test_connection_validator.py index 9c25c35..0a7582c 100644 --- a/test/test_connection_validator.py +++ b/test/test_connection_validator.py @@ -5,6 +5,7 @@ from validation.connectionvalidator import ConnectionValidator from parsing.connectionhandler import ConnectionHandler from parsing.exceptions import DataModelException +from models.connection import Connection CONNECTION_P2P = './test/data/p2p.json' @@ -14,7 +15,7 @@ def setUp(self): self.handler = ConnectionHandler() print("Import Connection:") self.handler.import_connection(CONNECTION_P2P) - conn = self.handler.connection + conn = self.handler.get_connection() self.validator = ConnectionValidator() self.validator.set_connection(conn) diff --git a/test/test_topology_grenmlconverter.py b/test/test_topology_grenmlconverter.py index b90a4a7..9a7c9a1 100644 --- a/test/test_topology_grenmlconverter.py +++ b/test/test_topology_grenmlconverter.py @@ -1,7 +1,7 @@ import unittest -import parsing -import topologymanager +import sdxdatamodel.parsing +import sdxdatamodel.topologymanager from validation.topologyvalidator import TopologyValidator from parsing.topologyhandler import TopologyHandler diff --git a/test/test_topology_handler.py b/test/test_topology_handler.py index d708d5e..4e268cc 100644 --- a/test/test_topology_handler.py +++ b/test/test_topology_handler.py @@ -1,9 +1,9 @@ import unittest -import parsing +import sdxdatamodel.parsing -from parsing.topologyhandler import TopologyHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.parsing.topologyhandler import TopologyHandler +from sdxdatamodel.parsing.exceptions import DataModelException TOPOLOGY_AMLIGHT = './test/data/amlight.json' diff --git a/test/test_topology_manager.py b/test/test_topology_manager.py index 75615c4..d03d4d6 100644 --- a/test/test_topology_manager.py +++ b/test/test_topology_manager.py @@ -4,14 +4,15 @@ import matplotlib.pyplot as plt import networkx as nx -import parsing -import topologymanager +from sdxdatamodel import parsing +from sdxdatamodel import topologymanager -from validation.topologyvalidator import TopologyValidator -from parsing.topologyhandler import TopologyHandler -from topologymanager.manager import TopologyManager -from topologymanager.grenmlconverter import GrenmlConverter -from parsing.exceptions import DataModelException +from sdxdatamodel import validation +from sdxdatamodel.validation.topologyvalidator import TopologyValidator +from sdxdatamodel.parsing.topologyhandler import TopologyHandler +from sdxdatamodel.topologymanager.manager import TopologyManager +from sdxdatamodel.topologymanager.grenmlconverter import GrenmlConverter +from sdxdatamodel.parsing.exceptions import DataModelException TOPOLOGY_AMLIGHT = './test/data/amlight.json' @@ -62,7 +63,7 @@ def testGenerateGraph(self): graph = self.manager.generate_graph() #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout nx.draw(graph,with_labels = True) - plt.savefig("amlight.png") + plt.savefig("./test/amlight.png") except DataModelException as e: print(e) return False diff --git a/test/test_topology_validator.py b/test/test_topology_validator.py index ee16c73..ecc87f9 100644 --- a/test/test_topology_validator.py +++ b/test/test_topology_validator.py @@ -1,10 +1,10 @@ import unittest -import parsing +import sdxdatamodel.parsing -from validation.topologyvalidator import TopologyValidator -from parsing.topologyhandler import TopologyHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.validation.topologyvalidator import TopologyValidator +from sdxdatamodel.parsing.topologyhandler import TopologyHandler +from sdxdatamodel.parsing.exceptions import DataModelException TOPOLOGY_AMLIGHT = './test/data/amlight.json' TOPOLOGY_AMPATH = './test/data/ampath.json' diff --git a/topologymanager/__init__.py b/topologymanager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/validation/__init__.py b/validation/__init__.py deleted file mode 100644 index e69de29..0000000 From 666e127f780353e1f41b38ba536eb176f0059943 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Thu, 13 Jan 2022 11:25:43 -0500 Subject: [PATCH 26/67] some attribute name adjustment according to the spec --- schemas/Connection.json | 6 +- .../models/__pycache__/link.cpython-38.pyc | Bin 10327 -> 10389 bytes sdxdatamodel/models/link.py | 108 +++++++++++++----- sdxdatamodel/models/port.py | 25 ++++ sdxdatamodel/parsing/linkhandler.py | 10 +- sdxdatamodel/sdxdatamodel.egg-info/PKG-INFO | 10 ++ .../__pycache__/manager.cpython-38.pyc | Bin 5678 -> 5666 bytes sdxdatamodel/topologymanager/manager.py | 6 +- test/data/amlight.json | 20 ++-- test/data/amlight_origin.json | 8 +- test/data/link.json | 4 +- test/data/sax.json | 32 +++--- test/data/zaoxi.json | 16 +-- 13 files changed, 167 insertions(+), 78 deletions(-) diff --git a/schemas/Connection.json b/schemas/Connection.json index 3771b0e..2644d0c 100644 --- a/schemas/Connection.json +++ b/schemas/Connection.json @@ -107,7 +107,11 @@ }, "status": { "type": "string", - "enum": [] + "enum": [ + "up", + "down", + "error" + ] } }, "required": ["id", "name", "ingress_port", "egress_port"] diff --git a/sdxdatamodel/models/__pycache__/link.cpython-38.pyc b/sdxdatamodel/models/__pycache__/link.cpython-38.pyc index 72e2baafb79eed6e84438c04a656d4be153a468f..23e0f174b9c951fb2dfbc78fda5729de5bbd614f 100644 GIT binary patch delta 2177 zcmZ`(UuauZ7|%_U+%##@CTW_sX>XgPw`n^2Z)@vnyRGV+b7NYqLkQyKX5WFOOWSyJ zGbaj#txtjsJkI%NPIP;4Fl5?S!3T#Tj`^VIxZp!Ud{FQ~MiG3_?>k-Y>NN@EJKy*F z{m$>)bIN2OppbU zN!yhS85j^u3#c%-?#hQ)3o5dNuHhmrxZn`vge1(uNDbSS47=a~!L+hgWJc^thEW(3 zOdD%MX56l1n1J1aiLeMVd+bVvNthB$ltq!5hP7HL!-KF_P%#!mYR0Z)cnD?%)6UwF znX@Yy_Q8I^#917f1GNg~;bGio2Rs6g;+TMg@EDGra0nj9u?r5v6F7E5!n5eg9o?ul zElN_Df8*(m=bH=q3cOSV^Q09uSfvP6y~N$#v9PaV>SncKd1+*P+G!Z*hqJ!vE-$6>(w&Io$~R5qCPlFULMox*EBqk=W!fwBO~oyYIOXK$9EWt25}e?HY^MWl2?`9MuyZ@Q~cNDU3FrI zp=dUTAz=s5@TQ_PGNbl9!Z&-rS9k9)6HV(dBkWo6bb-DbN?#*GYIW_(ojyt3v%`>3 z=Lr*5M8m{Jdumq7Op;u(q%#hk<)16*%LUN)zsiktPMYMG(hIvtZAcWRw&!_@CR=t) zHTEKRjwdn;zG;$9^5x9HG+kiJhu^?z$+SGWX&RQKS>DrnsmiRTQ|x7@3$11HuQNAN zV%0h=V)@KftzsJI`P&1}XO7WY^;CXMGg#TsG|m(1+RQxZ+!!n*pm@U6v}@ZRJIlLj zo@9pOK`fu9u@^P%BL7Y`QsQ_P$l|{TMXD91X_t9n@OXSb*@tM8#X{9&+Ol3YRDNr4 zIM`29MIWd6hjQ$YxIp6I#bJr9ik&=3!e=NfQnf=5~Gq-<7pG6J{akP_@wfniHQ%!m_9YJZK9b(6O4&I8)AIW#B=UuCUG1goO{my z_rL$Sv%tN1?)!4=LM+xCps(NN-Z0+zGNY5QIU6$oo;;Rr=Ow zt%TqdbU|uesf2|J-O#fhcvY!HMC4Kv^tzJ@eUO%eDjbGsNjgO*kw@G~g&8;^ zNm3+<%sLYsg=6%dU2q&u(3*lr;W1jf;c<9^)*hIHleG3i@_eB97d{7wqg#@VrJZusH9oA(DKLV z5L5O!iHy97tX-qTDf}XOUF&3rUuuNF214v^Ay1tX$f`GxwVMH~r6%$9)(tJm7Qe(M z0w3&@;^WG!JSZFOuu-dt6)+hV8|t3}_jY5gYFge$X+%E8TW!0g0{ere8{{02_j-4+ z_N9E|pW8RJE_V2xkN-{Y;AY1!g(7uMAgkU$*6!8c_(9@BEyWhU#1;ZS;*HL|!YFl4 zBCFm+)_$!g{*-)E>t>5zVwAw|_*&ODg)!=!L{`0tto_A_Uu!*#{nGd*QnB3q0Iwxf zP5z;>;1p9Rsw0<*ZV_$oI2-hta<}kE6Bh~-)*gr9e&4)6DVOi zy_erlofF8aH;}a%8TfoZYH4=(1-22G!<2SEKS7-n$f`GxwKB#BcC~({eqrqdp4n>6 zDEUe1oG@0sVXO%{U25lac1FpS*sj>hRgce(99c4!=fOC76+ap*DkZ!(SRP}ysc<<^ z-`BWkiDl1LbBfp+p2?NN2iaP}t=y0@fOm4)&I}8($g79*YWR2VldMekc{4j~RrOWN z+`zj-&*o0^c0X4{*G*9~b^R?olV6*jWnYkrO#l`bEM4EO-)nZLYF!kj=g4+g*TrRB zM+_CrY=(EA;UYU(__*v^5xTyK?-nj3XIP0DmSrx^zi0`4*{GR=7#tpL?u-3m9ZVM{D-+SRW-XFFmpXQKLRP3NB%xP}I S)%iHow to Contribute @@ -52,6 +53,7 @@ Description: ## Table of Contents ## Topology description schemas There are defined in the *schema* subfolder. Some attributes of each objects are requied (Can be found in the API definition) while some are optional. Two attributes are worth of mentioning: (1) In the *service* object, there is a *vender* attribute for the domain to list device vendors that are NOT in its domain, (2) in the topology, link, node, and port objects, there is an *private* attibute for the domain to list attributes that need to kept private.:wq + ## Usage ## Unittest ``` python -m unittest -v test.test_topology_handler @@ -59,6 +61,14 @@ Description: ## Table of Contents ``` python -m unittest -v test.test_topology_validator ``` + ## Install + ``` + pip install -r requirements.txt + ``` + ``` + python setup.py install + ``` + ## Accompanying AW-SDX Projects diff --git a/sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc index dd0b69ab7f1d0c85174c42161fb37699f2679110..d43238dc01d4ec93919d745e834c891a0b5f2da1 100644 GIT binary patch delta 81 zcmZ3dvq*732tCyG?*+dtjEs8#mK`{B{%uMu=M6k;iF6dT{0EZ delta 79 zcmZ3avrdOMl$V!_0SI)1I};~u>g56kh-U diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index 24b33ce..69f7f32 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -184,9 +184,9 @@ def generate_graph(self): edge = G.edges[end_nodes[0].name,end_nodes[1].name] edge['id'] = link.id edge['latency'] = link.latency - edge['total_bandwidth'] = link.total_bandwidth - edge['available_bandwidth'] = link.available_bandwidth - edge['latency'] = link.latency + edge['bandwidth'] = link.bandwidth + edge['residual_bandwidth'] = link.residual_bandwidth + #edge['latency'] = link.latency edge['packet_loss'] = link.packet_loss edge['availability'] = link.availability diff --git a/test/data/amlight.json b/test/data/amlight.json index d4bada7..cb530b3 100644 --- a/test/data/amlight.json +++ b/test/data/amlight.json @@ -3,7 +3,7 @@ "links": [ { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:amlight:B1-B2", "latency": 146582.15146899645, "name": "amlight:B1-B2", @@ -17,11 +17,11 @@ } ], "short_name": "Miami-BocaRaton", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:amlight:A1-B1", "latency": 146582.15146899645, "name": "amlight:A1-B1", @@ -35,11 +35,11 @@ } ], "short_name": "redclara-miami", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:amlight:A1-B2", "latency": 146582.15146899645, "name": "amlight:A1-B2", @@ -53,11 +53,11 @@ } ], "short_name": "redclara-BocaRaton", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", "latency": 146582.15146899645, "name": "nni:Miami-Sanpaolo", @@ -72,11 +72,11 @@ } ], "short_name": "Miami-Sanpaolo", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", "latency": 146582.15146899645, "name": "nni:BocaRaton-Fortaleza", @@ -91,7 +91,7 @@ } ], "short_name": "BocaRaton-Fortaleza", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 } ], "name": "amLight", diff --git a/test/data/amlight_origin.json b/test/data/amlight_origin.json index 1ae4011..09ab0ed 100644 --- a/test/data/amlight_origin.json +++ b/test/data/amlight_origin.json @@ -3,7 +3,7 @@ "links": [ { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residula_bandwidth": 602746.015561422, "id": "l-1", "latency": 146582.15146899645, "name": "miami-Boca.amLight.sdx", @@ -33,11 +33,11 @@ } ], "short_name": "Miami-BocaRaton", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residula_bandwidth": 602746.015561422, "id": "l-2", "latency": 146582.15146899645, "name": "miami-Boca.amLight.sdx", @@ -67,7 +67,7 @@ } ], "short_name": "Miami-BocaRaton", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 } ], "name": "amLight", diff --git a/test/data/link.json b/test/data/link.json index 912531c..49761cd 100644 --- a/test/data/link.json +++ b/test/data/link.json @@ -1,6 +1,6 @@ { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "id", "latency": 146582.15146899645, "name": "miami-Boca.amLight.sdx", @@ -30,5 +30,5 @@ } ], "short_name": "Miami-BocaRaton", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 } \ No newline at end of file diff --git a/test/data/sax.json b/test/data/sax.json index 4a9cc73..520922b 100644 --- a/test/data/sax.json +++ b/test/data/sax.json @@ -3,7 +3,7 @@ "links": [ { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:sax:B1-B2", "latency": 146582.15146899645, "name": "sax:B1-B2", @@ -17,11 +17,11 @@ } ], "short_name": "SaoPaulo-Fortaleza", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:sax:Panama-Fortaleza", "latency": 146582.15146899645, "name": "sax:Panama-Fortaleza", @@ -35,11 +35,11 @@ } ], "short_name": "Panama-Fortaleza", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:sax:SanPaolo-Fortaleza", "latency": 146582.15146899645, "name": "nni:SanPaolo-Fortaleza", @@ -53,11 +53,11 @@ } ], "short_name": "BocaRaton-Fortaleza", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:sax:A1-B1", "latency": 146582.15146899645, "name": "sax:A1-B1", @@ -71,11 +71,11 @@ } ], "short_name": "redclara-SaoPaulo", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:sax:A1-B2", "latency": 146582.15146899645, "name": "sax:A1-B2", @@ -89,11 +89,11 @@ } ], "short_name": "redclara-Fortaleza", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", "latency": 146582.15146899645, "name": "nni:Miami-Sanpaolo", @@ -107,11 +107,11 @@ } ], "short_name": "Miami-Sanpaolo", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", "latency": 146582.15146899645, "name": "nni:BocaRaton-Fortaleza", @@ -126,11 +126,11 @@ } ], "short_name": "BocaRaton-Fortaleza", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", "latency": 146582.15146899645, "name": "nni:Fortaleza-Sangano", @@ -145,7 +145,7 @@ } ], "short_name": "Fortaleza-Sangano", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 } ], "name": "sax", diff --git a/test/data/zaoxi.json b/test/data/zaoxi.json index a2eb470..2e89c77 100644 --- a/test/data/zaoxi.json +++ b/test/data/zaoxi.json @@ -3,7 +3,7 @@ "links": [ { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", "latency": 146582.15146899645, "name": "zaoxi:B1-B2", @@ -17,11 +17,11 @@ } ], "short_name": "Sangano-Capetown", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", "latency": 146582.15146899645, "name": "zaoxi:A1-B1", @@ -35,11 +35,11 @@ } ], "short_name": "Karoo-Sangano", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", "latency": 146582.15146899645, "name": "zaoxi:A1-B2", @@ -53,11 +53,11 @@ } ], "short_name": "Karoo-Capetown", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 }, { "availability": 56.37376656633328, - "available_bandwidth": 602746.015561422, + "residual_bandwidth": 602746.015561422, "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", "latency": 146582.15146899645, "name": "nni:Fortaleza-Sangano", @@ -72,7 +72,7 @@ } ], "short_name": "Fortaleza-Sangano", - "total_bandwidth": 80083.7389632821 + "bandwidth": 80083.7389632821 } ], "name": "zaoxi", From 86eccf33a7f923434aeed4813f380846b9f8602b Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Tue, 15 Mar 2022 12:38:26 -0400 Subject: [PATCH 27/67] requirement --- requirements.txt | 1 + .../models/__pycache__/link.cpython-38.pyc | Bin 10389 -> 11332 bytes .../models/__pycache__/port.cpython-38.pyc | Bin 6673 -> 7243 bytes sdxdatamodel/models/link.py | 2 ++ .../__pycache__/linkhandler.cpython-38.pyc | Bin 1977 -> 1970 bytes 5 files changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 2fd0d25..55aa08c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ grenml +networkx diff --git a/sdxdatamodel/models/__pycache__/link.cpython-38.pyc b/sdxdatamodel/models/__pycache__/link.cpython-38.pyc index 23e0f174b9c951fb2dfbc78fda5729de5bbd614f..bf1ac97eb8857ae16b92162c0fc5b6cb2f4f1eb9 100644 GIT binary patch delta 1808 zcmZ`(eQZ-z6z}c&KKk0%cJ1if+uHSG(9ytP!+k`EVN-O#KG-reWHG6Q`;ZB4%H$8_u5~_k3WeL0{b&=oUC#m}YCCruc3#&7}b}=_v!YU}) z?X^c(1tK@|2y~FS-U=|!+niUyBoRBtBt#x23-kt)5lKuD=nPX3$xIdKBc>u!m{*_+ z%!_D+$}|DLVH!do^9l4b^C9vx6ow*xWqw3DT$ERkAp9YxqgrlzeuVBHriER_NY1v# zhv`Iq540(3LUL}rkS|z8Mfizvcv9(h4({QDfh)>Bc_W{Y8}e!tMpYFi)bG^@Motqu z4b5gm;;q^Nd%d@H9?rV$G&);ya!r1(hBGN zN5~REQ!x1wQ2nwjYAL*y%!4OfFlIEt3xO9&R4`QxzBAh#%#aM%k@eUO=9zRuWcMW#EQppznf6 zFdn{yq;WSImpU2f86Mfa3TDDB+Vo)A4g)^-g@aD_*Cw}?Dy@cq8HHyf3Q5DU%9#6G zai-Ag&euvqf}52cva`&>Tp|Q_%}QWlGsb6gtxSR;BrK5M3%7^fbc8q+YPg)b63p+p zcPmP|99kA~k%Zoe#TVP6iSF}HUN3e3hb4z$UzI8OFmxLZMK6}7CTv=&$|`&$`l#5d$;LFloLFYpP92$1KKcu3*+*8zU=uHgwQw1izkmEx*)SWM>%{ zjcD@)Nt|DR+5pyQC-zvROgTI+v_j|uTX+}2kn zUUS4-Fdtf*u$wtyvhjc9K!?9hkCllD3NgIuHX#OvYEQwRaTA*29%4Z(_M5(@j2_~2 zksB3r*%=4;C1FaH$e9GA?rZ(NGIphyFV9UwFmaV6@dz<;^kg7O^p-MSSn$FaIgVA0 z+0ChSgtWq;bc&pTchgy1f0gd{wIadBv%BmzXsoZ5?#JaU^w#&w?L62DC+nBuy|_@{ zBys8~xLbb~x86z^-lAL>#W%~IgG);vCw)BTE2=qs-Nx)(?mYA^J79G3BK#l96Lfe- zA(uOiPZL~QmLi{ll-W-PU@B8XF2Spr4Bn-=%qp@TZe-HS_&ZXR$E`xavTgog+5F`Z zuZW%ED=!*2E;UANafUt~+rrHtH(Oz(Aw@ohV+|Wf16*zx@cP|E^LmKphrb)Dp?!HM TxQ!Eg&=lRaWj_FqEnoi+`1s^g delta 1556 zcmZuxZD?C%6waIE-rSqyrfKt?rOQXVWJ$WtZmEp<(W%wV*2!ioj-g!A-oQd@*4|#a zp)X9f;tz#w=CF$X7;~NCV4JJ`A&B?^>JPs_WFiq9PUw${_(wz-sOP*{V_OW|bMEt; zC+B_M_q@rcht3`JJ*jDK8-AXz&6iht7JUnheGc!kQ8u*XvZomfLf(<>HTD<}%-Ify zjR%c~Fh`6*<6+EE;}K&o=9sb1cocKoh>Y0s!%K^fKE@jGi*tY-hA!#FZW&iFT@(}w zDuzazb<`?P>SN`aaja@sN8l4_A6o&pJPc(yaAJ%*xJ|cnr@- zf}7(KA}5yxs&g5U#9abi;4VZmR|LAo6+|ws3iJV25h>g)&==f|NaY@Ze&8NNZtnF8 zxXir>J@AiQWEvD*nNGjH!FD64muJh>$@17_1;$-F;4RnCw*T3$8K6xETG}-m*OXa# z5UVyjyrM+mP30#gi0Cpi(=hD|z*&!zm0>~sOCvgOH`AC*xmBBhkKOOF5KM=o5*_k! zIOn;ogzjiz(wAT-QPK}9-qS2BWHg2|Ft6Q^MK6VVNkqN|i-A6P-}f?H?C4~dmd4w% zEKA$TJIx#>GlHQZm#3yGHA4(C?`Zl_;T?ZWN@4jLeC_{DBjKu7RrIH^e?&&K`Mniu zCS@I|r0_K1?_eB~YSyv(t|ybAhuh&~FqWWG`wE{KI|iHbT=21KZKAh11NgS1q|J}4 zZ`BjsFreM3(KYLA_ilz4LVm3WyREtl2zRv7_q6-K`IfuZwCbTKSya~%O`nBS2EX|; zKC2$1!edLSlh44#md{q99x7y7;J^FOG~6oC(oNTq=CCrtE-m`27}j#=CDiu|9Y^QipInCV7$Yz@g}mO_^87hzz0ZGnNqdLqnW`Qv7C9qzMs` z1<~vG&6ilG5c-kphsER{N*}(_iNe)Xf(;1S?GykcV^TYohTuy2J8j1u5hk-;G^N_8 zP~XU0XGzht137(Rp^Ird*XTQho@!OpN0x4O-@{l5Hf7U{!+qJjvxFu1W=kCAvO9|e zOh%y}L!LF9Rjcx>*+q;9-SoB#!zs9xJs{srumrnu-7F2yN>!!ttTRTs0rOvDj3yqOV=;cksQyb*{kq+{#fDw zDb|~4$huyu==yosR>-nVFk0wk_rs||CP%-!hCEZTtctne@6$|CBf7I@2HZ+IY>Ex< yC*pAmPf!>Et(az4V0&?pZGjJq2{>EqhpWYQST6e8o+8OGhKAj&nD>IzGw?5uy_LlP diff --git a/sdxdatamodel/models/__pycache__/port.cpython-38.pyc b/sdxdatamodel/models/__pycache__/port.cpython-38.pyc index bd1f889e6a6abc5b2355f7a701ec3e54bab225c0..8fbaf42246906dede4257c0f3ab847abd5a6ed2c 100644 GIT binary patch delta 1749 zcmZuxO>7%Q6y8~{ch|fAjqTXZ&&H-PjY|?}lG28j&>NSCAPNXsh^>=1Dyix^jJNzm zLP3xN2LwVRkU$6yAfy~B73IKzBjSLNKtf2g7OJY=;DiK1Jr&*?$A&EHm1o|2-?!hq zH}huSUHa}+`l(dP(eQU;^;TWlP;Y$t*4S^Rfbn5J>9$v|uBz?w1851zgi1LGsR zX+RuIu)u}{IN(B3MdPplX~^udrpYbHLT*=kgEeh#UrfNr(6b;91%)`=L8M6D4iX)F zX%tEda=D98dFWYCfiZ<7c@mNFp=ZGaOe!SBQ;2v&&w?qaDkRO*h}4Fj1^2)ag=7>m z4Qm6}f;!x*pe)bgOGgJD%s^vTYv$k>+=ufB%)%Vbd65?H)Yi}Cia-fBO(gJp-7g2 z!>U|0@6?lMHS|7-qEXDJ<#UM>@?-0DFGY+}1TBuS4Y^|fb*xM%@dl~{g;gomM%Ayq zG@((7CIZ{?5$F4Q1+9S?MZ;ng+m(sTaZJ&bm))1V46&j_Ed<_?vE)IT#2Y9R6jr9# z2demIQtx|NVnnIh2z)M+>Ff0|v<89{4GU828m9WCmm@SvjBd?WpWe){{!S8aph!?y zkz(Iais{!LNsfjw)1#L z1oQn?^Uc%g7tbA9;C!+s&u0ynKU974-$iMy34MD`Ud@)|tz@NN#W)HMs-R`URZ#2~ zwb2jfKJ!M15w$ne9F*G%QH5KlaP{C)o5PQCW|@9OCrsGnZQJb!e@a zOuk*1F^DL=!kGN2usBPO!0(nOy#D*fX29Da2~k5V#pGgf&730cq_h;m*uSl z%NH>2Bn;*__PA}}W2xdBrZ|gAVhM+yF1V0;Z N>2?~9&1~J){{e~OO@#me delta 1442 zcmZvbO=uHA6vub+vAfOYt4-3hN!>QKu^+b87PVGH^y0xw#gEG>ENS0JTdl3z)%I4Y zMG#bw2P)!C5sDy+2NBVW7eNp~5Dg+86g=py7QH!d(u8)|ESZ`2|Nr;L+ufPByKn4> zACJW%iunCJc%}5c#paD|SiSUSU($#7~+?0VvX&KxQR<>4Y z&;-rWiSnp$aJhVtB#$&?SRI4;Fp;bC@9v4nqtPJ@`7RBcGpDkO$OctQPJq)>OD5+c zBHlLU%s8d|pqenQVPABuRmxS#WO>P?-IA>?#%`JkO88M}Bj3ke@lVBOv8n<|^#VzI zBwKwI&LJc&n@LLfLFP2c From 0ad45fe1a3ca668d5d8eac6a3b13d3301c6079eb Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Wed, 11 May 2022 10:33:09 -0400 Subject: [PATCH 28/67] added model_version to topology description --- schemas/Topology.json | 11 ++++++++--- .../__pycache__/manager.cpython-38.pyc | Bin 5666 -> 5659 bytes test/data/amlight.json | 9 +++++---- test/data/sax.json | 9 +++++---- test/data/zaoxi.json | 9 +++++---- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/schemas/Topology.json b/schemas/Topology.json index b79aa71..bf4603c 100644 --- a/schemas/Topology.json +++ b/schemas/Topology.json @@ -4,7 +4,7 @@ "type": "object", "properties": { "id": { - "description": "This is supposed to be an unique identifier, starting with urn:ogf:networking:global", + "description": "This is supposed to be an unique identifier: urn:sdx:topology:", "type": "string" }, "name": { @@ -18,7 +18,12 @@ "version": { "description": "This is supposed to be in ISO format", "type": "number", - "minimum": 0, + "minimum": 0 + }, + "model_version": { + "description": "This is supposed to be 1.0.0", + "type": "string", + "minimum": 0 }, "domain_service": { "$ref": "Service" @@ -40,5 +45,5 @@ ] } }, - "required": ["id", "name", "version","time_stamp","nodes", "links"] + "required": ["id", "name", "version","model_version","time_stamp","nodes", "links"] } \ No newline at end of file diff --git a/sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc index d43238dc01d4ec93919d745e834c891a0b5f2da1..edb18729e8a5a128493468488b3ed4813c855c62 100644 GIT binary patch delta 48 zcmZ3aGh2r@l$V!_0SF@f9wfGI^!XETqG7z+Sj CwGTc3 delta 55 zcmbQOvq*z6F768n45ugA7 diff --git a/test/data/amlight.json b/test/data/amlight.json index cb530b3..a6ae13d 100644 --- a/test/data/amlight.json +++ b/test/data/amlight.json @@ -1,5 +1,9 @@ { - "id": "urn:ogf:network:sdx:topology:amlight", + "id": "urn:ogf:network:sdx:topology:amlight.net", + "name": "AmLight-OXP", + "model_version":"1.0.0", + "time_stamp": "2000-01-23T04:56:07+00:00", + "version": 1, "links": [ { "availability": 56.37376656633328, @@ -94,7 +98,6 @@ "bandwidth": 80083.7389632821 } ], - "name": "amLight", "nodes": [ { "id": "urn:ogf:network:sdx:node:amlight:B1", @@ -221,8 +224,6 @@ "short_name": "A1" } ], - "time_stamp": "2000-01-23T04:56:07+00:00", - "version": 1, "domain_service": { "owner":"FIU" } diff --git a/test/data/sax.json b/test/data/sax.json index 520922b..613683c 100644 --- a/test/data/sax.json +++ b/test/data/sax.json @@ -1,5 +1,9 @@ { - "id": "urn:ogf:network:sdx:topology:sax", + "id": "urn:ogf:network:sdx:topology:sax.net", + "name": "SAX-OXP", + "time_stamp": "2000-01-23T04:56:07+00:00", + "version": 1, + "model_version":"1.0.0", "links": [ { "availability": 56.37376656633328, @@ -148,7 +152,6 @@ "bandwidth": 80083.7389632821 } ], - "name": "sax", "nodes": [ { "id": "urn:ogf:network:sdx:node:sax:B1", @@ -343,8 +346,6 @@ "short_name": "A1" } ], - "time_stamp": "2000-01-23T04:56:07+00:00", - "version": 1, "domain_service": { "owner":"RNP" } diff --git a/test/data/zaoxi.json b/test/data/zaoxi.json index 2e89c77..07c1741 100644 --- a/test/data/zaoxi.json +++ b/test/data/zaoxi.json @@ -1,5 +1,9 @@ { - "id": "urn:ogf:network:sdx:topology:zaoxi", + "id": "urn:ogf:network:sdx:topology:zaoxi.net", + "name": "ZAOXI-OXP", + "time_stamp": "2000-01-23T04:56:07+00:00", + "version": 1, + "model_version":"1.0.0", "links": [ { "availability": 56.37376656633328, @@ -75,7 +79,6 @@ "bandwidth": 80083.7389632821 } ], - "name": "zaoxi", "nodes": [ { "id": "urn:ogf:network:sdx:node:zaoxi:B1", @@ -202,8 +205,6 @@ "short_name": "A1" } ], - "time_stamp": "2000-01-23T04:56:07+00:00", - "version": 1, "domain_service": { "owner":"FIU" } From d69619362ae15d3d19397d040a7c444ba161ddcf Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Wed, 11 May 2022 11:01:12 -0400 Subject: [PATCH 29/67] id changes unittest --- test/data/amlight.json | 54 +++++++++++++++++----------------- test/data/amlight.png | Bin 0 -> 42051 bytes test/data/sax.json | 4 +-- test/test_topology_graph.py | 2 +- test/test_topology_manager.py | 2 +- 5 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 test/data/amlight.png diff --git a/test/data/amlight.json b/test/data/amlight.json index a6ae13d..666ee1e 100644 --- a/test/data/amlight.json +++ b/test/data/amlight.json @@ -14,10 +14,10 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:B1:2" + "id": "urn:sdx:port:amlight.net:B1:2" }, { - "id": "urn:ogf:network:sdx:port:amlight:B2:2" + "id": "urn:sdx:port:amlight.net:B2:2" } ], "short_name": "Miami-BocaRaton", @@ -32,10 +32,10 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:A1:1" + "id": "urn:sdx:port:amlight.net:A1:1" }, { - "id": "urn:ogf:network:sdx:port:amlight:B1:3" + "id": "urn:sdx:port:amlight.net:B1:3" } ], "short_name": "redclara-miami", @@ -50,10 +50,10 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:A1:2" + "id": "urn:sdx:port:amlight.net:A1:2" }, { - "id": "urn:ogf:network:sdx:port:amlight:B2:3" + "id": "urn:sdx:port:amlight.net:B2:3" } ], "short_name": "redclara-BocaRaton", @@ -69,7 +69,7 @@ "nni": "True", "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:B1:1" + "id": "urn:sdx:port:amlight.net:B1:1" }, { "id": "urn:ogf:network:sdx:port:sax:B1:1" @@ -88,7 +88,7 @@ "nni": "True", "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:B2:1" + "id": "urn:sdx:port:amlight.net:B2:1" }, { "id": "urn:ogf:network:sdx:port:sax:B2:1" @@ -100,7 +100,7 @@ ], "nodes": [ { - "id": "urn:ogf:network:sdx:node:amlight:B1", + "id": "urn:sdx:node:amlight.net:B1", "location": { "address": "Miami", "latitude": 25.75633040531146, @@ -109,9 +109,9 @@ "name": "amlight:Novi01", "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:B1:1", + "id": "urn:sdx:port:amlight:B1:1", "name": "Novi01:1", - "node": "urn:ogf:network:sdx:node:amlight:B1", + "node": "urn:sdx:node:amlight.net:B1", "short_name": "B1:1", "label_range": [ "100-200", @@ -120,9 +120,9 @@ "status": "up" }, { - "id": "urn:ogf:network:sdx:port:amlight:B1:2", + "id": "urn:sdx:port:amlight.net:B1:2", "name": "Novi01:2", - "node": "urn:ogf:network:sdx:node:amlight:B1", + "node": "urn:sdx:node:amlight.net:B1", "short_name": "B1:2", "label_range": [ "100-200", @@ -131,9 +131,9 @@ "status": "up" }, { - "id": "urn:ogf:network:sdx:port:amlight:B1:3", + "id": "urn:sdx:port:amlight.net:B1:3", "name": "Novi01:3", - "node": "urn:ogf:network:sdx:node:amlight:B1", + "node": "urn:sdx:node:amlight.net:B1", "short_name": "B1:3", "label_range": [ "100-200", @@ -145,7 +145,7 @@ "short_name": "B1" }, { - "id": "urn:ogf:network:sdx:node:amlight:B2", + "id": "urn:sdx:node:amlight.net:B2", "location": { "address": "BocaRaton", "latitude": 26.381437356374075, @@ -154,35 +154,35 @@ "name": "amlight:Novi02", "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:B2:1", + "id": "urn:sdx:port:amlight.net:B2:1", "label_range": [ "100-200", "1000" ], "name": "Novi02:1", - "node": "urn:ogf:network:sdx:node:amlight:B2", + "node": "urn:sdx:node:amlight.net:B2", "short_name": "B2:1", "status": "up" }, { - "id": "urn:ogf:network:sdx:port:amlight:B2:2", + "id": "urn:sdx:port:amlight.net:B2:2", "label_range": [ "100-200", "10001" ], "name": "Novi02:2", - "node": "urn:ogf:network:sdx:node:amlight:B2", + "node": "urn:sdx:node:amlight.net:B2", "short_name": "B2:2", "status": "up" }, { - "id": "urn:ogf:network:sdx:port:amlight:B2:3", + "id": "urn:sdx:port:amlight.net:B2:3", "label_range": [ "100-200", "10001" ], "name": "Novi02:3", - "node": "urn:ogf:network:sdx:node:amlight:B2", + "node": "urn:sdx:node:amlight.net:B2", "short_name": "B2:3", "status": "up" } @@ -190,7 +190,7 @@ "short_name": "B2" }, { - "id": "urn:ogf:network:sdx:node:amlight:A1", + "id": "urn:sdx:node:amlight.net:A1", "location": { "address": "redclara", "latitude": 30.34943181039702, @@ -199,24 +199,24 @@ "name": "amlight:Novi100", "ports": [ { - "id": "urn:ogf:network:sdx:port:amlight:A1:1", + "id": "urn:sdx:port:amlight.net:A1:1", "label_range": [ "100-200", "1000" ], "name": "Novi100:1", - "node": "urn:ogf:network:sdx:node:amlight:A1", + "node": "urn:sdx:node:amlight.net:A1", "short_name": "A1:1", "status": "up" }, { - "id": "urn:ogf:network:sdx:port:amlight:A1:2", + "id": "urn:sdx:port:amlight.net:A1:2", "label_range": [ "100-200", "1000" ], "name": "Novi100:2", - "node": "urn:ogf:network:sdx:node:amlight:A1", + "node": "urn:sdx:node:amlight.net:A1", "short_name": "A1:2", "status": "up" } diff --git a/test/data/amlight.png b/test/data/amlight.png new file mode 100644 index 0000000000000000000000000000000000000000..2ba60a745900f55cf54833a5bf79c936f7cdd5ab GIT binary patch literal 42051 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 Date: Thu, 12 May 2022 10:42:13 -0400 Subject: [PATCH 30/67] make domain_service optional #18 --- .../topologyhandler.cpython-38.pyc | Bin 1861 -> 1887 bytes sdxdatamodel/parsing/topologyhandler.py | 4 +++- .../topologyvalidator.cpython-38.pyc | Bin 9673 -> 9684 bytes sdxdatamodel/validation/topologyvalidator.py | 3 ++- test/data/amlight.png | Bin 42051 -> 40992 bytes test/data/zaoxi.json | 5 +---- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdxdatamodel/parsing/__pycache__/topologyhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/topologyhandler.cpython-38.pyc index f337b9a0597faa75490a7fa8674b7b6a4cb5566a..16f7382a7c8367b6bfb3aac2701750f34b90cd5b 100644 GIT binary patch delta 201 zcmX@gcb|_pl$V!_0SG2Z*Cu6cNf}w<^hN*_RhNXtJhOLIZh9iZum%WA|p0$P{i*@oGmRxqWTP)eBmBo{}So8R0 mKyG7XV&niq7Dk@Q&8*#wI+OX?iWt==H?i3>3Qa!5)(ijz*ewMB delta 175 zcmcc5ca)Del$V!_0SGjMIuoTg^8RENn*!uj+0-zmFiJAiG688OAk92khebb-A)BkH zgpr|^laZlHs)VtIGm9yOLy`eTH#0RchBKHlqySaaaYisiGUPEuFqAOYFx4>Eu+*^D zu+^~FaHMedvez)gv(zwTu}ogWlFP_CS%Ec=TLRH!GXU@RCJz7r diff --git a/sdxdatamodel/parsing/topologyhandler.py b/sdxdatamodel/parsing/topologyhandler.py index a4fc138..86cda9e 100644 --- a/sdxdatamodel/parsing/topologyhandler.py +++ b/sdxdatamodel/parsing/topologyhandler.py @@ -27,7 +27,9 @@ def import_topology_data(self, data): try: id = data['id'] name=data['name'] - service=data['domain_service'] + service = None + if 'domain_service' in data.keys(): + service=data['domain_service'] version=data['version'] time_stamp=data['time_stamp'] nodes=data['nodes'] diff --git a/sdxdatamodel/validation/__pycache__/topologyvalidator.cpython-38.pyc b/sdxdatamodel/validation/__pycache__/topologyvalidator.cpython-38.pyc index c291adb533a4ce8f8d6be17f05fe8176fa39c58f..bcba22f01aea520e1037782a0a09e7243e0b8204 100644 GIT binary patch delta 175 zcmX@tE7f0 zm_d^{QJ!gXAh+!1)9m_8jDC}Uab6OT02#o;!pO(O#VEwY!MOPrS1}W#&t^Z~OlHQI z%{v4VnHcLgO9`hlGfvptBqq(mxPJ3Ish#YMQIjK-^nqlDQViq8%}lANr-wUti{q@Ra{k&lUsQHW{tQ?6nrM(@p$yqV05(VGto zBr-A9ZB`XdXJ+i*+$$!{!nkhpN~xXfjJGByE9nEt$x1Pd7dC%TDrXdx77*Z577!GW V=TIu*0UGM3DOTjXxke>|5dfc>ECB!j diff --git a/sdxdatamodel/validation/topologyvalidator.py b/sdxdatamodel/validation/topologyvalidator.py index c5e91f7..6724e1d 100644 --- a/sdxdatamodel/validation/topologyvalidator.py +++ b/sdxdatamodel/validation/topologyvalidator.py @@ -64,7 +64,8 @@ def _validate_topology(self, topology: Topology): errors.append('Global Institution must be in Topology {}'.format(topology.id)) service = topology.get_domain_service() - errors += self._validate_service(service, topology) + if service is not None: + errors += self._validate_service(service, topology) for node in topology.get_nodes(): errors += self._validate_node(node, topology) for link in topology.get_links(): diff --git a/test/data/amlight.png b/test/data/amlight.png index 2ba60a745900f55cf54833a5bf79c936f7cdd5ab..d1280cc2ef98bcdc10416fe4f8e1d6490659bec7 100644 GIT binary patch literal 40992 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 Date: Thu, 12 May 2022 14:20:21 -0400 Subject: [PATCH 31/67] adding more properties to the test data models --- test/data/amlight.json | 100 ++++++++++++++++++++++++--- test/data/amlight.png | Bin 40992 -> 42956 bytes test/data/sax.json | 150 ++++++++++++++++++++++++++++++++++++----- test/data/zaoxi.json | 75 ++++++++++++++++++--- 4 files changed, 292 insertions(+), 33 deletions(-) diff --git a/test/data/amlight.json b/test/data/amlight.json index 666ee1e..5ebebfc 100644 --- a/test/data/amlight.json +++ b/test/data/amlight.json @@ -14,10 +14,26 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:sdx:port:amlight.net:B1:2" + "id": "urn:sdx:port:amlight.net:B1:2", + "name": "Novi01:2", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" }, { - "id": "urn:sdx:port:amlight.net:B2:2" + "id": "urn:sdx:port:amlight.net:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:2", + "status": "up" } ], "short_name": "Miami-BocaRaton", @@ -32,10 +48,26 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:sdx:port:amlight.net:A1:1" + "id": "urn:sdx:port:amlight.net:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:sdx:node:amlight.net:A1", + "short_name": "A1:1", + "status": "up" }, { - "id": "urn:sdx:port:amlight.net:B1:3" + "id": "urn:sdx:port:amlight.net:B1:3", + "name": "Novi01:3", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" } ], "short_name": "redclara-miami", @@ -50,10 +82,26 @@ "packet_loss": 59.621339166831824, "ports": [ { - "id": "urn:sdx:port:amlight.net:A1:2" + "id": "urn:sdx:port:amlight.net:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:sdx:node:amlight.net:A1", + "short_name": "A1:2", + "status": "up" }, { - "id": "urn:sdx:port:amlight.net:B2:3" + "id": "urn:sdx:port:amlight.net:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:3", + "status": "up" } ], "short_name": "redclara-BocaRaton", @@ -69,10 +117,26 @@ "nni": "True", "ports": [ { - "id": "urn:sdx:port:amlight.net:B1:1" + "id": "urn:sdx:port:amlight:B1:1", + "name": "Novi01:1", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" }, { - "id": "urn:ogf:network:sdx:port:sax:B1:1" + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" } ], "short_name": "Miami-Sanpaolo", @@ -88,10 +152,26 @@ "nni": "True", "ports": [ { - "id": "urn:sdx:port:amlight.net:B2:1" + "id": "urn:sdx:port:amlight.net:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:1", + "status": "up" }, { - "id": "urn:ogf:network:sdx:port:sax:B2:1" + "id": "urn:ogf:network:sdx:port:sax:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:1", + "status": "up" } ], "short_name": "BocaRaton-Fortaleza", diff --git a/test/data/amlight.png b/test/data/amlight.png index d1280cc2ef98bcdc10416fe4f8e1d6490659bec7..d9370e392190cd6f65f7909d54390b4d346ab522 100644 GIT binary patch literal 42956 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 Date: Thu, 12 May 2022 14:26:38 -0400 Subject: [PATCH 32/67] #20 --- test/data/sax.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/data/sax.json b/test/data/sax.json index 02dd47c..1f78753 100644 --- a/test/data/sax.json +++ b/test/data/sax.json @@ -195,7 +195,15 @@ "status": "up" }, { - "id": "urn:ogf:network:sdx:port:sax:B1:1" + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" } ], "short_name": "Miami-Sanpaolo", From 5c5c8d05c60ecdf44a50bea0cdc6c62039175320 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sun, 29 May 2022 15:38:29 -0400 Subject: [PATCH 33/67] packaging, #12 --- .../__pycache__/__init__.cpython-38.pyc | Bin 557 -> 557 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 564 -> 564 bytes .../__pycache__/connection.cpython-38.pyc | Bin 8062 -> 8062 bytes .../models/__pycache__/link.cpython-38.pyc | Bin 11332 -> 11332 bytes .../link_measurement_period.cpython-38.pyc | Bin 3967 -> 3980 bytes .../__pycache__/location.cpython-38.pyc | Bin 4335 -> 4335 bytes .../models/__pycache__/node.cpython-38.pyc | Bin 6940 -> 6940 bytes .../models/__pycache__/port.cpython-38.pyc | Bin 7243 -> 7243 bytes .../models/__pycache__/service.cpython-38.pyc | Bin 6341 -> 6341 bytes .../__pycache__/topology.cpython-38.pyc | Bin 9921 -> 9921 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 565 -> 565 bytes .../__pycache__/exceptions.cpython-38.pyc | Bin 1034 -> 1034 bytes .../__pycache__/linkhandler.cpython-38.pyc | Bin 1970 -> 1970 bytes .../locationhandler.cpython-38.pyc | Bin 1593 -> 1593 bytes .../__pycache__/nodehandler.cpython-38.pyc | Bin 1586 -> 1586 bytes .../__pycache__/porthandler.cpython-38.pyc | Bin 1618 -> 1618 bytes .../__pycache__/servicehandler.cpython-38.pyc | Bin 1751 -> 1751 bytes .../topologyhandler.cpython-38.pyc | Bin 1887 -> 1887 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 573 -> 573 bytes .../grenmlconverter.cpython-38.pyc | Bin 2417 -> 2417 bytes .../__pycache__/manager.cpython-38.pyc | Bin 5659 -> 5659 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 568 -> 568 bytes .../topologyvalidator.cpython-38.pyc | Bin 9684 -> 9684 bytes setup.cfg | 14 +++++++++++--- test/data/amlight.png | Bin 42956 -> 0 bytes {test => tests}/__init__.py | 0 tests/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 500 bytes .../test_topology_handler.cpython-38.pyc | Bin 0 -> 1860 bytes .../test_topology_manager.cpython-38.pyc | Bin 0 -> 2785 bytes {test => tests}/data/amlight.json | 0 tests/data/amlight.png | Bin 0 -> 43502 bytes {test => tests}/data/amlight_origin.json | 0 {test => tests}/data/ampath-sax-zaoxi.pdf | Bin {test => tests}/data/ampath.json | 0 {test => tests}/data/link.json | 0 {test => tests}/data/location.json | 0 {test => tests}/data/node.json | 0 {test => tests}/data/p2p.json | 0 {test => tests}/data/port.json | 0 {test => tests}/data/sax.json | 0 {test => tests}/data/service.json | 0 {test => tests}/data/zaoxi.json | 0 {test => tests}/test_connection_handler.py | 2 +- {test => tests}/test_connection_validator.py | 2 +- {test => tests}/test_link_handler.py | 2 +- {test => tests}/test_location_handler.py | 2 +- {test => tests}/test_node_handler.py | 2 +- {test => tests}/test_port_handler.py | 2 +- {test => tests}/test_service_handler.py | 2 +- {test => tests}/test_topology_graph.py | 6 +++--- .../test_topology_grenmlconverter.py | 2 +- {test => tests}/test_topology_handler.py | 2 +- {test => tests}/test_topology_manager.py | 10 ++++++---- {test => tests}/test_topology_validator.py | 8 ++++---- 54 files changed, 33 insertions(+), 23 deletions(-) delete mode 100644 test/data/amlight.png rename {test => tests}/__init__.py (100%) create mode 100644 tests/__pycache__/__init__.cpython-38.pyc create mode 100644 tests/__pycache__/test_topology_handler.cpython-38.pyc create mode 100644 tests/__pycache__/test_topology_manager.cpython-38.pyc rename {test => tests}/data/amlight.json (100%) create mode 100644 tests/data/amlight.png rename {test => tests}/data/amlight_origin.json (100%) rename {test => tests}/data/ampath-sax-zaoxi.pdf (100%) rename {test => tests}/data/ampath.json (100%) rename {test => tests}/data/link.json (100%) rename {test => tests}/data/location.json (100%) rename {test => tests}/data/node.json (100%) rename {test => tests}/data/p2p.json (100%) rename {test => tests}/data/port.json (100%) rename {test => tests}/data/sax.json (100%) rename {test => tests}/data/service.json (100%) rename {test => tests}/data/zaoxi.json (100%) rename {test => tests}/test_connection_handler.py (94%) rename {test => tests}/test_connection_validator.py (95%) rename {test => tests}/test_link_handler.py (94%) rename {test => tests}/test_location_handler.py (94%) rename {test => tests}/test_node_handler.py (94%) rename {test => tests}/test_port_handler.py (94%) rename {test => tests}/test_service_handler.py (94%) rename {test => tests}/test_topology_graph.py (90%) rename {test => tests}/test_topology_grenmlconverter.py (95%) rename {test => tests}/test_topology_handler.py (96%) rename {test => tests}/test_topology_manager.py (91%) rename {test => tests}/test_topology_validator.py (83%) diff --git a/sdxdatamodel/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/__pycache__/__init__.cpython-38.pyc index 8c4eefc5cc59fec9a1d492f49e2db112a596df84..f4bb233fcb010f1375d1ddaff3bd61e1ad7bdd75 100644 GIT binary patch delta 50 zcmZ3>vX+G>l$V!_0SI_cPEOj$vxreBiYq=ot+b@HC^bI5hzTeYHTeLe3}e*fyNm$< DO1cgD delta 50 zcmZ3>vX+G>l$V!_0SJnMIukeYEMgS8#T6f)R$5Y8lo}sj!~~SNHTeLe4CAfIcNqf! DT^A2s diff --git a/sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc index 30cf8be92f3f5e9a7ebc39483a4e59032d0ce231..8d865d5f4bcaf2d651ebd150b0e412e1155af07f 100644 GIT binary patch delta 27 hcmdnOvW0~wl$V!_0SI_cPEOj$vx|{2YVvW$TmV|K2V4LE delta 27 hcmdnOvW0~wl$V!_0SMxKI}|$iRHTgJWE&y8J2d4l4 diff --git a/sdxdatamodel/models/__pycache__/connection.cpython-38.pyc b/sdxdatamodel/models/__pycache__/connection.cpython-38.pyc index d89cb9d40e3b2877effefb1ad6f79510c76b05a5..c45544f5c0fd465c46677fcf573a41a681484857 100644 GIT binary patch delta 21 bcmexo_s@&n%7<+Q3em(g-iaS65lu`}U=uLGHshM;#SXzACWq37GW1YTdO)LKumggdK3#{X5o!5!BR34pV8_^O7`T z#Ej&GMe%Jkp@TS0An3afdHWT7Dl=+au-~GtjpQ&t1dt&EtY_V4gIi%51+bSP#5`J0S;Q zKL>xZk>-Nc;0}};Z6Py&8y+ zqS^`hOi(`PZ{Hy8RwzFR;c@OLTnK0S*hNBRjpsMvp>KkNP1g|o2tOhnR^WPA?f4ZP z)X~@@HoQ{w71`1M=>*bg1q;Bk^g$ZZ!B{gezyk2FYnMn?kYHu0Ti~L@Itq3ZqluLo-rM5M~v5PM?FZWAHhavyb91 z1EcW>*9{lrlHah83@pT-u!RpiEjZUR6FP0muRshQ{_6v2Bg}r z$cmzo9omGlrp8)F7i`#4N!G6^I~-Q^X%ywqjHB6KlZ~Pv_3fn2V`NM-Doaxun;nvD??!^MMKJQ(rd&ev2n%We;oZ)g7V`)0PYvzgt@ zddCI9Q>Fi&j(y7)y&pMd0={#vIEU_nJ?Bx_=?a#9I#{;iNe29Sh}5D9=w_vgbd>sB z8KxzOPYj7*q~pJd;>iI^?!OWWl5AOk{KKks&ze|=k+?;ehkMn3;t7nPrb6N^rA6;+ zF@zB-k`oq%_pwd`#AyLR-vcNHQ}E1pTdcQ(xk|a3E@lK9N^*pjhgyx#D~fz&nz%4< zq}(;0HBKoI)!e%TTmIct61D6Bq|zhinKRJ00WUpi_~G?HI500ZpvtOWH>?NVNj+sa z{eRpRKsT%gx5Y*~hzB0l{gQeO5S$xxRURmYUWiS0kZQP9{|4@coMBoYy0-F;ycR4koNL+=n=xVq5ujiEBJ~>}f(1l?-sUy2)ehn- zu`L`^o=X%}>hW2ie9+VSTx_vJ)c_YBXHqZ`9_prpgvtu9#)5~wECU;!LD&mF7Q=R6 zKdiR>jvX`5=tpe&O3_!KZ+^cW#5OxvEi6l)qyYnrE&~&+79K@*#dbSLptRJ%F|k1d z1-FTLoX~e)FR7ul6Ftd<(kN#%=DJY_eeq!qBk(pR!hC!nfe(a1GgMCCW>tC4n1its z@HL*{hw+$(PBOU VOitl}FHByRr&QY5;iPy^_#Zz0ArAlm diff --git a/sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc b/sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc index 79b986aeab03430f68539b00b23a444c8a17c6f6..74f617660eb63ed6403022f950755661f7bc913a 100644 GIT binary patch delta 36 qcmew_*CWpp%FD~e00g`zCns&>*~iQnJoz-UDsyp4#pY+sGr0k{M+*Z0 delta 32 mcmeB?|1ZZA%FD~e00b>*&k{EB>|h)@9%nvAztGD}i(i^M=8U_u--4*X*)SsLov7XU>vV>$7qweHp$#7Ay6*eHfwjjb0L^w@;FWJfO24Y$P ViCauXsYTk8+okk?4&Ek}1^|~|FcJU& delta 204 zcmbPZHph%7l$V!_0SG<^b|!A*aTZ{_H90^)oAK7>8UbBqMwQL0gv%HiRVOQodIQN^ z(ExQ-pyDD%5TOPnG#PKPWR|4n7KwpGz=Sx6r8)VDXb77+h^ac+PAn9td5YK;AnC{< zJ~=@=mC+8&x+~trXgE1TVm+hdWC_VEM*YdnlHsCYE9^je?LmYyh;W(wUb2(l1H`le V61SL&Qj2saw@c{(9lT8{4FIKlG9dr} diff --git a/sdxdatamodel/models/__pycache__/port.cpython-38.pyc b/sdxdatamodel/models/__pycache__/port.cpython-38.pyc index 8fbaf42246906dede4257c0f3ab847abd5a6ed2c..102dd1b268173c2221e0759170982587457756e0 100644 GIT binary patch delta 21 bcmX?YaoU0>l$V!_0SI_cPEOj$V=n^$KKTW2 delta 21 bcmX?YaoU0>l$V!_0SIpTJV@NgV=n^$L$?MB diff --git a/sdxdatamodel/models/__pycache__/service.cpython-38.pyc b/sdxdatamodel/models/__pycache__/service.cpython-38.pyc index 2530e324444c70cc845b5be0a130dee5413c5c54..2f0e2da23339baba502efd0e685f72ceb15456ba 100644 GIT binary patch delta 21 bcmX?Vc+`+5l$V!_0SI_cPEOj$vr_^9J?{m} delta 21 bcmX?Vc+`+5l$V!_0SI2WbtZ1)*(m`4K=B3` diff --git a/sdxdatamodel/models/__pycache__/topology.cpython-38.pyc b/sdxdatamodel/models/__pycache__/topology.cpython-38.pyc index d573cb0ae782d6f22fd5a3d1142b65d01cc337c9..6462ed951215b6101fb03c32fc0626ec2d5765ad 100644 GIT binary patch delta 1510 zcmZ`(%}*0i5bv{J(01EeSg702ZKcvGL;-0vta<~P5Y_sg4Y%r)i$3moTj z&>we5mOljEI*~+noZGHmdPLs2v&JpgEINr?`wpBiKDnn+ypIC*5suS8oH#K_@Oq7~ zx5q@}&zK0Hv%<(k&v&684%1y32z^GO*Mme6d;qmsV9*Yhd%k%wOAqNFXCasF0zl{| zUwp^>kX_n1^p_A4$tahcV)Z$S$~(=G{nVF2H3e^$SCrzaTrI6D*xDT!dGeb3Aq?1& z!&$(16XaVL0Z{uQ0Z3X@BcDURgu&BTIOA(Lk8(^70JT}TE*1zml88*THHn_a7$h5! zBYxN}Z7lW8qDj`vN%K;;T+vjl7prPXDVAhi#=zS2$eN~9OE^!1yxUYPNWS%F!j@|R zzd$&?KE@`2N6_?OrJ`;sEtQ*I##=7oivYINk_LY;VzyGYblR$QYbn4g)a5m0MXz;K zd>p9Xyae+&aS&R$VelG;wyhB*w|tkLna#!l;{r?v%huppTfcJ@Vf&^+#2G!}C+*V4 zadZ)lGyPmNf1=-dx%$l1kFge@c8Z#|cp>sq_)okTc`rPlVRv%j5h!8BY@0@**nF!^ zFJq@FP6NiO;^Wvsfz@0AxVeo|fWd7f+`SV=LIgO{T|^LujdH?|1h(>F+x&KN3Hhz< zf2Vdk;=S~3G(vu+$5EUVM6o0HKr~X4S9_x3imc=7&>-E_g$!rb`FY#Ser9hFCTqG? zeYJB@$OO3*f;@Ha#^(*XaoG2-$*Q0ldp##=u+!69PkDzzqkEfqMc;@yFa_? g0-P;^k-+?umw8DeIut|nt38psZ`nOFM|N_BzZ?b^R{#J2 delta 1508 zcmZ`(y-yTT5a)5LkV} zS?xLyicVo;zWW*)XSG~3F7efxreeKZS1U@nBI_~+R?{nMno_Ue68$N9EXAVaTTh<1 zjRpJ(!U-EAtP{8drWY$UbxmoiZ2A~)wSvz9*p^H7g#96_l(MeVQq8HQFw0OEmX$@l z(U$R{cjM|1^y9)&DCL318yLzqTa-NVZF*)E8v~3RFl{XBeakKVM2c{{Q#^6S4}=M) zuyGi_k4Bk(ZW=$*Z>3s)YU#&V&qD4bH7)UY@qW>ixlB(jx<$G~0)eq+XF zdLD)B<$q_k+x~a5H_ozU4~gvg z82}I71`Lk{?ghivc@R8UNz>av~L`^?K2@qq-QZ}wQ>j&1j>7}+Y6{sMqz8o&Sm diff --git a/sdxdatamodel/parsing/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/__init__.cpython-38.pyc index b874ae7f1ce5343b2eee9036b7feedcf00e1ea7a..b1c77b9f828df41fcf4e378300edc38565249621 100644 GIT binary patch delta 27 hcmdnWvXzA=l$V!_0SI_cPEOj$vzw7IYVrxjTmV|}2VejI delta 27 hcmdnWvXzA=l$V!_0SKIfIukeY>}F)VHTeW%E&y5A2b2H+ diff --git a/sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc index bc5e37a55dae0167d5ee3aa136815ec389e96725..f7f9c7e07a5c675a1070f1c2b1ca6f0ddff7fe60 100644 GIT binary patch delta 21 bcmeC;=;GiB<>lpK00Q2Vlan^`{9^_HFBSze delta 21 bcmeC;=;GiB<>lpK0D>28orxQH{xJgpG8hGb diff --git a/sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc index 0fa515724e0984517f6f0c50da4e7f3096da8126..5339be8fd113ec6a7176db75fbb3a2d0401d2538 100644 GIT binary patch delta 57 zcmdnQzlonGl$V!_0SI_cPEOj$)62qmYx67?KSoBS%`aJn7`c^zQbk%ILVGeByBnk0 IWMB4F0F)OEG5`Po delta 57 zcmdnQzlonGl$V!_0SIOXJV@Ng)62pbwRskcA0wmU=9jEOjNFPqsUl4fp*5L}-HlOo IvM+lo0Fu-V4gdfE diff --git a/sdxdatamodel/parsing/__pycache__/locationhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/locationhandler.cpython-38.pyc index 4d13cf5b83710ae36a247946f0ef50a595790800..de2e8829405df2de8ef9db5b0a3ce47805e1195f 100644 GIT binary patch delta 66 zcmdnVvy+D>l$V!_0SI_cPEOj$l$V!_0SIJ*IukeYxG=NdV#&$ROTRTaocRx<)aDQtEkq|7ytkO diff --git a/sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc index b48222f482a0ecdae39067d301cf7fd02791b15c..a2a8a2911691967f51b1530a7526954ae6d2af64 100644 GIT binary patch delta 21 bcmdnQvx$c%l$V!_0SI_cPEOj$qs9gRG(iNm delta 21 bcmdnQvx$c%l$V!_0SFX=IukeYsIdV6F;@gf diff --git a/sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc index 15b164a3b59458d22bb03b642a62102c85a40eb5..448f0493c820d9c6138b2993327481f4415ca69a 100644 GIT binary patch delta 21 bcmcb_bBTv1l$V!_0SI_cPEOj$C3djJ3c diff --git a/sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc index 40f530ecfc21bb836926303d0a67b6fa7a8d9802..5a2e52d8b3e9e7b33dca09886523e8e52b615539 100644 GIT binary patch delta 27 hcmdnXvX_M?l$V!_0SI_cPEOj$bC{7aYVrlfTmW3@2Y>(o delta 27 hcmdnXvX_M?l$V!_0SLTl$V!_0SI_cPEOj$W5&!FHQAZjPdthBa^&BuqYjPFw0Y#%&!9KXf0-~Y>Gj;PyQj6lD z9s!9JDT7pT0!@Kl$V!_0SFv|I}3?h_3 z1UHCK0TDB Y4oF4-$*9TYqFO+5k*F$Ab=a3yF(Gai->_#OLLw z0CgLIq^v;dCf^Y{1hi_t@M8gYke~;M2nP{SlP8H}3EbkyOD!qSFUqbc@&k%SP395R eVT_t=DJlmfBY@pM2Kys0&Dp2HvXfOcXlROFl diff --git a/sdxdatamodel/validation/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/validation/__pycache__/__init__.cpython-38.pyc index 30008edd0445241ad49001dfc0847c535aa49041..c52faec40e5760a26426c016b73e4a74feff2ea7 100644 GIT binary patch delta 21 bcmdnNvV(;ul$V!_0SI_cPEOj$qr(IMGzkQ> delta 21 bcmdnNvV(;ul$V!_0SKysIukeY=r92QGw%eS diff --git a/sdxdatamodel/validation/__pycache__/topologyvalidator.cpython-38.pyc b/sdxdatamodel/validation/__pycache__/topologyvalidator.cpython-38.pyc index bcba22f01aea520e1037782a0a09e7243e0b8204..0da53e8d2b561001137fa68d30dbe7805df50799 100644 GIT binary patch delta 40 vcmccOeZ`wQl$V!_0SI_cPTt5JuEcn2a;RB=3.6 +setup_requires=( + 'pytest-runner',), +tests_require=( + 'pytest-cov',), [options.packages.find] exclude = diff --git a/test/data/amlight.png b/test/data/amlight.png deleted file mode 100644 index d9370e392190cd6f65f7909d54390b4d346ab522..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42956 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 ZtrU0X1l_~y;EpNaV=dZZoOM1I^lwkXzMB95 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_topology_manager.cpython-38.pyc b/tests/__pycache__/test_topology_manager.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82c05adbb3579d90ea318d81fa18f2b410304395 GIT binary patch literal 2785 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≧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 LifoLG?#2HB`$%Px literal 0 HcmV?d00001 diff --git a/test/data/amlight_origin.json b/tests/data/amlight_origin.json similarity index 100% rename from test/data/amlight_origin.json rename to tests/data/amlight_origin.json diff --git a/test/data/ampath-sax-zaoxi.pdf b/tests/data/ampath-sax-zaoxi.pdf similarity index 100% rename from test/data/ampath-sax-zaoxi.pdf rename to tests/data/ampath-sax-zaoxi.pdf diff --git a/test/data/ampath.json b/tests/data/ampath.json similarity index 100% rename from test/data/ampath.json rename to tests/data/ampath.json diff --git a/test/data/link.json b/tests/data/link.json similarity index 100% rename from test/data/link.json rename to tests/data/link.json diff --git a/test/data/location.json b/tests/data/location.json similarity index 100% rename from test/data/location.json rename to tests/data/location.json diff --git a/test/data/node.json b/tests/data/node.json similarity index 100% rename from test/data/node.json rename to tests/data/node.json diff --git a/test/data/p2p.json b/tests/data/p2p.json similarity index 100% rename from test/data/p2p.json rename to tests/data/p2p.json diff --git a/test/data/port.json b/tests/data/port.json similarity index 100% rename from test/data/port.json rename to tests/data/port.json diff --git a/test/data/sax.json b/tests/data/sax.json similarity index 100% rename from test/data/sax.json rename to tests/data/sax.json diff --git a/test/data/service.json b/tests/data/service.json similarity index 100% rename from test/data/service.json rename to tests/data/service.json diff --git a/test/data/zaoxi.json b/tests/data/zaoxi.json similarity index 100% rename from test/data/zaoxi.json rename to tests/data/zaoxi.json diff --git a/test/test_connection_handler.py b/tests/test_connection_handler.py similarity index 94% rename from test/test_connection_handler.py rename to tests/test_connection_handler.py index 60768a6..1ccd325 100644 --- a/test/test_connection_handler.py +++ b/tests/test_connection_handler.py @@ -5,7 +5,7 @@ from parsing.connectionhandler import ConnectionHandler from parsing.exceptions import DataModelException -CONNECTION_P2P = './test/data/p2p.json' +CONNECTION_P2P = './tests/data/p2p.json' class TestConnectionHandler(unittest.TestCase): diff --git a/test/test_connection_validator.py b/tests/test_connection_validator.py similarity index 95% rename from test/test_connection_validator.py rename to tests/test_connection_validator.py index 0a7582c..c491bf7 100644 --- a/test/test_connection_validator.py +++ b/tests/test_connection_validator.py @@ -7,7 +7,7 @@ from parsing.exceptions import DataModelException from models.connection import Connection -CONNECTION_P2P = './test/data/p2p.json' +CONNECTION_P2P = './tests/data/p2p.json' class TestConnectionValidator(unittest.TestCase): diff --git a/test/test_link_handler.py b/tests/test_link_handler.py similarity index 94% rename from test/test_link_handler.py rename to tests/test_link_handler.py index 6a59d5e..086443b 100644 --- a/test/test_link_handler.py +++ b/tests/test_link_handler.py @@ -5,7 +5,7 @@ from parsing.linkhandler import LinkHandler from parsing.exceptions import DataModelException -link = './test/data/link.json' +link = './tests/data/link.json' class TestLinkHandler(unittest.TestCase): diff --git a/test/test_location_handler.py b/tests/test_location_handler.py similarity index 94% rename from test/test_location_handler.py rename to tests/test_location_handler.py index 1644dc3..f10cbbf 100644 --- a/test/test_location_handler.py +++ b/tests/test_location_handler.py @@ -5,7 +5,7 @@ from parsing.locationhandler import LocationHandler from parsing.exceptions import DataModelException -location = './test/data/location.json' +location = './tests/data/location.json' class TestPortHandler(unittest.TestCase): diff --git a/test/test_node_handler.py b/tests/test_node_handler.py similarity index 94% rename from test/test_node_handler.py rename to tests/test_node_handler.py index 4f2b3b0..8f5890f 100644 --- a/test/test_node_handler.py +++ b/tests/test_node_handler.py @@ -5,7 +5,7 @@ from parsing.nodehandler import NodeHandler from parsing.exceptions import DataModelException -node = './test/data/node.json' +node = './tests/data/node.json' class TestNodeHandler(unittest.TestCase): diff --git a/test/test_port_handler.py b/tests/test_port_handler.py similarity index 94% rename from test/test_port_handler.py rename to tests/test_port_handler.py index e6d53d5..df6ec70 100644 --- a/test/test_port_handler.py +++ b/tests/test_port_handler.py @@ -5,7 +5,7 @@ from parsing.porthandler import PortHandler from parsing.exceptions import DataModelException -port = './test/data/port.json' +port = './tests/data/port.json' class TestPortHandler(unittest.TestCase): diff --git a/test/test_service_handler.py b/tests/test_service_handler.py similarity index 94% rename from test/test_service_handler.py rename to tests/test_service_handler.py index 6c9b28c..897d19d 100644 --- a/test/test_service_handler.py +++ b/tests/test_service_handler.py @@ -5,7 +5,7 @@ from parsing.servicehandler import ServiceHandler from parsing.exceptions import DataModelException -service = './test/data/service.json' +service = './tests/data/service.json' class TestServiceHandler(unittest.TestCase): diff --git a/test/test_topology_graph.py b/tests/test_topology_graph.py similarity index 90% rename from test/test_topology_graph.py rename to tests/test_topology_graph.py index cc6dd47..0621fd0 100644 --- a/test/test_topology_graph.py +++ b/tests/test_topology_graph.py @@ -14,9 +14,9 @@ from parsing.exceptions import DataModelException -TOPOLOGY_AMLIGHT = './test/data/amlight.json' -TOPOLOGY_SAX = './test/data/sax.json' -TOPOLOGY_ZAOXI = './test/data/zaoxi.json' +TOPOLOGY_AMLIGHT = './tests/data/amlight.json' +TOPOLOGY_SAX = './tests/data/sax.json' +TOPOLOGY_ZAOXI = './tests/data/zaoxi.json' class TestTopologyGrpah(unittest.TestCase): diff --git a/test/test_topology_grenmlconverter.py b/tests/test_topology_grenmlconverter.py similarity index 95% rename from test/test_topology_grenmlconverter.py rename to tests/test_topology_grenmlconverter.py index 9a7c9a1..540f79d 100644 --- a/test/test_topology_grenmlconverter.py +++ b/tests/test_topology_grenmlconverter.py @@ -10,7 +10,7 @@ from parsing.exceptions import DataModelException -TOPOLOGY_AMLIGHT = './test/data/amlight.json' +TOPOLOGY_AMLIGHT = './tests/data/amlight.json' class TestTopologyGRENMLConverter(unittest.TestCase): diff --git a/test/test_topology_handler.py b/tests/test_topology_handler.py similarity index 96% rename from test/test_topology_handler.py rename to tests/test_topology_handler.py index 4e268cc..999df70 100644 --- a/test/test_topology_handler.py +++ b/tests/test_topology_handler.py @@ -5,7 +5,7 @@ from sdxdatamodel.parsing.topologyhandler import TopologyHandler from sdxdatamodel.parsing.exceptions import DataModelException -TOPOLOGY_AMLIGHT = './test/data/amlight.json' +TOPOLOGY_AMLIGHT = './tests/data/amlight.json' class TestTopologyHandler(unittest.TestCase): diff --git a/test/test_topology_manager.py b/tests/test_topology_manager.py similarity index 91% rename from test/test_topology_manager.py rename to tests/test_topology_manager.py index 2bc37cd..2be81ed 100644 --- a/test/test_topology_manager.py +++ b/tests/test_topology_manager.py @@ -15,9 +15,11 @@ from sdxdatamodel.parsing.exceptions import DataModelException -TOPOLOGY_AMLIGHT = './test/data/amlight.json' -TOPOLOGY_SAX = './test/data/sax.json' -TOPOLOGY_ZAOXI = './test/data/zaoxi.json' +TOPOLOGY_AMLIGHT = './tests/data/amlight.json' +TOPOLOGY_SAX = './tests/data/sax.json' +TOPOLOGY_ZAOXI = './tests/data/zaoxi.json' + +TOPOLOGY_png = "./tests/data/amlight.png" topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_SAX, TOPOLOGY_ZAOXI] topology_file_list_2 = [TOPOLOGY_SAX, TOPOLOGY_ZAOXI] @@ -63,7 +65,7 @@ def testGenerateGraph(self): graph = self.manager.generate_graph() #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout nx.draw(graph,with_labels = True) - plt.savefig("./test/data/amlight.png") + plt.savefig(TOPOLOGY_png) except DataModelException as e: print(e) return False diff --git a/test/test_topology_validator.py b/tests/test_topology_validator.py similarity index 83% rename from test/test_topology_validator.py rename to tests/test_topology_validator.py index ecc87f9..bbc6f16 100644 --- a/test/test_topology_validator.py +++ b/tests/test_topology_validator.py @@ -6,10 +6,10 @@ from sdxdatamodel.parsing.topologyhandler import TopologyHandler from sdxdatamodel.parsing.exceptions import DataModelException -TOPOLOGY_AMLIGHT = './test/data/amlight.json' -TOPOLOGY_AMPATH = './test/data/ampath.json' -TOPOLOGY_SAX = './test/data/sax.json' -TOPOLOGY_ZAOXI = './test/data/zaoxi.json' +TOPOLOGY_AMLIGHT = './tests/data/amlight.json' +TOPOLOGY_AMPATH = './tests/data/ampath.json' +TOPOLOGY_SAX = './tests/data/sax.json' +TOPOLOGY_ZAOXI = './tests/data/zaoxi.json' class TestTopologyValidator(unittest.TestCase): From 398af983852bdb24c6d0612c74e63bab48e8f64b Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sun, 29 May 2022 15:50:10 -0400 Subject: [PATCH 34/67] tests --- README.md | 6 +++--- tests/data/amlight.png | Bin 43502 -> 41214 bytes tests/data/p2p.json | 14 ++++++++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index de05324..4050ab5 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,17 @@ There are defined in the *schema* subfolder. Some attributes of each objects are ## Usage ## Unittest ``` -python -m unittest -v test.test_topology_handler +python -m unittest -v tests.test_topology_handler ``` ``` -python -m unittest -v test.test_topology_validator +python -m unittest -v tests.test_topology_validator ``` ## Install ``` pip install -r requirements.txt ``` ``` -python setup.py install +python install -e . ``` diff --git a/tests/data/amlight.png b/tests/data/amlight.png index 7ce243a94fa12ce44143a4f0867c1b6bd257fa50..59e8a51bf2bb9b46d50a80686dc033777aa5df3b 100644 GIT binary patch literal 41214 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 literal 43502 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≧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 LifoLG?#2HB`$%Px diff --git a/tests/data/p2p.json b/tests/data/p2p.json index 51bf1ad..022910c 100644 --- a/tests/data/p2p.json +++ b/tests/data/p2p.json @@ -3,20 +3,22 @@ "name": "AMLight", "start_time": "2000-01-23T04:56:07.000Z", "end_time": "2000-01-23T04:56:07.000Z", + "bandwidth_required": 100, + "latency_required": 20, "egress_port": { "id": "urn:sdx:port:amlight.net:Novi07:10", "name": "10", - "short_name": "10", - "node": "urn:sdx:port:amlight.net:Novi07", - "status": "up" + "label": "vlan", + "label_range": "100", + "node": "urn:sdx:port:amlight.net:Novi07" }, "ingress_port": { "id": "urn:sdx:port:amlight.net:Novi06:9", "name": "Novi06:9", - "short_name": "9", - "node": "urn:sdx:port:amlight.net:Novi06", - "status": "up" + "label": "vlan", + "label_range": "100", + "node": "urn:sdx:port:amlight.net:Novi06" } } \ No newline at end of file From c713cbbebc8e83b0f5d3686caf4c479d0af1b4a6 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sun, 29 May 2022 15:52:07 -0400 Subject: [PATCH 35/67] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4050ab5..0d4ef9c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ python -m unittest -v tests.test_topology_validator pip install -r requirements.txt ``` ``` -python install -e . +pip install -e . ``` From ff5681160be79fa5c1d8e51e3369c9a5edfba8f0 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 30 May 2022 11:57:32 -0400 Subject: [PATCH 36/67] cleanup unittest --- .../__pycache__/__init__.cpython-38.pyc | Bin 564 -> 0 bytes .../__pycache__/connection.cpython-38.pyc | Bin 8062 -> 0 bytes .../models/__pycache__/link.cpython-38.pyc | Bin 11332 -> 0 bytes .../link_measurement_period.cpython-38.pyc | Bin 3980 -> 0 bytes .../__pycache__/location.cpython-38.pyc | Bin 4335 -> 0 bytes .../models/__pycache__/node.cpython-38.pyc | Bin 6940 -> 0 bytes .../models/__pycache__/port.cpython-38.pyc | Bin 7243 -> 0 bytes .../models/__pycache__/service.cpython-38.pyc | Bin 6341 -> 0 bytes .../__pycache__/topology.cpython-38.pyc | Bin 9921 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 565 -> 0 bytes .../connectionhandler.cpython-38.pyc | Bin 1667 -> 0 bytes .../__pycache__/exceptions.cpython-38.pyc | Bin 1034 -> 0 bytes .../__pycache__/linkhandler.cpython-38.pyc | Bin 1970 -> 0 bytes .../locationhandler.cpython-38.pyc | Bin 1593 -> 0 bytes .../__pycache__/nodehandler.cpython-38.pyc | Bin 1586 -> 0 bytes .../__pycache__/porthandler.cpython-38.pyc | Bin 1618 -> 0 bytes .../__pycache__/servicehandler.cpython-38.pyc | Bin 1751 -> 0 bytes .../topologyhandler.cpython-38.pyc | Bin 1887 -> 0 bytes sdxdatamodel/parsing/connectionhandler.py | 2 +- .../__pycache__/__init__.cpython-38.pyc | Bin 573 -> 0 bytes .../grenmlconverter.cpython-38.pyc | Bin 2417 -> 0 bytes .../__pycache__/manager.cpython-38.pyc | Bin 5659 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 568 -> 0 bytes .../connectionvalidator.cpython-38.pyc | Bin 5269 -> 0 bytes .../topologyvalidator.cpython-38.pyc | Bin 9684 -> 0 bytes .../validation/connectionvalidator.py | 4 ++-- setup.cfg | 3 ++- tests/__pycache__/__init__.cpython-38.pyc | Bin 500 -> 0 bytes .../test_topology_handler.cpython-38.pyc | Bin 1860 -> 0 bytes .../test_topology_manager.cpython-38.pyc | Bin 2785 -> 0 bytes tests/data/amlight.png | Bin 41214 -> 19477 bytes tests/data/p2p.json | 2 ++ tests/test_connection_handler.py | 6 +++--- tests/test_connection_validator.py | 10 +++++----- tests/test_link_handler.py | 6 ++---- tests/test_location_handler.py | 6 ++---- tests/test_node_handler.py | 6 ++---- tests/test_port_handler.py | 6 ++---- tests/test_service_handler.py | 6 ++---- tests/test_topology_graph.py | 15 ++++++--------- tests/test_topology_grenmlconverter.py | 10 +++++----- tests/test_topology_handler.py | 2 -- 42 files changed, 36 insertions(+), 48 deletions(-) delete mode 100644 sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/connection.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/link.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/location.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/node.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/port.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/service.cpython-38.pyc delete mode 100644 sdxdatamodel/models/__pycache__/topology.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/__init__.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/connectionhandler.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/locationhandler.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/servicehandler.cpython-38.pyc delete mode 100644 sdxdatamodel/parsing/__pycache__/topologyhandler.cpython-38.pyc delete mode 100644 sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc delete mode 100644 sdxdatamodel/topologymanager/__pycache__/grenmlconverter.cpython-38.pyc delete mode 100644 sdxdatamodel/topologymanager/__pycache__/manager.cpython-38.pyc delete mode 100644 sdxdatamodel/validation/__pycache__/__init__.cpython-38.pyc delete mode 100644 sdxdatamodel/validation/__pycache__/connectionvalidator.cpython-38.pyc delete mode 100644 sdxdatamodel/validation/__pycache__/topologyvalidator.cpython-38.pyc delete mode 100644 tests/__pycache__/__init__.cpython-38.pyc delete mode 100644 tests/__pycache__/test_topology_handler.cpython-38.pyc delete mode 100644 tests/__pycache__/test_topology_manager.cpython-38.pyc diff --git a/sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/models/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 8d865d5f4bcaf2d651ebd150b0e412e1155af07f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmYk2&uSYn5XNV;@@liT>7|F}1qxfJw+|3Pp`k|~8gf}EVjan;igwqP*5nWF$?XG_ zzCd#9qx2PY?J2JiD9&gLsRw45-ynVSNoUh(4iwYhKNk}O_!D72Mg(su{Rb)v6k2Go zLX17LtVyb*;T3O2)u>6UbeJbrR6>hep;MjcY>SF-*&S8~YNXO{pbt)9i&d_YQz*ne z=|X|wS-tR`UH4j=X4SdA;6W(k+PcvpThv|;U26Q&SRMH1&b0Fz{%W?mJ}2K#!KjO# z-%M!wwJ81f@)e1G0GtK*DReJm+3$RGzdjeEx-mvXu8*G^06;)5&E`Pr%_V+qmV zZgA{U!oV+Vw+I>Cp`Vj72->=~+6N(JZ7pd;`+H>)Z)HdSjvYtI!Nr8Vzzm<`BZCHv AVE_OC diff --git a/sdxdatamodel/models/__pycache__/connection.cpython-38.pyc b/sdxdatamodel/models/__pycache__/connection.cpython-38.pyc deleted file mode 100644 index c45544f5c0fd465c46677fcf573a41a681484857..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8062 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 Zy{tr%5WwwoB+sCkxM-tl4v*P%<$r%bTIK)% diff --git a/sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc b/sdxdatamodel/models/__pycache__/link_measurement_period.cpython-38.pyc deleted file mode 100644 index 74f617660eb63ed6403022f950755661f7bc913a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3980 zcmb_f&u<&Y6`ma~m&-LND^Bc2anlSMAPCc#bb|Im7={ZwHVUN74>>@~I9LyNM$$^; zE5_-d-rx^+rQ3dEa4g3NHf9mnPHP$NybOmTz||rH>G0S zu#dNlnf7}iRrz+`gC@HgC$ZjUho9xx2flEg9z`EDz0EQ<<<&rP67h_1Rn9>)q=X{p~z6JTZN?`y|b7DVZf{ zH&>hX-94E}#f=nfwA*J0R>J}|Q?oT2b(4H@yq0TxnpW)u5O2z?3ue)0Kr!!|kSMRlSC2 zixvAOk`cVQc-#}K;<*DdydW-#=i%NGFNn)H^wiPH=Z>rLX09rh76SH8I`)z7jbQ`h$;l2L;pt9y%P@{p;4O|{ zM`l)_A~f*uFSKR;qor%^9uG^v`>PAvSS-u92?g0Ez??V#+dXNt-Bw2h%Qv70f>}QM zX{WWDk1?jNq?svj*83o&Re-Kp#|tEX!EXP#;RY`9^}w6wdbgO&vKz>$Me_sDq?)=l9O#IM?}3zMoD3AIqC*`e z*1u4Wr+B#mj~p+}|5FlwuOv}9#rL9$?>L)P#ey&(!Ys-n#3jS=c}5f{^Ly3e!}%-A zMNy&p7P|OrrHg9xH!yTo8r4Xrj;i|&6P3T?I2FA?9<7>2;foqrJShC9;UPMkdyXTU@cehC2_twZL70}f8U>>lk!?*rYpbOXMuYG9ak_&O}EP&GuB7sRIoQm+zM4Fg0u2F*}uS3yS zpfV6Ig$MX9!S}pFcu`>Vq=T|y@)*ZIqh(`@Yi^8E&%ua}S9(uQ@m+>D#(Xjo{H6el z|3D%0-Z`RlvvzNc^MsG}O=!50cJlMVz6NtTuxmuXrl=nS@Y*4->%t!E>fWhjizoxn!8GEh>_#-{Y` zoL*ekKjq_@ENeS*_cUJIg9lJFo}PsI^aU@1KOc@x`oESRc`L3*ga5Vye6#9BbpGJ&tCF1gP5%u*_HZ9)M{*`X~*?;DjS&Vz0 diff --git a/sdxdatamodel/models/__pycache__/location.cpython-38.pyc b/sdxdatamodel/models/__pycache__/location.cpython-38.pyc deleted file mode 100644 index 481d9907efe3d02e5742f4513f06b309efe04798..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4335 zcmbVP&u<&Y72aPYmn%||9mlcawwc6e5C$1DZ@%un%SA1H#6UR z-+MEUAJyxgfv5hjueQdQ4CCL_nSC5|?xG~?sJPKJxXG=)*){ds>RS43ckKrTx4Cm{ za7Q>tcDKZ>O`}l?-Z2fsV|X?y@rd<;h;4@vXYoj}K`aFewy3%JMX<9Y zBnuS#WLK$SyVXjvPt7p??5%}Q4HiolN9>bO_L|#Lh$!Zw842|cyD^=3&a4)sTa6}g z-C$AtBxtku-d(?yq5EJcqF?;#mn<2I9{XI#B#fgryVYE8u50x7;z$KO)n*6#VRToD zs24V4xueG(h)Bpl3C^|-+Kj*^P+%uiyQ8gUFCOH&+zf_c(e459j)@fUa?5@L0yv!@QUE)9DRqh>|-Lk0gC0;u=j?Atr+`THV z7rhE!<|}%{6COrZi(Z9a;+OTvl32pXOL>o9;V&N>-5S5jU%|W1ukkg!m-(ywI^HY% z2mFV4uW}dkzm~f1$Gt$ofa9d|!Nd33?BQ;huwFk%5{5=FQZa+-h0U1S6>LfyBfI3I zAbBX`9T^Oo8A}^6h*M1PQ0Yn8nu8!31^xa(Q?rrUiIQn)yB`OtVW*XVb19M}b^E}i zMqH%TejLHP<47_^8BH;g>nxi`4AO1Cx)NZhzGPSU4ss^9bK0u7y0Nko5pPV;AmT!n`J-DrK3!1-I` z<*m-<{Z{bB&4ll_sPI4q8XxseJNi@7%JDUa2f*ZqQK)?XZ8F_8REAYHPybxBvogMv ztKZD$^l*3-54EXi4v43Tv1h<;=J?A8LM7S>ruwsZ8(sKUvEqE3YxkgIsV#*XNyPU< zINn94(?ojOQXLFMo59P91)rI63E=Qr!AqMmt8ls++SETsBOz}x-0^CGaJqL-56~bU zY8P%t=d^YuxE<8C#>bm4RNKp9MPnTX2%cQU@2T&ix*hova5bzJ##8+&a`IL z3v&90g45iSzb~m5p06pLVO1X+v%J{*pN@?|KMbE}WWId19O$A#h-<@S%K+~%k63xUo!@@&Kw zXWI)GC^VhM>{(S2u65{fF1vIc^a>^2@#P|Bi`~Biy8`fp>b#`<0!Kmm5waAp-QSux#O`hagMAb6GxkM+jz37Diep>koa`sJatd#$jiRd%qqVBR#h<* zcP!0(>YW&~vnNg=zocqf{vQ*Aq=04>XiA4z2Qnq>_16gfY`LgDoE1`3)rggJO)|DEw`@Q1f;l89+a|gGMH-*Lyy`H?d5HZ_VJ~ zLB0X3_L@e#rqOOATi1A<&ewFw>IJ24(VY#bPN>9S4z0Y7vEQL2wh)JGnccTRsN<{rjb?Cvt};oDFF+Xru_I1U$7)j1%%NbKIQuFXRegw8Nwr z0=K{~3CfPMC>B}HhfK9R970N^mEm?Q2Z74yIOpm~%YcL>zrw!R?PHBsX8i?Y3uud! z!o@6jt3lohoY(Fn%YLMHe#8R&G<%f6c+@cEIz-Pq9Vx*hU3~vfGZ0QMz-~SuIqOFXxkZ7!7cRHQMB~p}QlSs z`?SM!qyl5aJy7vFmP+dsDB;#f37^94GJSKW2i}3sP}6cE6vFrP=TPQ=+6it!UcO7! z&!{>@u3I`EBFMCCLK3d&)GQN!-;^uUciCI9Ot0#go;Uw?ue%QZDtU;vX^$qV)RIEJ eMvZbty_Ps{dz_(}9qy2iCFJCe=bEnNTHgXH(hj)* diff --git a/sdxdatamodel/models/__pycache__/node.cpython-38.pyc b/sdxdatamodel/models/__pycache__/node.cpython-38.pyc deleted file mode 100644 index 96a908033e17a32155174850c82257ead5a747cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6940 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 diff --git a/sdxdatamodel/models/__pycache__/service.cpython-38.pyc b/sdxdatamodel/models/__pycache__/service.cpython-38.pyc deleted file mode 100644 index 2f0e2da23339baba502efd0e685f72ceb15456ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6341 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 diff --git a/sdxdatamodel/parsing/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index b1c77b9f828df41fcf4e378300edc38565249621..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 565 zcmYjNOKKc35LKx)t#*%xY_fl8cQ=#P=6xz;1A=~BnL3L zfW6F7as_R@@)csjq|^yy3hMI;)vKpEn@nuq)KX5v3gXE>aXW_Hb9PP?mfh!p>fVvd^M zqWdX^C0Ub?hd0oZHEh7KHQa$B_uyGiV@H9a-{=9krC%88Nv_92n8x%%Aoc9=_jbE2 z5*H~$rW@riO#jI-gar+B`Z~bk2XSM>zuuQ;UOQi2U7Dtxoqj0g#VfBa%jin>vTl{O z5&Z|9blx<}a%W}8kF4?id0ThKDbo53LrN&gNKV`&E*^(mh{dX3IW2^XKdyM|r=3J) z38F#XAlXHRz?Qb1hYV-%bV3A*0;z=e?3B1hD}6ZYd&cJ%kzaXjy!VxPW%44%Uu DH1CbP diff --git a/sdxdatamodel/parsing/__pycache__/connectionhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/connectionhandler.cpython-38.pyc deleted file mode 100644 index a31056a87896240c1fb6d7eabccf306436de17b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1667 zcma)6OK%%D5FT>5tCb?BQ5qDrk;9@dM2A>Fkpf0h6b*v3MIQ!QBZ#{fV!NcAwO1>J zq)=N}^-vqgDd<0F?W6w^uRY}l^wLvj){^DKJ(L1xxFbHk`Mx2atgUqb+uzxG_NM^& zi$1Ql06)IKZa+q&K(P{1^1Ngto^l2%P@)V*A{7k20u`$01XOg)(@?P;=(k?sI@sLk_I(M zu3!hw*>*qhp`A{&@v)ReRX8bqXLTy_B&;rn=ghaI%*)JLDbL}bhl3|p8#_2UEULlw z&XoDi&Ir=~aH9LhfIEc(g7z;3l0$s*F#hAcYh(1Igp^wnFoUHAzqz6(y&ybfwnhjmo9RQxiU zx9c_v-`@7TP+nBoNc)a;nQ_t;IF5CtE=FAx+-M|sGB(cN(5nyAlxt+(!aU6_GCeZ4 z$-GU5#O;S(9OxtK<8SrRV`Ij~5UXBfX3rYpuHP|4nHQwaYBLm{;P?QMq^$*rG`cw7 zGi}C4_bZ4KU7P{B;+dMz`2KeuvNQU6=U3H+xNR%7uV5 zf#$73&PqT9IPfJQQ&ZTVb^95}m;4y~I@@tDXBiCh#Q&w!z#Ww`e zglMQ+D$ke;(G_to4x4yCG6_c0Uh|yT%~ioQHSIfk`DELs-kU{NPFWEVe|7=?bE2}* JgNm}t{{?07mtX(@ diff --git a/sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/exceptions.cpython-38.pyc deleted file mode 100644 index f7f9c7e07a5c675a1070f1c2b1ca6f0ddff7fe60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1034 zcmaJ=&2G~`5Z<*NCk;(i^~P;MNGL+o4?tD5K#7ou4^+M6vU0praOK$PuC1nNdTQT* z7l0#=!Yla7TW~h1_!9-37P>@ zOz~S(U^niFW9%pwV;4Baj`EIKzZGNVJ>eJ~pqgzoz(!m#%;h6jIhgNOJFlZ68s-Yp zqgf2oBFVG)v#vmS85s~zFN)J#i^2P&aQIG4BW;rGgUt&2Ue%E@izXmt)s=FRt1=~j zQ_Ax)N>@8=DODaz`ITuOky!I%5S$p$Cb*m>Sr8t-38IVVM$H0Jg}AU5q!g`#)!jh@ z%Ef7K0;R0CZbG03KPf*TO)Q6OfBRS%2%lN z$rgOUBwILW@#zp15BpBF)!b&8uXbQIg}4AEn+<$h&p>*rH?WkZL0c2A+ZfWbC0SBP zxk;B1bBz0Z{`j9w@74!m;<^^{>2L#C@y~3*&Jg&S^M$6&3Fl8&Meopbb&a$nDKxz7 zU0v@ZY9A+U5*16x1isJp1N{6BA1BHbT1pyh(?P?yN57x*NB`@v{clC8&ffALvTx^=Ky3TJ0A^J2 Ap8x;= diff --git a/sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/linkhandler.cpython-38.pyc deleted file mode 100644 index 5339be8fd113ec6a7176db75fbb3a2d0401d2538..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1970 zcmZ`)PjB2r6rUN7y)p=G zwn^mlKqB>2f#3@y7o>azz5y3Lz#M@~zXBI{&)MB95gqHjH}mt1=l%Enb~_?4+Q0q0 zQ8ozq6BqNtgT)6x<}F~7NLr8tB`fHFC5#g3NnUsZo^VP&CDNC{F_FO$OMFS!NU!l6 z&XLs~b^MFG+qBdGzA?7y?p~%wHXoK5?w=p10S;! z-czzkC6nH9m~hFZe@x)cKsG=IGL%h_ji-c@Bm_Mo*TVbnrKK#CKKSfb)P*rwb~lH* zJ4!Wb)V15Hn+?lSWvEV9tA}G{Y*#9i>D-PG%ggSL0abw81<_GP6`N<&HHFBE)EFVo$lv$+_l?q~ zzrUB4{nfQk`sw4_M(*{IWNOpFP^tn~7mJBOef;e9x!o(HeI0^UEmV&KBQ&B1SLQsk zGF2x-Gp1t^CLN%UfXv5WY|11JP)0!2VApctqv z{feHDQ?eWAZ{ds&X95|_upPhxz#6hK!=3=v04$W@4EqVN5U{3f&ahtqYXTO@Xomd? zSOi#0wr1GxfVBXNWjw?F04xTq{eYD3a$@Oj^dr?bDLL#=a&X}S$w!qEkI5#tU8uU$IEn} zTx7O~+KPJak6>zCWb=U%27)_s%?*g)<6PQpx1^QHStE?!7 z#<-=C&L6=Gkxt9ZxHx^3=0&=Z7rEVcO9Pdfu~q|BS^?>p52db4G}a3cYmI@|FCkk( zh8eA2LG~)LtH_p-T|@RVvg^QlzT>-U-?;E|wSQOZq1Kpej;DIdXiTi$qQ<`CIHv3W zUFDi+d~JK;S+S9Lso1D*O}vpP9I!OuXiMS^#K=*loA5#1o-xca#34MXe*46YjOe}${ zt9#iq?}rWa@TwsaH)m^qSa4v2>t~ zMnXU{92Yo`g?I>sGI`SwLJl(_>K1tJk~iSf`b}W2J?XZYDhk+g;R)=O8Wqw$FeHPi z8#2yg9(F^&#(RN&4bbG;d<$NwyRun9BkHtwk(&PJOkEQ7*}2Q+tbgy^iucifwc{}- GWA-nFAJ-}X diff --git a/sdxdatamodel/parsing/__pycache__/locationhandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/locationhandler.cpython-38.pyc deleted file mode 100644 index de2e8829405df2de8ef9db5b0a3ce47805e1195f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1593 zcmah}&2HO95T0EwDN2=M7f6A)Fc3A!p*q9@f+p}GC{h$@3)dCU8bMrM77Tag(56Ub zcZJvjl|yVGr=W*kdbE#zl)eJ5J>?a8>I^N}s!*T<%_> z6jj%~(%q!UbDdB~U87%5v~^vjZDLY4CSRI&4=s-M9SNni5SnvT3f$gCZ$V1v5-+=AjD3N_Gwxa1@w(B~xsJzVU0{WAt(KdCI%??V!&? zpLhZJwgAVDTIP(w%x5tD-0{*sM6PYXVm#&V*b8`sfX@84{yzK)Io}5-N?v*;a^ZtF z3(5fN9t^5Rtd!B#BDlywH&ME3W<`EbPfblo&#Yk*=2J53XnKAnj`Rs0`B9%dGo~>BibN4p5OtBh!ifVa2DQuw@3+kvckG;({DRHA z63)HSyI{ypEWji98GeC%1Q!7mgNm7{KXJnc+epe#=SiWkM+l-~tn*4x8&sHktjryJ zrB1vj=I{*D4B@6UB~8s|v||O8wUt!v;vO5I0Zbk_dG~)(PiX71P)+Ns=@JKr_%kvz z?eRLQ)zZ}A)b(i}=$qo)#}l?@J)3T>hOoS`*Q5^4w*v~+x#D`K7gnI diff --git a/sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/nodehandler.cpython-38.pyc deleted file mode 100644 index a2a8a2911691967f51b1530a7526954ae6d2af64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1586 zcmZ`(OK%%D5FT>5tCeiEPHLcW8lXZSKy`=(6bax%QS_BIXix#I5yagKu~|~~+N)iK zq*_~8^-vqgDd?d<&-T$jrGJ6fp7IxZ>dab7R0Ej>XGks|Gv7DE$K7rRP`ZEr`Sg(h z_=g6o65!wqZ1z)B3KT0KAY%e#vlzgRLKOYx8pdOXp1fbn5hj<4jLnUX>ZGUlsCz#O|QdkT3-aS1bvh!-y%a zg0nagN~rJ*@XkoJ&_*g&ZM3cDARvj+kB(oX`yZyIDzuq>|3RmRvKaP8mFZ1VLmu_q zf$n8hS?Y}3=^6cOs;%oOZ8MX*3C-nY@6e)c&d4aFrBI(^Bd6^qDhH=<&Mp9ff*K@O zunU)L*bjVYrxR^_EM;EiPD(FIbdZ3ux)@zD-jbGxVNCu>%k(R0UdjA75Db!#Kh>;8o7sb1F#O8ZLT1*y>us|FqT3& zLhWm8_AgYkZ)&F47R)(Xjy6CWpcQBZ+7NAsHc~KesiZ~;e=`3mPFoNqmWaz}t; zM;%i$n8yrew;iwJ!$7sRU};bJ@9YWONATwHZ}A@d3?<(KCu&{?wWz~7s#_|45zX6m z8(D4-Jw=q7mLFxvn zo21?$MSS){FOKwy_3Dq?Pd)RS+tTeUs zBKYIZoI7^TYkt9yRX7jo;DTZ53gY2M_zCuq4a8auJu|a#>PB~WFl%F7W|hJMB7~}m zF1?@{@uH|wW!}SEno#>zlDg&=t+ouaco|pEsUdC!lemVIkhvR}_4Vt8G&b*J@?Cpx>r4rbNRs?nsHnI4!8Z zF6FanS}gvwq*PTVWmAGzkbH-h8!{>Dr%PUE9OG1i}*$y zHuL?+ypOZVt<@)ByLlDtHsw*1o4%u$@2qX|w%NOK*-G@ET}AOZ;nx5kQ?~K{0Ip_m A=>Px# diff --git a/sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc b/sdxdatamodel/parsing/__pycache__/porthandler.cpython-38.pyc deleted file mode 100644 index 448f0493c820d9c6138b2993327481f4415ca69a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1618 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 WVvAGo<%NA;5z(3oB)P2RaOYofFvNxc diff --git a/sdxdatamodel/parsing/connectionhandler.py b/sdxdatamodel/parsing/connectionhandler.py index 8ca89f9..768c4a4 100644 --- a/sdxdatamodel/parsing/connectionhandler.py +++ b/sdxdatamodel/parsing/connectionhandler.py @@ -1,5 +1,5 @@ import json -from models.connection import Connection +from sdxdatamodel.models.connection import Connection from .exceptions import MissingAttributeException class ConnectionHandler(): diff --git a/sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 5a2e52d8b3e9e7b33dca09886523e8e52b615539..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 573 zcmYjN&2AGh5cYVT-R-6k7Y^_ONQ*?42dF9}5I1t5>SZNlIkgjv>~*j=YEz}Bzyly& zpgr;^yn?Ts`U;g$7%xa-F_JqAf67-FFX zNHsOoW^HC!%j~Ef*}Tn%erAfcP{nPba+Rra2a@gR9kfSsB=avs9laqtXe*hWk-E4? zUQ$DRB<8`Hb+5R#E9d)~#X@S|39Di`7eNhGuERnb71<}JyV*5+K3#1tAW{4?iaBY1 zjUJ{LmSjslJiCFOY+(nUZQ&Lqxr4xZnmQ6B{YsybZ}c-mJ=yh`bKU8lbEKZ#|K9KS zb>XMf@3Yf%=0U|V JeF7yshCjXXktYBE diff --git a/sdxdatamodel/topologymanager/__pycache__/grenmlconverter.cpython-38.pyc b/sdxdatamodel/topologymanager/__pycache__/grenmlconverter.cpython-38.pyc deleted file mode 100644 index 50ff64c12c7660dc5d514e3491d1b7a2b2628e88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2417 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 diff --git a/sdxdatamodel/validation/__pycache__/__init__.cpython-38.pyc b/sdxdatamodel/validation/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index c52faec40e5760a26426c016b73e4a74feff2ea7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 568 zcmYjN&2AGh5cYVT-R-6k7Y^_Ol@^IC4^UM|AjByLs$Nz?mQy>$$leXMLw^)bfd@dm zKzrm-cm-cM^%YuGVZ0!bk>=;KJl}jXCzDA<&?dir%6Z=l_iBbk3CYVS4KKwHV|nAF8B z@{$_jBQf{ASp~(lUG^^2EEZC`PFNMox$tVJa_tw|sK`F{x|?0GXVc~7DI|)2MlmPN zuh9Jz!x>qV_Xl4fkTq<;u{B(SBsbt$py@?|q~GXM@|}KSs3*G`bFMobI7jN)?ZbAv ztuvP>W2rjn&vp3V7{Z!HI(-pg@sqeQ;=8xaiC4}ymlwKgX2)+DasJ%Pizc}gLD*g@ zlhA+gSs0B-_g%9yHCBg4`*7OZ&I~#1Pq;>F2S;9=smsu|P2^qkJql7sen*T45*br>^qw@3*6L;X%bdeF7ys FhCgylk9Gh6 diff --git a/sdxdatamodel/validation/__pycache__/connectionvalidator.cpython-38.pyc b/sdxdatamodel/validation/__pycache__/connectionvalidator.cpython-38.pyc deleted file mode 100644 index 1cf989f089904da5e5ae676a35ab97079134b463..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5269 zcmd5=TW{RP73OVMtJUgaS$5*uNm(~d%-SohBs;c+BCTM@g$h_MDy1kWD+IMOvZ!*& z^$g`$n-x&h0`k)SgFHx&e$KBc(YHSJFBApR?+nSkSSzPZ`%p?8&gIORb7sDCnf+{X zvZ~?v>tC+=8<#ZgZ`2rkOf-IhSFWRAnyWFL8KK@XTtlb28Je5)wL+^^a!akUTUPz0 zaI95vE2x*{wf2pww8$Z_A_$$MmVAT!Hndp3T{d%?jKX|Gehf&;?fn2dy?H7I+ zFh2?6$Zm$dly(x^9GwS#!tG?wPf&1M9<sjOJcv=_&j2_E8<> zQeJMV`pQ7LRlm7C(Cqy4#^*PFzOuDgzfaxEM|HID-%vf>#jQqtY1^UBqUR~=E}{3{ zw$sgemoasaaOr+`xzQWEbN8MaB`(X0sykmy&AyG%t)<%^-`n15sQTjaoxAt9x0XK6 z_@6h{XgKx8Qhn*Zvk5+1jYUYWI zzC^(_R|ggirn}}>+Aa{u%;Sn{F^iRsHMhjdYz*IWhB$YOU12pgd91q?KF+4T*4Z?h zIkwy?pFsP<*9Mzq7tyZqNwnwCzQiu0J;mN(?}E?tSDK}{GeGPQ(u@5={%wY{Sacp% zZ9JPm)fw!ydgQ-!K|RVftS>lb0`w+m3E?^YA`6Rt9b^~ zHni9Jx?`p#AeoD_;(0+7B%YU6f!`d_ul3ZDJlw^1-1C$*c;0L6ACDG3lU&G!!UU31nbm|#TyuLh_(I;uEG`(|u)Z)YBO+`xNc#0iX zB3wg9qILB|e`Uzo1mJq5|55Mi*lm+`+cDNV4LalAx!bJ+nbWsE>NZTtQRGUScT{%bN;VDC#PIw|A zl*YGEp-soDlUx6@aRhxIYTLCVBQd(hD_z%~&n0FTJ5i$@FxNNX(Xbljzh=HG%G7wnU>V|O%*GF^c0WxNjW-xO`m~s=b@<}U*Bw!I>ke3 z^;El!Mpu_Jq@JKshicl1(bZ4&BeQFYM%NIH#AN!3)iqDEqc+tcr+U>Xxf25r%M_c- zghUV-93tCHt^GKN97`!H5Jffd4i#6a7)Gyn7kwY#l>}`%#IicbsdFr+*jpF`@f|`h zDPO{?&T<8nXW9`?4QfL5Q~hqy!kO-q;>J)9PPOQP+PcqSi^>m?D+59)gbswuzQNgT z5x)o+@ZRD)L3D*l{A%OLx-FY~yyegLn5CkG;;v?C1Q4B}lZa%cxq!mnd>r7d~X{VgWj9A)`Q-B20Z{uUhA4C$Ocao(z4%f z^N6M8EXftqI8#a~$cj8q{0Pm$zx^2X^FenVqpNr`(A6y6(&vn+iP53C4?5Zjf?E=_ z_;$fM@s;YmYzm9zFTZ4~OiE~JNf}W6M2#E6;}{*2Pt^#3oXw=ZQ-i#xnmwjw|3T4M zXKA_y0AKP0|DZnALfHXufhN*FY*}+vY$LKpKG`GH5Ut6Ly>r_BiBbi`$!*5oNIg zZTm6Z73gwWPGC27f$pqH+3L_%)X9`awT*!wbemu*s1dcY$*YgdD=Sis=iN9^8fTc>e9m)w^{`$F#i zlhdz!oa=Up!u(H1=Pg{}?{ISzQ-v64Of^ePW2Fa*sWY9G@hr0ntKwNNiOr!ra`p>Xu z(Vt;Q*mJ->%T724*im3T#*7DLbB;A25+~!ClvN+dZ4CI}Lc_ps8Jjk?wLP0*PlI&8 zG3;zV#iXu^HK#vnYq&1q3Q294GE^+wmy|Em&KKZ&l*j_wdgyqoUnqPQaR2(jnH!sz?b9Kfo8N#V@2_V=JN6h?P{(7%J;n1)oHdx9B!aH=1U6Qe>M-= z71RNqe$+1E5}zg{kup>x^}ZI)Mv$ue>Mzxy3bvMrt!=F{SfGu|Hq+>BhLN%27=36a zbZfLhLWXfDR9+X^;*Zc7(qwIOE9u}?n0XgwJ%i_ylgoQ+09jReC!1{D&< ziL4qVo19h7F{KJ>^m%a0WzdnH<3#XcP^0&ju62yCd(-LJ3mJ-wFo1~*6UuedzD4L9 zxGc{LZoB@PG@$f3V|Q&Kz8ZjybFLrWYBjuH!LyT9?fmzTI=? zja#sdaJTbDg1-lzCka^YPiv+aQ&VeMtgpexU=R3E4H^|4$#oZo@g%gZEp86c3%)aDP#VM16F5| z#)$k2+=({1PoW7BWdy5W%3Z?^!%c^17T+YtG1}5W`&~)hVs1sL<;0BCUK{teI_-)0 z>5=-$AjV4eD&%EoK&-heuGsy)JOnk?vSnjR9jO;Eck%X+m}Kk4In8nA}O^hl`_)yT0$YDXUbE(IB}rBJV~gJwUWDJgD0DfU6;}n*rj@p_6d`C_ z8@yOBNO6Q9mG)1X`N2Hka+~ct4igD6B5-K<>Za!zBj|ROlTD0EVZ`|hXz>^6M!_bh z2r_N}*1B@-hwoopZkJ`iT1~iVHb$S3fq6lYm-jHqypMo4B!yIQIj=Fzjy$=#qp+UZJno*6Z8Ix{zFAOc?n;?=ONBSUr&liV6Q45u|t*)O2m% z2z>7@Rn~l}u%TXaPNoODqY>ggWjd(Q*>bWQrqhrJVPY-_rcfP*J0Bz<@{uTZME{A5 z6ERZ8uM%0`os54?HI%_QK?TgSSW?QH6(_pwI6fHEM=da1FfMI!S|ZqYZ)!KTNko?^p6k!M&iIspx}8&Z)MSd< z@FPtS8W!t>YIp2TiTkIxLUI^n$y?AgVab&m*=*SI#G0dkR3YnL-Cql7vv%b5V=yjR~!KQ;cgn$1z$(AVo7v=?J}(x)~vBU$2VUzYs78 z)$j(`M=&CrFWc_^*?dw|&QgRlSdvL}F16poM^KEO;CLJ*Ed+=DEneQOY6;TC%b7~0 z60`^8=<~Kn`}Y7N+1VH6;;Kk^lkeZOk(`hiL!M3 zJmZ=?PuSbeJb4~7m`&}4sLF_q9UQ{Df^8JpF(N#6kIOqxjFSNOA|178>&ul zb-vC@l$EV3_f;LW?+uk-XZnU3uKPGc6Z6aC^M9oH^STmMHyZr6szQGPvN8_&W5w4# zq;Q}nsQMCAeJNn8y(MkqE^C<3yFa(!wP&)-QbKDsxp zIlKB~^d(rc`5AfVC3WZbpbgBiJxNhE_X&A(qjTo!wM*A7zmr2CgQRa@Dm(&raBwWP zKIeyw0M+TNh$~TzmB3O-NJ7n2F22zl$Qp#>BXRFKRyPPZbA4p=;!K;4h#Z)E=HS@5 zhl4|ihr5~+A5mscc|D2>6LkMoF}fp)4e%IOQ*q81gTpfl%5BL{_v)iZkG}pBZ0+C; zfEUh4KxK@N;64+FR9f*Iy!TkMb1afXTq#hT!3znllsD;~?jrs7M7l|&pWkD`o|I>T zJXn03sgFl?g6iV93W$oX2KxJ+o1TQ2`Z) z;Z))ZIc%zc<>40vkM|VI^=X#BbhC_!IjvYn;N~2QC_4IN!Cc~G?{s$AOQNV%t za$8+)9~2=~+^{VC>%gXmY%6YB)+d{`Cv&g3YTtgGBh%BZlhZTP$7&}8T&I1AUjkS>)x{Yja(aZ%;ex%=<&@gS8h2#9u$ljt2=hR` zw;=v!A~cT_yj)1nam4YJSgHAJ5nNJv7aTAT7eP=tKyajZn9@wars83d>e7FT6qA|m ns^shf-~@My*c;d0kURY*qELCnrRz<#Nyl3n9d9LnC0+YB`67Ow diff --git a/sdxdatamodel/validation/connectionvalidator.py b/sdxdatamodel/validation/connectionvalidator.py index a297d32..50f9e8c 100644 --- a/sdxdatamodel/validation/connectionvalidator.py +++ b/sdxdatamodel/validation/connectionvalidator.py @@ -6,8 +6,8 @@ from datetime import * from re import match -from models.port import Port -from models.connection import Connection +from sdxdatamodel.models.port import Port +from sdxdatamodel.models.connection import Connection ISO_FORMAT = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[-+]\d{2}:\d{2}' diff --git a/setup.cfg b/setup.cfg index ae5105d..dc7b331 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,8 @@ classifiers = include_package_data = True package_dir = = sdxdatamodel -packages =setuptools.find_packages(where='./'), +packages = find: +#packages =setuptools.find_packages(where='./datamodel'), install_requires = python-dateutil~=2.8 python_requires = >=3.6 diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 4b57056b3df1e3bcc5ebda2ab2c805789a3d5bbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 500 zcmYjNy-ou$47PK*9}3+V;02J1M30FHRY)MlE{NqM6kSPxpxj-#9BPZoRCoZy3&6;u zurg+4;uVlkaSB5}$)97(@n`$A-EIM){r)`oAphxuWrP$QklAA*2@(wym|?*(Rs>m4 z@QkC+GZSW^3SUCS6{={4GMKSf%wox9_y8(Cf*EFsWGB#xzNi;;xQnH(%e+u78RS+i zUXivJ+9=1bD_!>Q_;ziISx{Roq5F3sgXZ076lzD0&KlAg$Bb0i1PF$w;!7#l~cvl~=ne zNh9{c&c*qI{6N~9Kc&C0*PeRrt*6dVTF12oG(dq;;4Ei29L{?~QakN-lR#_#_U8GY zoRHs9x!Ax)?n5^}f{;Ygl!Wsy4H@Vx^`<=J6m4GWPitWf#yqTV5b4X>Ya(lkOvI@d zHdIsA_n8h9dAsrp0WUfteenmxB|)E-tK)o@r}^H|W}L}XX)tvj#5R7MOO<|qxT|J1 z$+Mz67+PiQP{PzOo~Fs(3p<#YJln-U7aIfRK6LX92t`6F38YLVdrd-5dXmGJ&yx&& zS(gIznrz4>eCu)rl53UM$FSF>#C9pfs~V1Rqjl(}3&IjhPw6p<*fE)~x1K{vM&yhJ zeXh~9yf)tcVS8(P<5~3W%$$R zv>6^9CfP6;Jsid_zcli2h#eW3B3X_gJ3DC3;1DBr`=Oav4uYf3pRk41Mzsy!aGspm;#iz^ms~rEaB`&~VZp#1$ zMc2aFIi(c129D{3;rI#3zT(7s`%S$`$q87B4$M2hC*&FVl}k2)bWi9j`V{nxJ%tks zMdm@##$aN;`Va*mzQ550uyjVdBY8}su;+~Gj&H}rHfTJ)r+<2sp-rf0X@D(NRAwhMJ@dd zmzQ3uvIRXFkzXe0##Mq`@K3;Z!IFT(xCR_?+M>l5@5hph@qbX&#e8~ONw$B9ss&SS z=3kgfakahSgNoMchdZ-wF@ODvv)a19=<^EYcS zi?vCzy|8uB8s567RX&62J}%xg_o{}UEp=7ZABS<5SJxoDQeYj|V|Yn1ZG8i695Y-7 ZtrU0X1l_~y;EpNaV=dZZoOM1I^lwkXzMB95 diff --git a/tests/__pycache__/test_topology_manager.cpython-38.pyc b/tests/__pycache__/test_topology_manager.cpython-38.pyc deleted file mode 100644 index 82c05adbb3579d90ea318d81fa18f2b410304395..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2785 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 diff --git a/tests/data/p2p.json b/tests/data/p2p.json index 022910c..b9ee9d5 100644 --- a/tests/data/p2p.json +++ b/tests/data/p2p.json @@ -9,6 +9,7 @@ { "id": "urn:sdx:port:amlight.net:Novi07:10", "name": "10", + "short_name": "Novi07:10", "label": "vlan", "label_range": "100", "node": "urn:sdx:port:amlight.net:Novi07" @@ -17,6 +18,7 @@ { "id": "urn:sdx:port:amlight.net:Novi06:9", "name": "Novi06:9", + "short_name": "Novi06:9", "label": "vlan", "label_range": "100", "node": "urn:sdx:port:amlight.net:Novi06" diff --git a/tests/test_connection_handler.py b/tests/test_connection_handler.py index 1ccd325..8cfe98c 100644 --- a/tests/test_connection_handler.py +++ b/tests/test_connection_handler.py @@ -1,9 +1,9 @@ import unittest -import parsing +#import parsing -from parsing.connectionhandler import ConnectionHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.parsing.connectionhandler import ConnectionHandler +from sdxdatamodel.parsing.exceptions import DataModelException CONNECTION_P2P = './tests/data/p2p.json' diff --git a/tests/test_connection_validator.py b/tests/test_connection_validator.py index c491bf7..500fa08 100644 --- a/tests/test_connection_validator.py +++ b/tests/test_connection_validator.py @@ -1,11 +1,11 @@ import unittest -import parsing +#import parsing -from validation.connectionvalidator import ConnectionValidator -from parsing.connectionhandler import ConnectionHandler -from parsing.exceptions import DataModelException -from models.connection import Connection +from sdxdatamodel.validation.connectionvalidator import ConnectionValidator +from sdxdatamodel.parsing.connectionhandler import ConnectionHandler +from sdxdatamodel.parsing.exceptions import DataModelException +from sdxdatamodel.models.connection import Connection CONNECTION_P2P = './tests/data/p2p.json' diff --git a/tests/test_link_handler.py b/tests/test_link_handler.py index 086443b..2311645 100644 --- a/tests/test_link_handler.py +++ b/tests/test_link_handler.py @@ -1,9 +1,7 @@ import unittest -import parsing - -from parsing.linkhandler import LinkHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.parsing.linkhandler import LinkHandler +from sdxdatamodel.parsing.exceptions import DataModelException link = './tests/data/link.json' diff --git a/tests/test_location_handler.py b/tests/test_location_handler.py index f10cbbf..7b7f5d4 100644 --- a/tests/test_location_handler.py +++ b/tests/test_location_handler.py @@ -1,9 +1,7 @@ import unittest -import parsing - -from parsing.locationhandler import LocationHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.parsing.locationhandler import LocationHandler +from sdxdatamodel.parsing.exceptions import DataModelException location = './tests/data/location.json' diff --git a/tests/test_node_handler.py b/tests/test_node_handler.py index 8f5890f..7ee25cb 100644 --- a/tests/test_node_handler.py +++ b/tests/test_node_handler.py @@ -1,9 +1,7 @@ import unittest -import parsing - -from parsing.nodehandler import NodeHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.parsing.nodehandler import NodeHandler +from sdxdatamodel.parsing.exceptions import DataModelException node = './tests/data/node.json' diff --git a/tests/test_port_handler.py b/tests/test_port_handler.py index df6ec70..3a9dd44 100644 --- a/tests/test_port_handler.py +++ b/tests/test_port_handler.py @@ -1,9 +1,7 @@ import unittest -import parsing - -from parsing.porthandler import PortHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.parsing.porthandler import PortHandler +from sdxdatamodel.parsing.exceptions import DataModelException port = './tests/data/port.json' diff --git a/tests/test_service_handler.py b/tests/test_service_handler.py index 897d19d..63452e3 100644 --- a/tests/test_service_handler.py +++ b/tests/test_service_handler.py @@ -1,9 +1,7 @@ import unittest -import parsing - -from parsing.servicehandler import ServiceHandler -from parsing.exceptions import DataModelException +from sdxdatamodel.parsing.servicehandler import ServiceHandler +from sdxdatamodel.parsing.exceptions import DataModelException service = './tests/data/service.json' diff --git a/tests/test_topology_graph.py b/tests/test_topology_graph.py index 0621fd0..6cc57b6 100644 --- a/tests/test_topology_graph.py +++ b/tests/test_topology_graph.py @@ -4,14 +4,11 @@ import matplotlib.pyplot as plt import networkx as nx -import parsing -import topologymanager - -from validation.topologyvalidator import TopologyValidator -from parsing.topologyhandler import TopologyHandler -from topologymanager.manager import TopologyManager -from topologymanager.grenmlconverter import GrenmlConverter -from parsing.exceptions import DataModelException +from sdxdatamodel.validation.topologyvalidator import TopologyValidator +from sdxdatamodel.parsing.topologyhandler import TopologyHandler +from sdxdatamodel.topologymanager.manager import TopologyManager +from sdxdatamodel.topologymanager.grenmlconverter import GrenmlConverter +from sdxdatamodel.parsing.exceptions import DataModelException TOPOLOGY_AMLIGHT = './tests/data/amlight.json' @@ -36,7 +33,7 @@ def testGenerateGraph(self): graph = self.manager.generate_graph() #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout nx.draw(graph) - plt.savefig("./test/data/amlight.png") + plt.savefig("./tests/data/amlight.png") except DataModelException as e: print(e) return False diff --git a/tests/test_topology_grenmlconverter.py b/tests/test_topology_grenmlconverter.py index 540f79d..fa74702 100644 --- a/tests/test_topology_grenmlconverter.py +++ b/tests/test_topology_grenmlconverter.py @@ -3,11 +3,11 @@ import sdxdatamodel.parsing import sdxdatamodel.topologymanager -from validation.topologyvalidator import TopologyValidator -from parsing.topologyhandler import TopologyHandler -from topologymanager.manager import TopologyManager -from topologymanager.grenmlconverter import GrenmlConverter -from parsing.exceptions import DataModelException +from sdxdatamodel.validation.topologyvalidator import TopologyValidator +from sdxdatamodel.parsing.topologyhandler import TopologyHandler +from sdxdatamodel.topologymanager.manager import TopologyManager +from sdxdatamodel.topologymanager.grenmlconverter import GrenmlConverter +from sdxdatamodel.parsing.exceptions import DataModelException TOPOLOGY_AMLIGHT = './tests/data/amlight.json' diff --git a/tests/test_topology_handler.py b/tests/test_topology_handler.py index 999df70..dda7e94 100644 --- a/tests/test_topology_handler.py +++ b/tests/test_topology_handler.py @@ -1,7 +1,5 @@ import unittest -import sdxdatamodel.parsing - from sdxdatamodel.parsing.topologyhandler import TopologyHandler from sdxdatamodel.parsing.exceptions import DataModelException From 1ff933587d2c650ff4beb105e399bc2b63723a3e Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 30 May 2022 21:42:42 -0400 Subject: [PATCH 37/67] refined the models and the unittest to support/test PCE #12, #22 --- sdxdatamodel/models/connection.py | 57 +- sdxdatamodel/models/link.py | 2 +- sdxdatamodel/parsing/connectionhandler.py | 6 +- sdxdatamodel/parsing/porthandler.py | 14 +- .../topologymanager/grenmlconverter.py | 4 +- sdxdatamodel/topologymanager/manager.py | 11 +- tests/data/amlight.json | 30 +- tests/data/sax.json | 48 +- tests/data/sdx.json | 1067 +++++++++++++++++ tests/data/test_request.json | 18 + tests/data/zaoxi.json | 24 +- tests/test_connection_handler.py | 3 +- tests/test_connection_validator.py | 3 +- tests/test_te_manager.py | 42 + tests/test_topology_manager.py | 7 +- 15 files changed, 1263 insertions(+), 73 deletions(-) create mode 100644 tests/data/sdx.json create mode 100644 tests/data/test_request.json create mode 100644 tests/test_te_manager.py diff --git a/sdxdatamodel/models/connection.py b/sdxdatamodel/models/connection.py index c1d2ca7..41aaec8 100644 --- a/sdxdatamodel/models/connection.py +++ b/sdxdatamodel/models/connection.py @@ -48,19 +48,28 @@ class Connection(object): 'quantity': 'quantity', 'start_time': 'start_time', 'end_time': 'end_time', + 'bandwidth': 'float', + 'latency': 'float', + 'latency': 'float', 'status': 'status', } - def __init__(self, id=None, name=None, ingress_port=None, egress_port=None, quantity=None, start_time=None, end_time=None, status=None, complete=False): # noqa: E501 - """Connection - a model defined in Swagger""" # noqa: E501 + def __init__(self, id=None, name=None, ingress_port=None, egress_port=None, bandwidth=None, latency=None, quantity=None, start_time=None, end_time=None, status=None, complete=False): # noqa: E501 + + self._id = id + self._name = name self._quantity = None self._start_time = None self._end_time = None self._status = None - self._id = id - self._name = name + self._bandwidth = None, + self._latency = None, self.set_ingress_port(ingress_port) self.set_egress_port(egress_port) + if bandwidth is not None: + self._bandwidth = bandwidth + if latency is not None: + self._latency = latency if quantity is not None: self._quantity = quantity if start_time is not None: @@ -190,6 +199,46 @@ def quantity(self, quantity): self._quantity = quantity + @property + def bandwidth(self): + """Gets the quantity of this Connection. # noqa: E501 + + + :return: The quantity of this Connection. # noqa: E501 + :rtype: float + """ + return self._bandwidth + + def set_bandwidth(self, bw): + """Sets the bw of this Connection. + + + :param bw: The bw of this Connection. # noqa: E501 + :type: float + """ + + self._bandwidth = bw + + @property + def latency(self): + """Gets the latency of this Connection. # noqa: E501 + + + :return: The latency of this Connection. # noqa: E501 + :rtype: float + """ + return self._latency + + def set_latency(self, latency): + """Sets the latency of this Connection. + + + :param bw: The latency of this Connection. # noqa: E501 + :type: float + """ + + self._latency = latency + @property def start_time(self): """Gets the start_time of this Connection. # noqa: E501 diff --git a/sdxdatamodel/models/link.py b/sdxdatamodel/models/link.py index e018596..0db9a78 100644 --- a/sdxdatamodel/models/link.py +++ b/sdxdatamodel/models/link.py @@ -187,7 +187,7 @@ def set_ports(self, ports): self._ports=[] for port in ports: - self._ports.append(port['id']) + self._ports.append(port) return self.ports @ports.setter diff --git a/sdxdatamodel/parsing/connectionhandler.py b/sdxdatamodel/parsing/connectionhandler.py index 768c4a4..7f4d9fb 100644 --- a/sdxdatamodel/parsing/connectionhandler.py +++ b/sdxdatamodel/parsing/connectionhandler.py @@ -16,6 +16,10 @@ def import_connection_data(self, data): try: id = data['id'] name=data['name'] + if 'bandwidth_required' in data.keys(): + bw=data['bandwidth_required'] + if 'latency_required' in data.keys(): + la=data['latency_required'] if 'start_time' in data.keys(): start=data['start_time'] if 'end_time' in data.keys(): @@ -25,7 +29,7 @@ def import_connection_data(self, data): except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) - connection=Connection(id=id, name=name, start_time = start, end_time = end, ingress_port = ingress, egress_port = egress) + connection=Connection(id=id, name=name, start_time = start, end_time = end, bandwidth=bw, latency=la, ingress_port = ingress, egress_port = egress) return connection diff --git a/sdxdatamodel/parsing/porthandler.py b/sdxdatamodel/parsing/porthandler.py index 11f7962..dfa1733 100644 --- a/sdxdatamodel/parsing/porthandler.py +++ b/sdxdatamodel/parsing/porthandler.py @@ -16,20 +16,24 @@ def import_port_data(self, data): try: id = data['id'] name=data['name'] - short_name=data['short_name'] - node=data['node'] + node=None + short_name=None l_r=None + p_a=None + if 'node' in data.keys(): + node=data['node'] + if 'short_name' in data.keys(): + node=data['short_name'] if 'label_range' in data.keys(): l_r=data['label_range'] - p_a=None if 'private_attributes' in data.keys(): p_a=data['private_attributes'] except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) - port=Port(id=id, name=name,short_name=short_name, node=node, label_range=l_r, status=None, private_attributes=p_a) + self.port=Port(id=id, name=name,short_name=short_name, node=node, label_range=l_r, status=None, private_attributes=p_a) - return port + return self.port def import_port(self,file): with open(file, 'r', encoding='utf-8') as data_file: diff --git a/sdxdatamodel/topologymanager/grenmlconverter.py b/sdxdatamodel/topologymanager/grenmlconverter.py index 177235a..50fd349 100644 --- a/sdxdatamodel/topologymanager/grenmlconverter.py +++ b/sdxdatamodel/topologymanager/grenmlconverter.py @@ -42,13 +42,13 @@ def add_links(self,links): ports=link.ports end_nodes=[] for port in ports: - node=self.topology.get_node_by_port(port) + node=self.topology.get_node_by_port(port['id']) if node is not None: location = node.get_location() grenml_node = Node(node.id, node.name,node.short_name,longitude=location.longitude,latitude=location.latitude, address=location.address) end_nodes.append(grenml_node) else: - print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port) + print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port['id']) inter_domain_link = True if not inter_domain_link: self.grenml_manager.add_link(link.id, link.name,link.short_name, nodes=end_nodes) diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index 69f7f32..a36bf98 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -58,7 +58,7 @@ def add_topology(self, data): links = topology.get_links() for link in links: for port in link.ports: - self.port_list[port] = link + self.port_list[port['id']] = link else: ## update the version and timestamp to be the latest self.update_version(False) @@ -134,7 +134,7 @@ def inter_domain_check(self,topology): for link in links: link_dict[link.id] = link for port in link.ports: - interdomain_port_dict[port]=link + interdomain_port_dict[port['id']]=link #ToDo: raise an warning or exception if len(interdomain_port_dict)==0: @@ -171,14 +171,14 @@ def generate_graph(self): ports=link.ports end_nodes=[] for port in ports: - node=self.topology.get_node_by_port(port) + node=self.topology.get_node_by_port(port['id']) if node is None: - print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port) + print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port['id']) inter_domain_link = True break else: end_nodes.append(node) - print("graph node:"+node.id) + #print("graph node:"+node.id) if not inter_domain_link: G.add_edge(end_nodes[0].name,end_nodes[1].name) edge = G.edges[end_nodes[0].name,end_nodes[1].name] @@ -186,6 +186,7 @@ def generate_graph(self): edge['latency'] = link.latency edge['bandwidth'] = link.bandwidth edge['residual_bandwidth'] = link.residual_bandwidth + edge['weight'] = 1000.0*(1.0/link.residual_bandwidth) #edge['latency'] = link.latency edge['packet_loss'] = link.packet_loss edge['availability'] = link.availability diff --git a/tests/data/amlight.json b/tests/data/amlight.json index 5ebebfc..46127a4 100644 --- a/tests/data/amlight.json +++ b/tests/data/amlight.json @@ -7,9 +7,9 @@ "links": [ { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:amlight:B1-B2", - "latency": 146582.15146899645, + "latency": 5, "name": "amlight:B1-B2", "packet_loss": 59.621339166831824, "ports": [ @@ -37,13 +37,13 @@ } ], "short_name": "Miami-BocaRaton", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:amlight:A1-B1", - "latency": 146582.15146899645, + "latency": 5, "name": "amlight:A1-B1", "packet_loss": 59.621339166831824, "ports": [ @@ -71,13 +71,13 @@ } ], "short_name": "redclara-miami", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:amlight:A1-B2", - "latency": 146582.15146899645, + "latency": 5, "name": "amlight:A1-B2", "packet_loss": 59.621339166831824, "ports": [ @@ -105,13 +105,13 @@ } ], "short_name": "redclara-BocaRaton", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", - "latency": 146582.15146899645, + "latency": 10, "name": "nni:Miami-Sanpaolo", "packet_loss": 59.621339166831824, "nni": "True", @@ -140,13 +140,13 @@ } ], "short_name": "Miami-Sanpaolo", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", - "latency": 146582.15146899645, + "latency": 10, "name": "nni:BocaRaton-Fortaleza", "packet_loss": 59.621339166831824, "nni": "True", @@ -175,7 +175,7 @@ } ], "short_name": "BocaRaton-Fortaleza", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 } ], "nodes": [ diff --git a/tests/data/sax.json b/tests/data/sax.json index 1f78753..8b32915 100644 --- a/tests/data/sax.json +++ b/tests/data/sax.json @@ -7,9 +7,9 @@ "links": [ { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:sax:B1-B2", - "latency": 146582.15146899645, + "latency": 5, "name": "sax:B1-B2", "packet_loss": 59.621339166831824, "ports": [ @@ -37,13 +37,13 @@ } ], "short_name": "SaoPaulo-Fortaleza", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:sax:Panama-Fortaleza", - "latency": 146582.15146899645, + "latency": 5, "name": "sax:Panama-Fortaleza", "packet_loss": 59.621339166831824, "ports": [ @@ -71,13 +71,13 @@ } ], "short_name": "Panama-Fortaleza", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:sax:SanPaolo-Fortaleza", - "latency": 146582.15146899645, + "latency": 5, "name": "nni:SanPaolo-Fortaleza", "packet_loss": 59.621339166831824, "ports": [ @@ -105,13 +105,13 @@ } ], "short_name": "BocaRaton-Fortaleza", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:sax:A1-B1", - "latency": 146582.15146899645, + "latency": 5, "name": "sax:A1-B1", "packet_loss": 59.621339166831824, "ports": [ @@ -139,13 +139,13 @@ } ], "short_name": "redclara-SaoPaulo", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:sax:A1-B2", - "latency": 146582.15146899645, + "latency": 5, "name": "sax:A1-B2", "packet_loss": 59.621339166831824, "ports": [ @@ -173,13 +173,13 @@ } ], "short_name": "redclara-Fortaleza", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", - "latency": 146582.15146899645, + "latency": 10, "name": "nni:Miami-Sanpaolo", "packet_loss": 59.621339166831824, "ports": [ @@ -207,13 +207,13 @@ } ], "short_name": "Miami-Sanpaolo", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", - "latency": 146582.15146899645, + "latency": 10, "name": "nni:BocaRaton-Fortaleza", "packet_loss": 59.621339166831824, "nni": "True", @@ -242,13 +242,13 @@ } ], "short_name": "BocaRaton-Fortaleza", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", - "latency": 146582.15146899645, + "latency": 5, "name": "nni:Fortaleza-Sangano", "packet_loss": 59.621339166831824, "nni": "True", @@ -277,7 +277,7 @@ } ], "short_name": "Fortaleza-Sangano", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 } ], "nodes": [ diff --git a/tests/data/sdx.json b/tests/data/sdx.json new file mode 100644 index 0000000..79b4ad4 --- /dev/null +++ b/tests/data/sdx.json @@ -0,0 +1,1067 @@ +{ + "id": "urn:ogf:network:sdx", + "name": "AmLight-OXP", + "domain_service": { + "monitoring_capability": null, + "owner": "FIU", + "private_attributes": null, + "provisioning_system": null, + "provisioning_url": null, + "vendor": null + }, + "version": "0.0", + "time_stamp": "2000-01-23T04:56:07+00:00", + "nodes": [ + { + "id": "urn:sdx:node:amlight.net:B1", + "name": "amlight:Novi01", + "short_name": "B1", + "location": { + "address": "Miami", + "latitude": -80.37676058477908, + "longitude": 25.75633040531146 + }, + "ports": [ + { + "id": "urn:sdx:port:amlight:B1:1", + "name": "Novi01:1", + "short_name": null, + "node": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B1:2", + "name": "Novi01:2", + "short_name": null, + "node": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B1:3", + "name": "Novi01:3", + "short_name": null, + "node": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:sdx:node:amlight.net:B2", + "name": "amlight:Novi02", + "short_name": "B2", + "location": { + "address": "BocaRaton", + "latitude": -80.10225977485742, + "longitude": 26.381437356374075 + }, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:B2:1", + "name": "Novi02:1", + "short_name": null, + "node": "B2:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B2:2", + "name": "Novi02:2", + "short_name": null, + "node": "B2:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B2:3", + "name": "Novi02:3", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:sdx:node:amlight.net:A1", + "name": "amlight:Novi100", + "short_name": "A1", + "location": { + "address": "redclara", + "latitude": -81.66666016473143, + "longitude": 30.34943181039702 + }, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:A1:1", + "name": "Novi100:1", + "short_name": null, + "node": "A1:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:A1:2", + "name": "Novi100:2", + "short_name": null, + "node": "A1:2", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:B1", + "name": "sax:Novi01", + "short_name": "B1", + "location": { + "address": "SaoPaulo", + "latitude": -46.650271781410524, + "longitude": -23.5311561958366 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", + "short_name": null, + "node": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:2", + "name": "Novi01:2", + "short_name": null, + "node": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:3", + "name": "Novi01:3", + "short_name": null, + "node": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4", + "name": "Novi01:3", + "short_name": null, + "node": "B1:4", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:B2", + "name": "sax:Novi02", + "short_name": "B2", + "location": { + "address": "PanamaCity", + "latitude": -79.4947050137491, + "longitude": 8.993040465928525 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B2:1", + "name": "Novi02:1", + "short_name": null, + "node": "B2:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:2", + "name": "Novi02:2", + "short_name": null, + "node": "B2:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:3", + "name": "Novi02:3", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:4", + "name": "Novi02:4", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:B3", + "name": "sax:Novi03", + "short_name": "B3", + "location": { + "address": "Fortaleza", + "latitude": -38.52443289673026, + "longitude": -3.73163824920348 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:1", + "name": "Novi02:3", + "short_name": null, + "node": "B3:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B3:2", + "name": "Novi02:3", + "short_name": null, + "node": "B3:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B3:3", + "name": "Novi03:3", + "short_name": null, + "node": "B3:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:A1", + "name": "sax:Novi100", + "short_name": "A1", + "location": { + "address": "Santiago", + "latitude": -70.64634765264213, + "longitude": -33.4507049233331 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:1", + "name": "Novi100:1", + "short_name": null, + "node": "A1:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:A1:2", + "name": "Novi100:2", + "short_name": null, + "node": "A1:2", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:B1", + "name": "zaoxi:Novi01", + "short_name": "B1", + "location": { + "address": "Sangano", + "latitude": 13.216709879405311, + "longitude": -9.533459658700743 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", + "name": "Novi01:1", + "short_name": null, + "node": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", + "short_name": null, + "node": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", + "short_name": null, + "node": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:B2", + "name": "zaoxi:Novi02", + "short_name": "B2", + "location": { + "address": "CapeTown", + "latitude": -38.52443289673026, + "longitude": -3.73163824920348 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:1", + "name": "Novi02:1", + "short_name": null, + "node": "B2:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "name": "Novi02:2", + "short_name": null, + "node": "B2:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "name": "Novi02:3", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:A1", + "name": "zaoxi:Novi100", + "short_name": "A1", + "location": { + "address": "Karoo", + "latitude": 22.541224555821298, + "longitude": -32.3632301851245 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "name": "Novi100:1", + "short_name": null, + "node": "A1:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "name": "Novi100:2", + "short_name": null, + "node": "A1:2", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + } + ], + "links": [ + { + "id": "urn:ogf:network:sdx:link:amlight:B1-B2", + "name": "amlight:B1-B2", + "short_name": "Miami-BocaRaton", + "ports": [ + { + "id": "urn:sdx:port:amlight.net:B1:2", + "name": "Novi01:2", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:sdx:port:amlight.net:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:amlight:A1-B1", + "name": "amlight:A1-B1", + "short_name": "redclara-miami", + "ports": [ + { + "id": "urn:sdx:port:amlight.net:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:sdx:node:amlight.net:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:sdx:port:amlight.net:B1:3", + "name": "Novi01:3", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:amlight:A1-B2", + "name": "amlight:A1-B2", + "short_name": "redclara-BocaRaton", + "ports": [ + { + "id": "urn:sdx:port:amlight.net:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:sdx:node:amlight.net:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:sdx:port:amlight.net:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:B1-B2", + "name": "sax:B1-B2", + "short_name": "SaoPaulo-Fortaleza", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:Panama-Fortaleza", + "name": "sax:Panama-Fortaleza", + "short_name": "Panama-Fortaleza", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:4", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:4", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:SanPaolo-Fortaleza", + "name": "nni:SanPaolo-Fortaleza", + "short_name": "BocaRaton-Fortaleza", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi03:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:3", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:4", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:A1-B1", + "name": "sax:A1-B1", + "short_name": "redclara-SaoPaulo", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:sax:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:A1-B2", + "name": "sax:A1-B2", + "short_name": "redclara-Fortaleza", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:sax:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", + "name": "nni:Miami-Sanpaolo", + "short_name": "Miami-Sanpaolo", + "ports": [ + { + "id": "urn:sdx:port:amlight:B1:1", + "name": "Novi01:1", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:port:sax:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 10, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", + "name": "nni:BocaRaton-Fortaleza", + "short_name": "BocaRaton-Fortaleza", + "ports": [ + { + "id": "urn:sdx:port:amlight.net:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:1", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 10, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", + "name": "zaoxi:B1-B2", + "short_name": "Sangano-Capetown", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", + "name": "zaoxi:A1-B1", + "short_name": "Karoo-Sangano", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", + "name": "zaoxi:A1-B2", + "short_name": "Karoo-Capetown", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", + "name": "nni:Fortaleza-Sangano", + "short_name": "Fortaleza-Sangano", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 10, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + } + ], + "private_attributes": null +} \ No newline at end of file diff --git a/tests/data/test_request.json b/tests/data/test_request.json new file mode 100644 index 0000000..f5d4074 --- /dev/null +++ b/tests/data/test_request.json @@ -0,0 +1,18 @@ +{ + "id": "id", + "name": "AMLight", + "start_time": "2000-01-23T04:56:07.000Z", + "end_time": "2000-01-23T04:56:07.000Z", + "bandwidth_required": 100, + "latency_required": 20, + "egress_port": + { + "id": "urn:sdx:port:amlight.net:A1:1", + "name": "Novi100:1" + }, + "ingress_port": + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "name": "Novi100:2" + } +} \ No newline at end of file diff --git a/tests/data/zaoxi.json b/tests/data/zaoxi.json index 8b58c05..8526074 100644 --- a/tests/data/zaoxi.json +++ b/tests/data/zaoxi.json @@ -7,9 +7,9 @@ "links": [ { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", - "latency": 146582.15146899645, + "latency": 5, "name": "zaoxi:B1-B2", "packet_loss": 59.621339166831824, "ports": [ @@ -32,13 +32,13 @@ } ], "short_name": "Sangano-Capetown", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", - "latency": 146582.15146899645, + "latency": 5, "name": "zaoxi:A1-B1", "packet_loss": 59.621339166831824, "ports": [ @@ -66,13 +66,13 @@ } ], "short_name": "Karoo-Sangano", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", - "latency": 146582.15146899645, + "latency": 5, "name": "zaoxi:A1-B2", "packet_loss": 59.621339166831824, "ports": [ @@ -100,13 +100,13 @@ } ], "short_name": "Karoo-Capetown", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 }, { "availability": 56.37376656633328, - "residual_bandwidth": 602746.015561422, + "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", - "latency": 146582.15146899645, + "latency": 10, "name": "nni:Fortaleza-Sangano", "packet_loss": 59.621339166831824, "nni": "True", @@ -135,7 +135,7 @@ } ], "short_name": "Fortaleza-Sangano", - "bandwidth": 80083.7389632821 + "bandwidth": 100000 } ], "nodes": [ diff --git a/tests/test_connection_handler.py b/tests/test_connection_handler.py index 8cfe98c..d79c313 100644 --- a/tests/test_connection_handler.py +++ b/tests/test_connection_handler.py @@ -5,7 +5,8 @@ from sdxdatamodel.parsing.connectionhandler import ConnectionHandler from sdxdatamodel.parsing.exceptions import DataModelException -CONNECTION_P2P = './tests/data/p2p.json' +#CONNECTION_P2P = './tests/data/p2p.json' +CONNECTION_P2P = './tests/data/test_connection.json' class TestConnectionHandler(unittest.TestCase): diff --git a/tests/test_connection_validator.py b/tests/test_connection_validator.py index 500fa08..36131e8 100644 --- a/tests/test_connection_validator.py +++ b/tests/test_connection_validator.py @@ -7,7 +7,8 @@ from sdxdatamodel.parsing.exceptions import DataModelException from sdxdatamodel.models.connection import Connection -CONNECTION_P2P = './tests/data/p2p.json' +#CONNECTION_P2P = './tests/data/p2p.json' +CONNECTION_P2P = './tests/data/test_connection.json' class TestConnectionValidator(unittest.TestCase): diff --git a/tests/test_te_manager.py b/tests/test_te_manager.py new file mode 100644 index 0000000..c2fc9d8 --- /dev/null +++ b/tests/test_te_manager.py @@ -0,0 +1,42 @@ +import unittest +import json +from networkx import MultiGraph, Graph +import networkx as nx + +from sdxdatamodel import parsing +from sdxdatamodel import topologymanager + +from sdxdatamodel import validation +from sdxdatamodel.validation.topologyvalidator import TopologyValidator +from sdxdatamodel.parsing.topologyhandler import TopologyHandler +from sdxdatamodel.topologymanager.manager import TopologyManager +from sdxdatamodel.topologymanager.temanager import TEManager +from sdxdatamodel.parsing.exceptions import DataModelException + +TOPOLOGY = "./tests/data/sdx.json" +CONNECTION = "./tests/data/test_request.json" + +class TestTopologyManager(unittest.TestCase): + + def setUp(self): + with open(TOPOLOGY, 'r', encoding='utf-8') as t: + topology_data = json.load(t) + with open(CONNECTION, 'r', encoding='utf-8') as c: + connection_data = json.load(c) + + self.temanager = TEManager(topology_data, connection_data) + + def tearDown(self): + pass + + def testGenerateSolverInput(self): + print("generate networkx graph of the topology") + graph=self.temanager.graph + + print(graph.nodes[0]) + print(graph.edges) + + print("Test Convert Connection To Topology") + connection=self.temanager.generate_connection_te() + print(connection) + diff --git a/tests/test_topology_manager.py b/tests/test_topology_manager.py index 2be81ed..12f070a 100644 --- a/tests/test_topology_manager.py +++ b/tests/test_topology_manager.py @@ -19,7 +19,8 @@ TOPOLOGY_SAX = './tests/data/sax.json' TOPOLOGY_ZAOXI = './tests/data/zaoxi.json' -TOPOLOGY_png = "./tests/data/amlight.png" +TOPOLOGY_png = "./tests/data/sdx.png" +TOPOLOGY = "./tests/data/sdx.json" topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_SAX, TOPOLOGY_ZAOXI] topology_file_list_2 = [TOPOLOGY_SAX, TOPOLOGY_ZAOXI] @@ -40,7 +41,9 @@ def testMergeTopology(self): data = json.load(data_file) print("Adding Topology:" + topology_file) self.manager.add_topology(data) - + with open(TOPOLOGY, 'w') as t_file: + json.dump(self.manager.topology.to_dict(), t_file, indent=4) + except DataModelException as e: print(e) return False From ab04eb7a1b74b2ad482141c93e182fdf82d6e59a Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 30 May 2022 21:55:21 -0400 Subject: [PATCH 38/67] factor out a temanager for PCE --- sdxdatamodel/topologymanager/temanager.py | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 sdxdatamodel/topologymanager/temanager.py diff --git a/sdxdatamodel/topologymanager/temanager.py b/sdxdatamodel/topologymanager/temanager.py new file mode 100644 index 0000000..5100bfe --- /dev/null +++ b/sdxdatamodel/topologymanager/temanager.py @@ -0,0 +1,57 @@ + +import json +import copy + +import networkx as nx + +from sdxdatamodel.models.topology import Topology, SDX_TOPOLOGY_ID_prefix,TOPOLOGY_INITIAL_VERSION +from sdxdatamodel.models.link import Link +from sdxdatamodel.models.port import Port +from sdxdatamodel.models.connection import Connection + +from sdxdatamodel.parsing.connectionhandler import ConnectionHandler +from sdxdatamodel.topologymanager.manager import TopologyManager +from sdxdatamodel.parsing.topologyhandler import TopologyHandler +from sdxdatamodel.parsing.exceptions import DataModelException + +from .manager import TopologyManager + +class TEManager(): + + """" + TE Manager for connection - topology operations: (1) generate inputs to the PCE solver; (2) converter the solver output. + """ + + + def __init__(self, topology_data, connection_data): + super().__init__() + + self.manager = TopologyManager() + self.connection_handler = ConnectionHandler() + + self.manager.topology = self.manager.get_handler().import_topology_data(topology_data) + self.connection = self.connection_handler.import_connection_data(connection_data) + + self.graph = self.generate_graph_te() + + def generate_connection_te(self): + ingress_port = self.connection.ingress_port + ingress_node = self.manager.topology.get_node_by_port(ingress_port.id) + egress_port = self.connection.egress_port + egress_node = self.manager.topology.get_node_by_port(egress_port.id) + + i_node = [x for x,y in self.graph.nodes(data=True) if y['name']==ingress_node.name] + e_node = [x for x,y in self.graph.nodes(data=True) if y['name']==egress_node.name] + + bandwidth_required=self.connection.bandwidth + latency_required=self.connection.latency + requests=[] + request=[i_node[0],e_node[0],bandwidth_required,latency_required] + requests.append(request) + + return requests + + def generate_graph_te(self): + graph = self.manager.generate_graph() + graph = nx.convert_node_labels_to_integers(graph,label_attribute='name') + return graph From f91e542e6a219386d112acee79131e0746235b56 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Tue, 31 May 2022 02:35:36 -0400 Subject: [PATCH 39/67] fix a typo in json; path domain breakdown --- sdxdatamodel/models/topology.py | 16 +++++- sdxdatamodel/topologymanager/manager.py | 12 ++++- sdxdatamodel/topologymanager/temanager.py | 66 +++++++++++++++++++++-- tests/data/sax.json | 10 ++-- 4 files changed, 93 insertions(+), 11 deletions(-) diff --git a/sdxdatamodel/models/topology.py b/sdxdatamodel/models/topology.py index 193171f..0a152e1 100644 --- a/sdxdatamodel/models/topology.py +++ b/sdxdatamodel/models/topology.py @@ -244,7 +244,21 @@ def get_node_by_port(self,aPort): return node return None - + def get_port_by_link(self,n1_id,n2_id): + for x in self.links: + print("--------") + print(x.ports[0]['node']) + print(x.ports[1]['node']) + if x.ports[0]['node']==n1_id and x.ports[1]['node']==n2_id: + return n1_id,x.ports[0],n2_id,x.ports[1] + if x.ports[0]['node']==n2_id and x.ports[1]['node']==n1_id: + return n1_id,x.ports[1],n2_id,x.ports[0] + + def has_node_by_id(self,id): + for node in self.nodes: + if id == node.id: + return True + return False @property def links(self): diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index a36bf98..22b6107 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -77,6 +77,14 @@ def add_topology(self, data): self.update_version(False) + def get_domain_name(self,node_id): + domain_id=None + for id, topology in self.topology_list.items(): + if topology.has_node_by_id(node_id): + domain_id = id + break + return domain_id + def generate_id(self): self.topology.set_id(SDX_TOPOLOGY_ID_prefix) self.topology.set_version(TOPOLOGY_INITIAL_VERSION) @@ -180,8 +188,8 @@ def generate_graph(self): end_nodes.append(node) #print("graph node:"+node.id) if not inter_domain_link: - G.add_edge(end_nodes[0].name,end_nodes[1].name) - edge = G.edges[end_nodes[0].name,end_nodes[1].name] + G.add_edge(end_nodes[0].id,end_nodes[1].id) + edge = G.edges[end_nodes[0].id,end_nodes[1].id] edge['id'] = link.id edge['latency'] = link.latency edge['bandwidth'] = link.bandwidth diff --git a/sdxdatamodel/topologymanager/temanager.py b/sdxdatamodel/topologymanager/temanager.py index 5100bfe..b4e0f30 100644 --- a/sdxdatamodel/topologymanager/temanager.py +++ b/sdxdatamodel/topologymanager/temanager.py @@ -40,8 +40,8 @@ def generate_connection_te(self): egress_port = self.connection.egress_port egress_node = self.manager.topology.get_node_by_port(egress_port.id) - i_node = [x for x,y in self.graph.nodes(data=True) if y['name']==ingress_node.name] - e_node = [x for x,y in self.graph.nodes(data=True) if y['name']==egress_node.name] + i_node = [x for x,y in self.graph.nodes(data=True) if y['id']==ingress_node.id] + e_node = [x for x,y in self.graph.nodes(data=True) if y['id']==egress_node.id] bandwidth_required=self.connection.bandwidth latency_required=self.connection.latency @@ -53,5 +53,65 @@ def generate_connection_te(self): def generate_graph_te(self): graph = self.manager.generate_graph() - graph = nx.convert_node_labels_to_integers(graph,label_attribute='name') + graph = nx.convert_node_labels_to_integers(graph,label_attribute='id') return graph + + def generate_connection_breakdown(self, connection): + breakdown = {} + paths = connection[0] #p2p for now + cost=connection[1] + i_port = None + e_port = None + for i,j in paths.items(): + current_link_set=[] + for count,link in enumerate(j): + node_1 = self.graph.nodes[link[0]] + node_2 = self.graph.nodes[link[1]] + domain_1 = self.manager.get_domain_name(node_1['id']) + domain_2 = self.manager.get_domain_name(node_2['id']) + current_link_set.append(link) + if domain_1 == domain_2: + current_domain = domain_1 + if count==len(j)-1: + breakdown[current_domain]=current_link_set.copy() + else: + breakdown[current_domain]=current_link_set.copy() + current_domain=None + current_link_set=[] + + #now starting with the ingress_port + first = True + i=0 + domain_breakdown={} + for domain, links in breakdown.items(): + print(i) + segment={} + if first: + first=False + last_link=links[-1] + n1=self.graph.nodes[last_link[0]]['id'] + n2=self.graph.nodes[last_link[1]]['id'] + n1,p1,n2,p2=self.manager.topology.get_port_by_link(n1,n2) + i_port=self.connection.ingress_port.to_dict() + e_port=p1 + next_i = p2 + elif i==len(breakdown)-1: + i_port=next_i + e_port=self.connection.egress_port.to_dict() + else: + last_link=links[-1] + n1=self.graph.nodes[last_link[0]]['id'] + n2=self.graph.nodes[last_link[1]]['id'] + print(last_link[0]) + print(n1) + print(n2) + n1,p1,n2,p2=self.manager.topology.get_port_by_link(n1,n2) + i_port=next_i + e_port=p1 + next_i=p2 + segment['ingress_port'] = i_port + segment['egress_port'] = e_port + domain_breakdown[domain]=segment.copy() + i=i+1 + + return domain_breakdown diff --git a/tests/data/sax.json b/tests/data/sax.json index 8b32915..b874522 100644 --- a/tests/data/sax.json +++ b/tests/data/sax.json @@ -16,7 +16,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:2", "name": "Novi01:2", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:2", "label_range": [ "100-200", @@ -95,7 +95,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:4", "name": "Novi01:3", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:4", "label_range": [ "100-200", @@ -129,7 +129,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:3", "name": "Novi01:3", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:3", "label_range": [ "100-200", @@ -197,7 +197,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:1", "name": "Novi01:1", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:1", "label_range": [ "100-200", @@ -267,7 +267,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:1", "name": "Novi01:1", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:1", "label_range": [ "100-200", From b5ef9b6a86a4bd3d9ed553a6051b06fbdd257422 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Tue, 31 May 2022 08:37:10 -0400 Subject: [PATCH 40/67] remove print --- sdxdatamodel/topologymanager/temanager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sdxdatamodel/topologymanager/temanager.py b/sdxdatamodel/topologymanager/temanager.py index b4e0f30..8b694c7 100644 --- a/sdxdatamodel/topologymanager/temanager.py +++ b/sdxdatamodel/topologymanager/temanager.py @@ -84,7 +84,6 @@ def generate_connection_breakdown(self, connection): i=0 domain_breakdown={} for domain, links in breakdown.items(): - print(i) segment={} if first: first=False @@ -102,9 +101,6 @@ def generate_connection_breakdown(self, connection): last_link=links[-1] n1=self.graph.nodes[last_link[0]]['id'] n2=self.graph.nodes[last_link[1]]['id'] - print(last_link[0]) - print(n1) - print(n2) n1,p1,n2,p2=self.manager.topology.get_port_by_link(n1,n2) i_port=next_i e_port=p1 From 05236913df6cd0074cadbaf033957d0e3ffa7242 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sat, 4 Jun 2022 22:32:05 -0400 Subject: [PATCH 41/67] log --- sdxdatamodel/models/topology.py | 6 +++--- sdxdatamodel/topologymanager/temanager.py | 7 +++++-- tests/data/sdx.json | 8 ++++---- tests/test_topology_handler.py | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/sdxdatamodel/models/topology.py b/sdxdatamodel/models/topology.py index 0a152e1..7232c11 100644 --- a/sdxdatamodel/models/topology.py +++ b/sdxdatamodel/models/topology.py @@ -246,9 +246,9 @@ def get_node_by_port(self,aPort): return None def get_port_by_link(self,n1_id,n2_id): for x in self.links: - print("--------") - print(x.ports[0]['node']) - print(x.ports[1]['node']) + #print("--------") + #print(x.ports[0]['node']) + #print(x.ports[1]['node']) if x.ports[0]['node']==n1_id and x.ports[1]['node']==n2_id: return n1_id,x.ports[0],n2_id,x.ports[1] if x.ports[0]['node']==n2_id and x.ports[1]['node']==n1_id: diff --git a/sdxdatamodel/topologymanager/temanager.py b/sdxdatamodel/topologymanager/temanager.py index 8b694c7..d0a1829 100644 --- a/sdxdatamodel/topologymanager/temanager.py +++ b/sdxdatamodel/topologymanager/temanager.py @@ -62,6 +62,7 @@ def generate_connection_breakdown(self, connection): cost=connection[1] i_port = None e_port = None + print("domain breakdown:") for i,j in paths.items(): current_link_set=[] for count,link in enumerate(j): @@ -70,6 +71,8 @@ def generate_connection_breakdown(self, connection): domain_1 = self.manager.get_domain_name(node_1['id']) domain_2 = self.manager.get_domain_name(node_2['id']) current_link_set.append(link) + print("domain_1:"+domain_1) + print("domain_2:"+domain_2) if domain_1 == domain_2: current_domain = domain_1 if count==len(j)-1: @@ -77,8 +80,8 @@ def generate_connection_breakdown(self, connection): else: breakdown[current_domain]=current_link_set.copy() current_domain=None - current_link_set=[] - + current_link_set=[] + print(breakdown) #now starting with the ingress_port first = True i=0 diff --git a/tests/data/sdx.json b/tests/data/sdx.json index 79b4ad4..86f34d6 100644 --- a/tests/data/sdx.json +++ b/tests/data/sdx.json @@ -646,7 +646,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:2", "name": "Novi01:2", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:2", "label_range": [ "100-200", @@ -735,7 +735,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:4", "name": "Novi01:3", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:4", "label_range": [ "100-200", @@ -774,7 +774,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:3", "name": "Novi01:3", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:3", "label_range": [ "100-200", @@ -852,7 +852,7 @@ { "id": "urn:ogf:network:sdx:port:sax:B1:1", "name": "Novi01:1", - "node": "urn:ogf:network:sdx:port:sax:B1", + "node": "urn:ogf:network:sdx:node:sax:B1", "short_name": "B1:1", "label_range": [ "100-200", diff --git a/tests/test_topology_handler.py b/tests/test_topology_handler.py index dda7e94..32bb90f 100644 --- a/tests/test_topology_handler.py +++ b/tests/test_topology_handler.py @@ -8,7 +8,7 @@ class TestTopologyHandler(unittest.TestCase): def setUp(self): - self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) # noqa: E501 + self.handler = TopologyHandler(TOPOLOGY_AMLIGHT) self.handler.import_topology() def tearDown(self): pass From 069a094cd988bbe4ff01a7ebaa4da2207695b37c Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sat, 4 Jun 2022 22:40:10 -0400 Subject: [PATCH 42/67] adding a debug print --- sdxdatamodel/topologymanager/manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index 22b6107..27f2033 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -79,7 +79,10 @@ def add_topology(self, data): def get_domain_name(self,node_id): domain_id=None + print("len of topology_list:"+str(len(self.topology_list))) for id, topology in self.topology_list.items(): + print(id) + print(topology.id) if topology.has_node_by_id(node_id): domain_id = id break From 2b487551f114f1331ff7286944a5da309443e4c8 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Sat, 4 Jun 2022 23:23:41 -0400 Subject: [PATCH 43/67] typo --- tests/data/sax.json | 4 +- tests/data/sdx.json | 452 ++++++++++++++++++++++---------------------- 2 files changed, 228 insertions(+), 228 deletions(-) diff --git a/tests/data/sax.json b/tests/data/sax.json index b874522..891877d 100644 --- a/tests/data/sax.json +++ b/tests/data/sax.json @@ -265,9 +265,9 @@ "status": "up" }, { - "id": "urn:ogf:network:sdx:port:sax:B1:1", + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", "name": "Novi01:1", - "node": "urn:ogf:network:sdx:node:sax:B1", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", "short_name": "B1:1", "label_range": [ "100-200", diff --git a/tests/data/sdx.json b/tests/data/sdx.json index 86f34d6..07d5fae 100644 --- a/tests/data/sdx.json +++ b/tests/data/sdx.json @@ -156,17 +156,17 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:B1", - "name": "sax:Novi01", + "id": "urn:ogf:network:sdx:node:zaoxi:B1", + "name": "zaoxi:Novi01", "short_name": "B1", "location": { - "address": "SaoPaulo", - "latitude": -46.650271781410524, - "longitude": -23.5311561958366 + "address": "Sangano", + "latitude": 13.216709879405311, + "longitude": -9.533459658700743 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:B1:1", + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", "name": "Novi01:1", "short_name": null, "node": "B1:1", @@ -179,7 +179,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B1:2", + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", "name": "Novi01:2", "short_name": null, "node": "B1:2", @@ -192,7 +192,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B1:3", + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", "name": "Novi01:3", "short_name": null, "node": "B1:3", @@ -203,35 +203,22 @@ "status": null, "state": null, "private_attributes": null - }, - { - "id": "urn:ogf:network:sdx:port:sax:B1:4", - "name": "Novi01:3", - "short_name": null, - "node": "B1:4", - "label_range": [ - "100-200", - "10001" - ], - "status": null, - "state": null, - "private_attributes": null } ], "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:B2", - "name": "sax:Novi02", + "id": "urn:ogf:network:sdx:node:zaoxi:B2", + "name": "zaoxi:Novi02", "short_name": "B2", "location": { - "address": "PanamaCity", - "latitude": -79.4947050137491, - "longitude": 8.993040465928525 + "address": "CapeTown", + "latitude": -38.52443289673026, + "longitude": -3.73163824920348 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:B2:1", + "id": "urn:ogf:network:sdx:port:zaoxi:B2:1", "name": "Novi02:1", "short_name": null, "node": "B2:1", @@ -244,7 +231,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B2:2", + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", "name": "Novi02:2", "short_name": null, "node": "B2:2", @@ -257,7 +244,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B2:3", + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", "name": "Novi02:3", "short_name": null, "node": "B2:3", @@ -268,15 +255,41 @@ "status": null, "state": null, "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:A1", + "name": "zaoxi:Novi100", + "short_name": "A1", + "location": { + "address": "Karoo", + "latitude": 22.541224555821298, + "longitude": -32.3632301851245 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "name": "Novi100:1", + "short_name": null, + "node": "A1:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B2:4", - "name": "Novi02:4", + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "name": "Novi100:2", "short_name": null, - "node": "B2:3", + "node": "A1:2", "label_range": [ "100-200", - "10001" + "1000" ], "status": null, "state": null, @@ -286,33 +299,33 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:B3", - "name": "sax:Novi03", - "short_name": "B3", + "id": "urn:ogf:network:sdx:node:sax:B1", + "name": "sax:Novi01", + "short_name": "B1", "location": { - "address": "Fortaleza", - "latitude": -38.52443289673026, - "longitude": -3.73163824920348 + "address": "SaoPaulo", + "latitude": -46.650271781410524, + "longitude": -23.5311561958366 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:B3:1", - "name": "Novi02:3", + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", "short_name": null, - "node": "B3:1", + "node": "B1:1", "label_range": [ "100-200", - "1000" + "10001" ], "status": null, "state": null, "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B3:2", - "name": "Novi02:3", + "id": "urn:ogf:network:sdx:port:sax:B1:2", + "name": "Novi01:2", "short_name": null, - "node": "B3:2", + "node": "B1:2", "label_range": [ "100-200", "10001" @@ -322,10 +335,23 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B3:3", - "name": "Novi03:3", + "id": "urn:ogf:network:sdx:port:sax:B1:3", + "name": "Novi01:3", "short_name": null, - "node": "B3:3", + "node": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4", + "name": "Novi01:3", + "short_name": null, + "node": "B1:4", "label_range": [ "100-200", "10001" @@ -338,20 +364,20 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:A1", - "name": "sax:Novi100", - "short_name": "A1", + "id": "urn:ogf:network:sdx:node:sax:B2", + "name": "sax:Novi02", + "short_name": "B2", "location": { - "address": "Santiago", - "latitude": -70.64634765264213, - "longitude": -33.4507049233331 + "address": "PanamaCity", + "latitude": -79.4947050137491, + "longitude": 8.993040465928525 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:A1:1", - "name": "Novi100:1", + "id": "urn:ogf:network:sdx:port:sax:B2:1", + "name": "Novi02:1", "short_name": null, - "node": "A1:1", + "node": "B2:1", "label_range": [ "100-200", "1000" @@ -361,36 +387,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:A1:2", - "name": "Novi100:2", - "short_name": null, - "node": "A1:2", - "label_range": [ - "100-200", - "1000" - ], - "status": null, - "state": null, - "private_attributes": null - } - ], - "private_attributes": null - }, - { - "id": "urn:ogf:network:sdx:node:zaoxi:B1", - "name": "zaoxi:Novi01", - "short_name": "B1", - "location": { - "address": "Sangano", - "latitude": 13.216709879405311, - "longitude": -9.533459658700743 - }, - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", - "name": "Novi01:1", + "id": "urn:ogf:network:sdx:port:sax:B2:2", + "name": "Novi02:2", "short_name": null, - "node": "B1:1", + "node": "B2:2", "label_range": [ "100-200", "10001" @@ -400,10 +400,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", - "name": "Novi01:2", + "id": "urn:ogf:network:sdx:port:sax:B2:3", + "name": "Novi02:3", "short_name": null, - "node": "B1:2", + "node": "B2:3", "label_range": [ "100-200", "10001" @@ -413,10 +413,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", - "name": "Novi01:3", + "id": "urn:ogf:network:sdx:port:sax:B2:4", + "name": "Novi02:4", "short_name": null, - "node": "B1:3", + "node": "B2:3", "label_range": [ "100-200", "10001" @@ -429,20 +429,20 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:zaoxi:B2", - "name": "zaoxi:Novi02", - "short_name": "B2", + "id": "urn:ogf:network:sdx:node:sax:B3", + "name": "sax:Novi03", + "short_name": "B3", "location": { - "address": "CapeTown", + "address": "Fortaleza", "latitude": -38.52443289673026, "longitude": -3.73163824920348 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:1", - "name": "Novi02:1", + "id": "urn:ogf:network:sdx:port:sax:B3:1", + "name": "Novi02:3", "short_name": null, - "node": "B2:1", + "node": "B3:1", "label_range": [ "100-200", "1000" @@ -452,10 +452,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", - "name": "Novi02:2", + "id": "urn:ogf:network:sdx:port:sax:B3:2", + "name": "Novi02:3", "short_name": null, - "node": "B2:2", + "node": "B3:2", "label_range": [ "100-200", "10001" @@ -465,10 +465,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", - "name": "Novi02:3", + "id": "urn:ogf:network:sdx:port:sax:B3:3", + "name": "Novi03:3", "short_name": null, - "node": "B2:3", + "node": "B3:3", "label_range": [ "100-200", "10001" @@ -481,17 +481,17 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:zaoxi:A1", - "name": "zaoxi:Novi100", + "id": "urn:ogf:network:sdx:node:sax:A1", + "name": "sax:Novi100", "short_name": "A1", "location": { - "address": "Karoo", - "latitude": 22.541224555821298, - "longitude": -32.3632301851245 + "address": "Santiago", + "latitude": -70.64634765264213, + "longitude": -33.4507049233331 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "id": "urn:ogf:network:sdx:port:sax:A1:1", "name": "Novi100:1", "short_name": null, "node": "A1:1", @@ -504,7 +504,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "id": "urn:ogf:network:sdx:port:sax:A1:2", "name": "Novi100:2", "short_name": null, "node": "A1:2", @@ -638,6 +638,118 @@ "time_stamp": null, "measurement_period": null }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", + "name": "zaoxi:B1-B2", + "short_name": "Sangano-Capetown", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", + "name": "zaoxi:A1-B1", + "short_name": "Karoo-Sangano", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", + "name": "zaoxi:A1-B2", + "short_name": "Karoo-Capetown", + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, { "id": "urn:ogf:network:sdx:link:sax:B1-B2", "name": "sax:B1-B2", @@ -911,118 +1023,6 @@ "time_stamp": null, "measurement_period": null }, - { - "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", - "name": "zaoxi:B1-B2", - "short_name": "Sangano-Capetown", - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", - "name": "Novi01:2", - "node": "urn:ogf:network:sdx:node:zaoxi:B1", - "status": "up" - }, - { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", - "label_range": [ - "100-200", - "10001" - ], - "name": "Novi02:2", - "node": "urn:ogf:network:sdx:node:zaoxi:B2", - "short_name": "B2:2", - "status": "up" - } - ], - "bandwidth": 100000, - "residual_bandwidth": 100000, - "latency": 5, - "packet_loss": 59.621339166831824, - "availability": 56.37376656633328, - "status": null, - "state": null, - "private_attributes": null, - "time_stamp": null, - "measurement_period": null - }, - { - "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", - "name": "zaoxi:A1-B1", - "short_name": "Karoo-Sangano", - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", - "label_range": [ - "100-200", - "1000" - ], - "name": "Novi100:1", - "node": "urn:ogf:network:sdx:node:zaoxi:A1", - "short_name": "A1:1", - "status": "up" - }, - { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", - "name": "Novi01:3", - "node": "urn:ogf:network:sdx:node:zaoxi:B1", - "short_name": "B1:3", - "label_range": [ - "100-200", - "10001" - ], - "status": "up" - } - ], - "bandwidth": 100000, - "residual_bandwidth": 100000, - "latency": 5, - "packet_loss": 59.621339166831824, - "availability": 56.37376656633328, - "status": null, - "state": null, - "private_attributes": null, - "time_stamp": null, - "measurement_period": null - }, - { - "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", - "name": "zaoxi:A1-B2", - "short_name": "Karoo-Capetown", - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", - "label_range": [ - "100-200", - "1000" - ], - "name": "Novi100:2", - "node": "urn:ogf:network:sdx:node:zaoxi:A1", - "short_name": "A1:2", - "status": "up" - }, - { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", - "label_range": [ - "100-200", - "10001" - ], - "name": "Novi02:3", - "node": "urn:ogf:network:sdx:node:zaoxi:B2", - "short_name": "B2:3", - "status": "up" - } - ], - "bandwidth": 100000, - "residual_bandwidth": 100000, - "latency": 5, - "packet_loss": 59.621339166831824, - "availability": 56.37376656633328, - "status": null, - "state": null, - "private_attributes": null, - "time_stamp": null, - "measurement_period": null - }, { "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", "name": "nni:Fortaleza-Sangano", @@ -1053,7 +1053,7 @@ ], "bandwidth": 100000, "residual_bandwidth": 100000, - "latency": 10, + "latency": 5, "packet_loss": 59.621339166831824, "availability": 56.37376656633328, "status": null, From 3cd4e2d067fb720a8d9c57c291c9297af7e8f820 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 6 Jun 2022 11:28:50 -0400 Subject: [PATCH 44/67] some code clean up, and simpler version update, timestamp using current time. --- sdxdatamodel/models/topology.py | 4 ++- sdxdatamodel/parsing/exceptions.py | 17 ++++++++++-- sdxdatamodel/parsing/topologyhandler.py | 9 ++++--- sdxdatamodel/topologymanager/manager.py | 33 +++++++++++++---------- sdxdatamodel/topologymanager/temanager.py | 19 +++++++++++-- setup.cfg | 2 +- tests/data/amlight.json | 2 +- tests/data/sax.json | 2 +- tests/data/sdx.json | 4 +-- tests/data/zaoxi.json | 2 +- tests/test_topology_manager.py | 2 +- 11 files changed, 67 insertions(+), 29 deletions(-) diff --git a/sdxdatamodel/models/topology.py b/sdxdatamodel/models/topology.py index 7232c11..a8dd9d2 100644 --- a/sdxdatamodel/models/topology.py +++ b/sdxdatamodel/models/topology.py @@ -13,6 +13,7 @@ import pprint import re # noqa: F401 import six +import datetime from sdxdatamodel.parsing.servicehandler import ServiceHandler from sdxdatamodel.parsing.nodehandler import NodeHandler @@ -153,7 +154,8 @@ def version(self): """ return self._version - def set_version(self, version): + @version.setter + def version(self, version): """Sets the version of this Topology. diff --git a/sdxdatamodel/parsing/exceptions.py b/sdxdatamodel/parsing/exceptions.py index 9914ffe..f4146bb 100644 --- a/sdxdatamodel/parsing/exceptions.py +++ b/sdxdatamodel/parsing/exceptions.py @@ -1,12 +1,12 @@ class DataModelException(Exception): """ - Base exception for GRENML parsing + Base exception for topology data model functions """ pass class MissingAttributeException(DataModelException): """ - A required attribute could not be found when parsing an element + A required attribute could not be found when parsing a model element in JSON """ def __init__(self, attribute_name, expected_attribute): @@ -19,5 +19,18 @@ def __str__(self): self.attribute_name, ) +class GraphNotConnectedException(DataModelException): + """ + The topology is not connected + """ + + def __init__(self,graph, connectivity): + self.graph = graph + self.connectivity = connectivity + def __str__(self): + return ("Graph <{}> is Not {} Connected ").format( + self.graph, + self.connectivity, + ) diff --git a/sdxdatamodel/parsing/topologyhandler.py b/sdxdatamodel/parsing/topologyhandler.py index 86cda9e..451fd6a 100644 --- a/sdxdatamodel/parsing/topologyhandler.py +++ b/sdxdatamodel/parsing/topologyhandler.py @@ -19,9 +19,12 @@ def topology_file_name(self,topology_filename=None): self.topology_file = topology_filename def import_topology(self): - with open(self.topology_file, 'r', encoding='utf-8') as data_file: - data = json.load(data_file) - self.import_topology_data(data) + try: + with open(self.topology_file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.import_topology_data(data) + except (IOError, FileNotFoundError) as e: + print(e) def import_topology_data(self, data): try: diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index 27f2033..e6a1600 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -3,6 +3,7 @@ import copy import networkx as nx +import datetime from sdxdatamodel.models.topology import Topology, SDX_TOPOLOGY_ID_prefix,TOPOLOGY_INITIAL_VERSION from sdxdatamodel.models.link import Link @@ -60,10 +61,6 @@ def add_topology(self, data): for port in link.ports: self.port_list[port['id']] = link else: - ## update the version and timestamp to be the latest - self.update_version(False) - self.update_timestamp() - ##check the inter-domain links first. self.num_interdomain_link+=self.inter_domain_check(topology) if self.num_interdomain_link==0: @@ -74,15 +71,15 @@ def add_topology(self, data): ##links links = topology.get_links() self.topology.add_links(links) - + #version self.update_version(False) + self.update_timestamp() + def get_domain_name(self,node_id): domain_id=None print("len of topology_list:"+str(len(self.topology_list))) for id, topology in self.topology_list.items(): - print(id) - print(topology.id) if topology.has_node_by_id(node_id): domain_id = id break @@ -90,7 +87,7 @@ def get_domain_name(self,node_id): def generate_id(self): self.topology.set_id(SDX_TOPOLOGY_ID_prefix) - self.topology.set_version(TOPOLOGY_INITIAL_VERSION) + self.topology.version=TOPOLOGY_INITIAL_VERSION return id def remove_topology(self, topology_id): @@ -131,12 +128,24 @@ def get_topology(self): return self.topology def update_version(self,sub:bool): - [ver, sub_ver] = self.topology.version.split('.') + try: + [ver, sub_ver] = self.topology.version.split('.') + except ValueError: + ver=self.topology.version + sub_ver='0' + if not sub: ver=str(int(ver)+1) + sub_ver='0' else: sub_ver=str(int(sub_ver)+1) + self.topology.version=ver+"."+sub_ver + + def update_timestamp(self): + ct = datetime.datetime.now().isoformat() + self.topology.time_stamp=ct + def inter_domain_check(self,topology): interdomain_port_dict={} num_interdomain_link=0 @@ -163,9 +172,6 @@ def inter_domain_check(self,topology): return num_interdomain_link - def update_timestamp(self): - pass - def add_domain_service(self): pass @@ -184,7 +190,7 @@ def generate_graph(self): for port in ports: node=self.topology.get_node_by_port(port['id']) if node is None: - print("This port doesn't belong to any node in the topology, likely an Interdomain port!" + port['id']) + print("This port doesn't belong to any node in the topology, likely a Non-SDX port!" + port['id']) inter_domain_link = True break else: @@ -198,7 +204,6 @@ def generate_graph(self): edge['bandwidth'] = link.bandwidth edge['residual_bandwidth'] = link.residual_bandwidth edge['weight'] = 1000.0*(1.0/link.residual_bandwidth) - #edge['latency'] = link.latency edge['packet_loss'] = link.packet_loss edge['availability'] = link.availability diff --git a/sdxdatamodel/topologymanager/temanager.py b/sdxdatamodel/topologymanager/temanager.py index d0a1829..f365ddf 100644 --- a/sdxdatamodel/topologymanager/temanager.py +++ b/sdxdatamodel/topologymanager/temanager.py @@ -3,6 +3,7 @@ import copy import networkx as nx +from networkx.algorithms import approximation as approx from sdxdatamodel.models.topology import Topology, SDX_TOPOLOGY_ID_prefix,TOPOLOGY_INITIAL_VERSION from sdxdatamodel.models.link import Link @@ -56,6 +57,22 @@ def generate_graph_te(self): graph = nx.convert_node_labels_to_integers(graph,label_attribute='id') return graph + def graph_node_connectivity(self, source=None, dest=None): + conn = approx.node_connectivity(self.graph,source,dest) + return conn + + def requests_connectivity(self, requests): + for request in requests: + conn = self.graph_node_connectivity(request[0],request[1]) + print("Request Connectivity: {}, {} = {}".format( + request[0], + request[1], + conn, + ) + ) + return True + + def generate_connection_breakdown(self, connection): breakdown = {} paths = connection[0] #p2p for now @@ -71,8 +88,6 @@ def generate_connection_breakdown(self, connection): domain_1 = self.manager.get_domain_name(node_1['id']) domain_2 = self.manager.get_domain_name(node_2['id']) current_link_set.append(link) - print("domain_1:"+domain_1) - print("domain_2:"+domain_2) if domain_1 == domain_2: current_domain = domain_1 if count==len(j)-1: diff --git a/setup.cfg b/setup.cfg index dc7b331..060a043 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ name = sdxdatamodel version = 0.0.1 author = Y. Xin author_email = yxin@renci.org -description = +description = Topology and request description data model in JSON long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/atlanticwave-sdx/datamodel diff --git a/tests/data/amlight.json b/tests/data/amlight.json index 46127a4..649638e 100644 --- a/tests/data/amlight.json +++ b/tests/data/amlight.json @@ -3,7 +3,7 @@ "name": "AmLight-OXP", "model_version":"1.0.0", "time_stamp": "2000-01-23T04:56:07+00:00", - "version": 1, + "version": 1.0, "links": [ { "availability": 56.37376656633328, diff --git a/tests/data/sax.json b/tests/data/sax.json index 891877d..286874a 100644 --- a/tests/data/sax.json +++ b/tests/data/sax.json @@ -2,7 +2,7 @@ "id": "urn:ogf:network:sdx:topology:sax.net", "name": "SAX-OXP", "time_stamp": "2000-01-23T04:56:07+00:00", - "version": 1, + "version": 1.0, "model_version":"1.0.0", "links": [ { diff --git a/tests/data/sdx.json b/tests/data/sdx.json index 07d5fae..6712439 100644 --- a/tests/data/sdx.json +++ b/tests/data/sdx.json @@ -9,8 +9,8 @@ "provisioning_url": null, "vendor": null }, - "version": "0.0", - "time_stamp": "2000-01-23T04:56:07+00:00", + "version": "2.0", + "time_stamp": "2022-06-05T11:33:51.585671", "nodes": [ { "id": "urn:sdx:node:amlight.net:B1", diff --git a/tests/data/zaoxi.json b/tests/data/zaoxi.json index 8526074..2f274c2 100644 --- a/tests/data/zaoxi.json +++ b/tests/data/zaoxi.json @@ -2,7 +2,7 @@ "id": "urn:ogf:network:sdx:topology:zaoxi.net", "name": "ZAOXI-OXP", "time_stamp": "2000-01-23T04:56:07+00:00", - "version": 1, + "version": 1.0, "model_version":"1.0.0", "links": [ { diff --git a/tests/test_topology_manager.py b/tests/test_topology_manager.py index 12f070a..c08084b 100644 --- a/tests/test_topology_manager.py +++ b/tests/test_topology_manager.py @@ -22,7 +22,7 @@ TOPOLOGY_png = "./tests/data/sdx.png" TOPOLOGY = "./tests/data/sdx.json" -topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_SAX, TOPOLOGY_ZAOXI] +topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_ZAOXI,TOPOLOGY_SAX] topology_file_list_2 = [TOPOLOGY_SAX, TOPOLOGY_ZAOXI] class TestTopologyManager(unittest.TestCase): From e3603ba1d093bed73b8c9f9ec759ca93487d0ae9 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 6 Jun 2022 15:48:09 -0400 Subject: [PATCH 45/67] print xml in string --- sdxdatamodel/topologymanager/grenmlconverter.py | 2 +- tests/test_topology_grenmlconverter.py | 2 +- tests/test_topology_manager.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdxdatamodel/topologymanager/grenmlconverter.py b/sdxdatamodel/topologymanager/grenmlconverter.py index 50fd349..7313b7d 100644 --- a/sdxdatamodel/topologymanager/grenmlconverter.py +++ b/sdxdatamodel/topologymanager/grenmlconverter.py @@ -28,7 +28,7 @@ def read_topology(self): self.topology_str = self.grenml_manager.write_to_string() - print(self.topology_str) + #print(self.topology_str) def add_nodes(self,nodes): for node in nodes: diff --git a/tests/test_topology_grenmlconverter.py b/tests/test_topology_grenmlconverter.py index fa74702..0c98f8a 100644 --- a/tests/test_topology_grenmlconverter.py +++ b/tests/test_topology_grenmlconverter.py @@ -29,7 +29,7 @@ def testGrenmlConverter(self): print(self.handler.topology) converter = GrenmlConverter(self.handler.topology) converter.read_topology() - print(converter.get_xml_str) + print(converter.get_xml_str()) except DataModelException as e: print(e) return False diff --git a/tests/test_topology_manager.py b/tests/test_topology_manager.py index c08084b..ecf96ae 100644 --- a/tests/test_topology_manager.py +++ b/tests/test_topology_manager.py @@ -55,7 +55,7 @@ def testGrenmlConverter(self): self.testMergeTopology() converter = GrenmlConverter(self.manager.get_topology()) converter.read_topology() - print(converter.get_xml_str) + print(converter.get_xml_str()) except DataModelException as e: print(e) return False From b1bada3686a51ef2f169b3daeb7a41925b1e628e Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Wed, 8 Jun 2022 16:47:52 -0400 Subject: [PATCH 46/67] changed the test input files --- tests/test_connection_handler.py | 4 ++-- tests/test_connection_validator.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_connection_handler.py b/tests/test_connection_handler.py index d79c313..8bde228 100644 --- a/tests/test_connection_handler.py +++ b/tests/test_connection_handler.py @@ -5,8 +5,8 @@ from sdxdatamodel.parsing.connectionhandler import ConnectionHandler from sdxdatamodel.parsing.exceptions import DataModelException -#CONNECTION_P2P = './tests/data/p2p.json' -CONNECTION_P2P = './tests/data/test_connection.json' +CONNECTION_P2P = './tests/data/p2p.json' +#CONNECTION_P2P = './tests/data/test_connection.json' class TestConnectionHandler(unittest.TestCase): diff --git a/tests/test_connection_validator.py b/tests/test_connection_validator.py index 36131e8..498641a 100644 --- a/tests/test_connection_validator.py +++ b/tests/test_connection_validator.py @@ -7,8 +7,8 @@ from sdxdatamodel.parsing.exceptions import DataModelException from sdxdatamodel.models.connection import Connection -#CONNECTION_P2P = './tests/data/p2p.json' -CONNECTION_P2P = './tests/data/test_connection.json' +CONNECTION_P2P = './tests/data/p2p.json' +#CONNECTION_P2P = './tests/data/test_connection.json' class TestConnectionValidator(unittest.TestCase): From 6bfb23eaf84ce8ff2f8c108a54fb998349bccd1d Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 13 Jun 2022 12:57:44 -0400 Subject: [PATCH 47/67] complete (1) version and time_stamp update; (2) update a property of an element in the topology, both the json and the topology object, unitest is included #15, #17 --- sdxdatamodel/parsing/linkhandler.py | 2 +- sdxdatamodel/parsing/locationhandler.py | 2 +- sdxdatamodel/parsing/nodehandler.py | 2 +- sdxdatamodel/parsing/porthandler.py | 2 +- sdxdatamodel/parsing/servicehandler.py | 2 +- sdxdatamodel/topologymanager/manager.py | 82 ++++++++++++++++++++----- tests/data/sdx.json | 2 +- tests/test_topology_manager.py | 31 ++++++++++ 8 files changed, 105 insertions(+), 20 deletions(-) diff --git a/sdxdatamodel/parsing/linkhandler.py b/sdxdatamodel/parsing/linkhandler.py index 7187cbe..87fb783 100644 --- a/sdxdatamodel/parsing/linkhandler.py +++ b/sdxdatamodel/parsing/linkhandler.py @@ -49,5 +49,5 @@ def import_link(self,file): data = json.load(data_file) self.link = self.import_link_data(data) - def get_link(): + def get_link(self): return self.link diff --git a/sdxdatamodel/parsing/locationhandler.py b/sdxdatamodel/parsing/locationhandler.py index e9830ad..9fa5b36 100644 --- a/sdxdatamodel/parsing/locationhandler.py +++ b/sdxdatamodel/parsing/locationhandler.py @@ -33,5 +33,5 @@ def import_location(self,file): data = json.load(data_file) self.location = self.import_location_data(data) - def get_location(): + def get_location(self): return self.location diff --git a/sdxdatamodel/parsing/nodehandler.py b/sdxdatamodel/parsing/nodehandler.py index 5692ae9..f0f49c5 100644 --- a/sdxdatamodel/parsing/nodehandler.py +++ b/sdxdatamodel/parsing/nodehandler.py @@ -34,5 +34,5 @@ def import_node(self,file): data = json.load(data_file) self.node = self.import_node_data(data) - def get_node(): + def get_node(self): return self.node diff --git a/sdxdatamodel/parsing/porthandler.py b/sdxdatamodel/parsing/porthandler.py index dfa1733..a34b058 100644 --- a/sdxdatamodel/parsing/porthandler.py +++ b/sdxdatamodel/parsing/porthandler.py @@ -40,5 +40,5 @@ def import_port(self,file): data = json.load(data_file) self.port = self.import_port_data(data) - def get_port(): + def get_port(self): return self.port diff --git a/sdxdatamodel/parsing/servicehandler.py b/sdxdatamodel/parsing/servicehandler.py index 486a339..189061d 100644 --- a/sdxdatamodel/parsing/servicehandler.py +++ b/sdxdatamodel/parsing/servicehandler.py @@ -38,5 +38,5 @@ def import_service(self,file): data = json.load(data_file) self.service = self.import_service_data(data) - def get_service(): + def get_service(self): return self.service diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index e6a1600..e2c553c 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -41,6 +41,9 @@ def topology_id(self, id): def set_topology(self, topology): self.topology = topology + def get_topology(self): + return self.topology + def clear_topology(self): self.topology=None self.topology_list={} @@ -124,9 +127,6 @@ def update_topology(self,data): self.update_version(True) self.update_timestamp() - def get_topology(self): - return self.topology - def update_version(self,sub:bool): try: [ver, sub_ver] = self.topology.version.split('.') @@ -134,18 +134,25 @@ def update_version(self,sub:bool): ver=self.topology.version sub_ver='0' + self.topology.version=self.new_version(ver, sub_ver,sub) + + return self.topology.version + + def new_version(self,ver, sub_ver,sub:bool): if not sub: ver=str(int(ver)+1) sub_ver='0' else: sub_ver=str(int(sub_ver)+1) - self.topology.version=ver+"."+sub_ver + return ver+"."+sub_ver def update_timestamp(self): ct = datetime.datetime.now().isoformat() self.topology.time_stamp=ct + return ct + def inter_domain_check(self,topology): interdomain_port_dict={} num_interdomain_link=0 @@ -172,13 +179,6 @@ def inter_domain_check(self,topology): return num_interdomain_link - def add_domain_service(self): - pass - - #may need to read from a configuration file. - def update_private_properties(self): - pass - #adjacent matrix of the graph, in jason? def generate_graph(self): G = nx.Graph() @@ -209,9 +209,63 @@ def generate_graph(self): return G - - def generate_grenml(self): self.converter = GrenmlConverter(self.topology) - return self.converter.read_topology() \ No newline at end of file + return self.converter.read_topology() + + def add_domain_service(self): + pass + + #may need to read from a configuration file. + def update_private_properties(self): + pass + + # on performance properties for now + def update_link_property(self,link_id,property,value): + #1. update the individual topology + for id, topology in self.topology_list.items(): + links = topology.get_links() + for link in links: + print(link.id + ";" + id) + if link.id == link_id: + setattr(link,property,value) + print("updated the link.") + #1.2 need to change the sub_ver of the topology? + + #2. check on the inter-domain link? + #3. update the interodamin topology + links = self.topology.get_links() + for link in links: + if link.id == link_id: + setattr(link,property,value) + print("updated the link.") + #2.2 need to change the sub_ver of the topology? + + self.update_version(True) + self.update_timestamp() + #4. Signal update the (networkx) graph + + # 5. signal Reoptimization of TE? + + def update_element_property_json(self,data,element, element_id,property,value): + elements=data[element] + for element in elements: + if element['id'] == element_id: + element[property] = value + + try: + [ver, sub_ver] = data['version'].split('.') + except ValueError: + ver='0' + sub_ver='0' + + data['version'] = self.new_version(ver,sub_ver,True) + data['time_stamp'] = datetime.datetime.now().isoformat() + + + def update_node_property(self): + pass + + def update_port_property(self): + pass diff --git a/tests/data/sdx.json b/tests/data/sdx.json index 6712439..01e8d88 100644 --- a/tests/data/sdx.json +++ b/tests/data/sdx.json @@ -10,7 +10,7 @@ "vendor": null }, "version": "2.0", - "time_stamp": "2022-06-05T11:33:51.585671", + "time_stamp": "2022-06-13T12:55:33.034500", "nodes": [ { "id": "urn:sdx:node:amlight.net:B1", diff --git a/tests/test_topology_manager.py b/tests/test_topology_manager.py index ecf96ae..9361b0a 100644 --- a/tests/test_topology_manager.py +++ b/tests/test_topology_manager.py @@ -25,6 +25,9 @@ topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_ZAOXI,TOPOLOGY_SAX] topology_file_list_2 = [TOPOLOGY_SAX, TOPOLOGY_ZAOXI] +link_id = "urn:ogf:network:sdx:link:amlight:A1-B2" +inter_link_id = "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo" + class TestTopologyManager(unittest.TestCase): def setUp(self): @@ -74,5 +77,33 @@ def testGenerateGraph(self): return False return True + def testLinkPropertyUpdate(self): + print("Test Topology Link Property Update!") + try: + self.testMergeTopology() + self.manager.update_link_property(link_id,'latency',8) + self.manager.update_link_property(inter_link_id,'latency',8) + with open(TOPOLOGY, 'w') as t_file: + json.dump(self.manager.topology.to_dict(), t_file, indent=4) + except DataModelException as e: + print(e) + return False + return True + + def testLinkPropertyUpdateJson(self): + print("Test Topology JSON Link Property Update!") + try: + with open(TOPOLOGY, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + self.manager.update_element_property_json(data,'links',link_id,'latency',20) + with open(TOPOLOGY, 'w') as t_file: + json.dump(data, t_file, indent=4) + except DataModelException as e: + print(e) + return False + return True + + + if __name__ == '__main__': unittest.main() \ No newline at end of file From 24404db59452659b8f10236f2b281feb9dacf7ed Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 13 Jun 2022 15:17:04 -0400 Subject: [PATCH 48/67] a typo --- sdxdatamodel/topologymanager/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index e2c553c..3b80eab 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -110,7 +110,7 @@ def update_topology(self,data): ##links links = topology.get_links() for link in links: - self.topology.remove_links(link.id) + self.topology.remove_link(link.id) ##check the inter-domain links first. num_interdomain_link=self.inter_domain_check(topology) From 8031c908885362888438dac9154e1e4a46393bfb Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 13 Jun 2022 18:39:47 -0400 Subject: [PATCH 49/67] fixing a topology update bug, adding an unittest --- sdxdatamodel/models/link.py | 28 ++++++++++++++++++++++- sdxdatamodel/parsing/linkhandler.py | 6 +++-- sdxdatamodel/topologymanager/manager.py | 17 ++++++++++---- sdxdatamodel/topologymanager/temanager.py | 6 ++++- tests/data/sax.json | 1 + tests/test_topology_manager.py | 23 ++++++++++++++++++- 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/sdxdatamodel/models/link.py b/sdxdatamodel/models/link.py index 0db9a78..10210a1 100644 --- a/sdxdatamodel/models/link.py +++ b/sdxdatamodel/models/link.py @@ -27,6 +27,7 @@ class Link(object): 'id': 'str', 'name': 'str', 'short_name': 'str', + 'nni': 'bool', 'ports': 'list[Port]', 'bandwidth': 'float', 'residual_bandwidth': 'float', @@ -44,6 +45,7 @@ class Link(object): 'id': 'id', 'name': 'name', 'short_name': 'short_name', + 'nni': 'nni', 'ports': 'ports', 'bandwidth': 'bandwidth', 'residual_bandwidth': 'residual_bandwidth', @@ -57,11 +59,12 @@ class Link(object): 'measurement_period': 'measurement_period' } - def __init__(self, id=None, name=None, short_name=None, ports=None, bandwidth=None, residual_bandwidth=None, latency=None, packet_loss=None, availability=None, private_attributes=None, time_stamp=None, measurement_period=None): # noqa: E501 + def __init__(self, id=None, name=None, short_name=None, nni=None, ports=None, bandwidth=None, residual_bandwidth=None, latency=None, packet_loss=None, availability=None, private_attributes=None, time_stamp=None, measurement_period=None): # noqa: E501 """Link - a model defined in Swagger""" # noqa: E501 self._id = None self._name = None self._short_name = None + self._nni = None self._ports = None self._bandwidth = None self._residual_bandwidth = None @@ -77,6 +80,8 @@ def __init__(self, id=None, name=None, short_name=None, ports=None, bandwidth=No self._name = name if short_name is not None: self._short_name = short_name + if nni is not None: + self._nni = nni if ports is not None: self._ports = self.set_ports(ports) if bandwidth is not None: @@ -163,6 +168,27 @@ def short_name(self, short_name): self._short_name = short_name + @property + def nni(self): + """Gets the short_name of this Link. # noqa: E501 + + + :return: The short_name of this Link. # noqa: E501 + :rtype: str + """ + return self._nni + + @nni.setter + def nni(self, nni): + """Sets the short_name of this Link. + + + :param short_name: The short_name of this Link. # noqa: E501 + :type: str + """ + + self._nni = nni + @property def ports(self): """Gets the ports of this Link. # noqa: E501 diff --git a/sdxdatamodel/parsing/linkhandler.py b/sdxdatamodel/parsing/linkhandler.py index 87fb783..240493b 100644 --- a/sdxdatamodel/parsing/linkhandler.py +++ b/sdxdatamodel/parsing/linkhandler.py @@ -19,7 +19,9 @@ def import_link_data(self, data): short_name=data['short_name'] ports=data['ports'] - timestamp=None;t_b=None;a_b=None;latency=None;p_l=None;p_a=None;avai=None;m_p=None + n=None;timestamp=None;t_b=None;a_b=None;latency=None;p_l=None;p_a=None;avai=None;m_p=None + if 'nni' in data.keys(): + n=bool(data['nni']) if 'time_stamp' in data.keys(): timestamp=data['time_stamp'] if 'bandwidth' in data.keys(): @@ -39,7 +41,7 @@ def import_link_data(self, data): except KeyError as e: raise MissingAttributeException(e.args[0],e.args[0]) - link=Link(id=id, name=name, short_name=short_name, ports=ports, bandwidth=t_b, residual_bandwidth=a_b, + link=Link(id=id, name=name, short_name=short_name, ports=ports, nni=n, bandwidth=t_b, residual_bandwidth=a_b, latency=latency, packet_loss=p_l, availability=avai, private_attributes=p_a, time_stamp=timestamp, measurement_period=m_p) return link diff --git a/sdxdatamodel/topologymanager/manager.py b/sdxdatamodel/topologymanager/manager.py index 3b80eab..8b70a82 100644 --- a/sdxdatamodel/topologymanager/manager.py +++ b/sdxdatamodel/topologymanager/manager.py @@ -81,7 +81,7 @@ def add_topology(self, data): def get_domain_name(self,node_id): domain_id=None - print("len of topology_list:"+str(len(self.topology_list))) + #print("len of topology_list:"+str(len(self.topology_list))) for id, topology in self.topology_list.items(): if topology.has_node_by_id(node_id): domain_id = id @@ -100,7 +100,8 @@ def remove_topology(self, topology_id): def update_topology(self,data): #likely adding new inter-domain links - topology = self.handler.import_topology_data(data) + update_handler = TopologyHandler() + topology = update_handler.import_topology_data(data) self.topology_list[topology.id] = topology ##nodes @@ -110,7 +111,11 @@ def update_topology(self,data): ##links links = topology.get_links() for link in links: - self.topology.remove_link(link.id) + if link.nni != True: + #print(link.id+";......."+str(link.nni)) + self.topology.remove_link(link.id) + for port in link.ports: + self.port_list.pop(port['id']) ##check the inter-domain links first. num_interdomain_link=self.inter_domain_check(topology) @@ -165,13 +170,17 @@ def inter_domain_check(self,topology): #ToDo: raise an warning or exception if len(interdomain_port_dict)==0: + print("interdomain_port_dict==0") return False #match any ports in the existing topology for port_id in interdomain_port_dict: + #print("interdomain_port:") + #print(port_id) for existing_port, existing_link in self.port_list.items(): + #print(existing_port) if port_id == existing_port: - print("Interdomain port:" + port_id) + #print("Interdomain port:" + port_id) #remove redundant link between two domains self.topology.remove_link(existing_link.id) num_interdomain_link=+1 diff --git a/sdxdatamodel/topologymanager/temanager.py b/sdxdatamodel/topologymanager/temanager.py index f365ddf..136b5ff 100644 --- a/sdxdatamodel/topologymanager/temanager.py +++ b/sdxdatamodel/topologymanager/temanager.py @@ -55,6 +55,8 @@ def generate_connection_te(self): def generate_graph_te(self): graph = self.manager.generate_graph() graph = nx.convert_node_labels_to_integers(graph,label_attribute='id') + self.graph=graph + #print(list(graph.nodes(data=True))) return graph def graph_node_connectivity(self, source=None, dest=None): @@ -88,8 +90,9 @@ def generate_connection_breakdown(self, connection): domain_1 = self.manager.get_domain_name(node_1['id']) domain_2 = self.manager.get_domain_name(node_2['id']) current_link_set.append(link) + current_domain = domain_1 if domain_1 == domain_2: - current_domain = domain_1 + #current_domain = domain_1 if count==len(j)-1: breakdown[current_domain]=current_link_set.copy() else: @@ -101,6 +104,7 @@ def generate_connection_breakdown(self, connection): first = True i=0 domain_breakdown={} + for domain, links in breakdown.items(): segment={} if first: diff --git a/tests/data/sax.json b/tests/data/sax.json index 286874a..c627727 100644 --- a/tests/data/sax.json +++ b/tests/data/sax.json @@ -179,6 +179,7 @@ "availability": 56.37376656633328, "residual_bandwidth": 100000, "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", + "nni": "True", "latency": 10, "name": "nni:Miami-Sanpaolo", "packet_loss": 59.621339166831824, diff --git a/tests/test_topology_manager.py b/tests/test_topology_manager.py index 9361b0a..0831d23 100644 --- a/tests/test_topology_manager.py +++ b/tests/test_topology_manager.py @@ -23,7 +23,7 @@ TOPOLOGY = "./tests/data/sdx.json" topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_ZAOXI,TOPOLOGY_SAX] -topology_file_list_2 = [TOPOLOGY_SAX, TOPOLOGY_ZAOXI] +topology_file_list_update = [TOPOLOGY_ZAOXI] link_id = "urn:ogf:network:sdx:link:amlight:A1-B2" inter_link_id = "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo" @@ -52,6 +52,27 @@ def testMergeTopology(self): return False return True + def testUpdateTopology(self): + print("Test Topology Update!") + try: + self.testMergeTopology() + for topology_file in topology_file_list_update: + with open(topology_file, 'r', encoding='utf-8') as data_file: + data = json.load(data_file) + print("Updating Topology:" + topology_file) + self.manager.update_topology(data) + + with open(TOPOLOGY, 'w') as t_file: + json.dump(self.manager.topology.to_dict(), t_file, indent=4) + graph = self.manager.generate_graph() + #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout + nx.draw(graph,with_labels = True) + plt.savefig(TOPOLOGY_png) + except DataModelException as e: + print(e) + return False + return True + def testGrenmlConverter(self): try: print("Test Topology GRENML Converter") From 3f99454f232416a3c4fa66355c74ade13478c529 Mon Sep 17 00:00:00 2001 From: Yufeng Xin Date: Mon, 13 Jun 2022 18:40:36 -0400 Subject: [PATCH 50/67] #26 --- tests/data/sdx.json | 470 +++++++++++++++++++++++--------------------- 1 file changed, 242 insertions(+), 228 deletions(-) diff --git a/tests/data/sdx.json b/tests/data/sdx.json index 01e8d88..f2834f2 100644 --- a/tests/data/sdx.json +++ b/tests/data/sdx.json @@ -9,8 +9,8 @@ "provisioning_url": null, "vendor": null }, - "version": "2.0", - "time_stamp": "2022-06-13T12:55:33.034500", + "version": "2.1", + "time_stamp": "2022-06-13T18:24:16.850814", "nodes": [ { "id": "urn:sdx:node:amlight.net:B1", @@ -156,17 +156,17 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:zaoxi:B1", - "name": "zaoxi:Novi01", + "id": "urn:ogf:network:sdx:node:sax:B1", + "name": "sax:Novi01", "short_name": "B1", "location": { - "address": "Sangano", - "latitude": 13.216709879405311, - "longitude": -9.533459658700743 + "address": "SaoPaulo", + "latitude": -46.650271781410524, + "longitude": -23.5311561958366 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", + "id": "urn:ogf:network:sdx:port:sax:B1:1", "name": "Novi01:1", "short_name": null, "node": "B1:1", @@ -179,7 +179,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "id": "urn:ogf:network:sdx:port:sax:B1:2", "name": "Novi01:2", "short_name": null, "node": "B1:2", @@ -192,7 +192,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "id": "urn:ogf:network:sdx:port:sax:B1:3", "name": "Novi01:3", "short_name": null, "node": "B1:3", @@ -203,22 +203,35 @@ "status": null, "state": null, "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4", + "name": "Novi01:3", + "short_name": null, + "node": "B1:4", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null } ], "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:zaoxi:B2", - "name": "zaoxi:Novi02", + "id": "urn:ogf:network:sdx:node:sax:B2", + "name": "sax:Novi02", "short_name": "B2", "location": { - "address": "CapeTown", - "latitude": -38.52443289673026, - "longitude": -3.73163824920348 + "address": "PanamaCity", + "latitude": -79.4947050137491, + "longitude": 8.993040465928525 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:1", + "id": "urn:ogf:network:sdx:port:sax:B2:1", "name": "Novi02:1", "short_name": null, "node": "B2:1", @@ -231,7 +244,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "id": "urn:ogf:network:sdx:port:sax:B2:2", "name": "Novi02:2", "short_name": null, "node": "B2:2", @@ -244,7 +257,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "id": "urn:ogf:network:sdx:port:sax:B2:3", "name": "Novi02:3", "short_name": null, "node": "B2:3", @@ -255,41 +268,15 @@ "status": null, "state": null, "private_attributes": null - } - ], - "private_attributes": null - }, - { - "id": "urn:ogf:network:sdx:node:zaoxi:A1", - "name": "zaoxi:Novi100", - "short_name": "A1", - "location": { - "address": "Karoo", - "latitude": 22.541224555821298, - "longitude": -32.3632301851245 - }, - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", - "name": "Novi100:1", - "short_name": null, - "node": "A1:1", - "label_range": [ - "100-200", - "1000" - ], - "status": null, - "state": null, - "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", - "name": "Novi100:2", + "id": "urn:ogf:network:sdx:port:sax:B2:4", + "name": "Novi02:4", "short_name": null, - "node": "A1:2", + "node": "B2:3", "label_range": [ "100-200", - "1000" + "10001" ], "status": null, "state": null, @@ -299,46 +286,33 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:B1", - "name": "sax:Novi01", - "short_name": "B1", + "id": "urn:ogf:network:sdx:node:sax:B3", + "name": "sax:Novi03", + "short_name": "B3", "location": { - "address": "SaoPaulo", - "latitude": -46.650271781410524, - "longitude": -23.5311561958366 + "address": "Fortaleza", + "latitude": -38.52443289673026, + "longitude": -3.73163824920348 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:B1:1", - "name": "Novi01:1", - "short_name": null, - "node": "B1:1", - "label_range": [ - "100-200", - "10001" - ], - "status": null, - "state": null, - "private_attributes": null - }, - { - "id": "urn:ogf:network:sdx:port:sax:B1:2", - "name": "Novi01:2", + "id": "urn:ogf:network:sdx:port:sax:B3:1", + "name": "Novi02:3", "short_name": null, - "node": "B1:2", + "node": "B3:1", "label_range": [ "100-200", - "10001" + "1000" ], "status": null, "state": null, "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B1:3", - "name": "Novi01:3", + "id": "urn:ogf:network:sdx:port:sax:B3:2", + "name": "Novi02:3", "short_name": null, - "node": "B1:3", + "node": "B3:2", "label_range": [ "100-200", "10001" @@ -348,10 +322,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B1:4", - "name": "Novi01:3", + "id": "urn:ogf:network:sdx:port:sax:B3:3", + "name": "Novi03:3", "short_name": null, - "node": "B1:4", + "node": "B3:3", "label_range": [ "100-200", "10001" @@ -364,20 +338,20 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:B2", - "name": "sax:Novi02", - "short_name": "B2", + "id": "urn:ogf:network:sdx:node:sax:A1", + "name": "sax:Novi100", + "short_name": "A1", "location": { - "address": "PanamaCity", - "latitude": -79.4947050137491, - "longitude": 8.993040465928525 + "address": "Santiago", + "latitude": -70.64634765264213, + "longitude": -33.4507049233331 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:B2:1", - "name": "Novi02:1", + "id": "urn:ogf:network:sdx:port:sax:A1:1", + "name": "Novi100:1", "short_name": null, - "node": "B2:1", + "node": "A1:1", "label_range": [ "100-200", "1000" @@ -387,10 +361,36 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B2:2", - "name": "Novi02:2", + "id": "urn:ogf:network:sdx:port:sax:A1:2", + "name": "Novi100:2", "short_name": null, - "node": "B2:2", + "node": "A1:2", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:B1", + "name": "zaoxi:Novi01", + "short_name": "B1", + "location": { + "address": "Sangano", + "latitude": 13.216709879405311, + "longitude": -9.533459658700743 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", + "name": "Novi01:1", + "short_name": null, + "node": "B1:1", "label_range": [ "100-200", "10001" @@ -400,10 +400,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B2:3", - "name": "Novi02:3", + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", "short_name": null, - "node": "B2:3", + "node": "B1:2", "label_range": [ "100-200", "10001" @@ -413,10 +413,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B2:4", - "name": "Novi02:4", + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", "short_name": null, - "node": "B2:3", + "node": "B1:3", "label_range": [ "100-200", "10001" @@ -429,20 +429,20 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:B3", - "name": "sax:Novi03", - "short_name": "B3", + "id": "urn:ogf:network:sdx:node:zaoxi:B2", + "name": "zaoxi:Novi02", + "short_name": "B2", "location": { - "address": "Fortaleza", + "address": "CapeTown", "latitude": -38.52443289673026, "longitude": -3.73163824920348 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:B3:1", - "name": "Novi02:3", + "id": "urn:ogf:network:sdx:port:zaoxi:B2:1", + "name": "Novi02:1", "short_name": null, - "node": "B3:1", + "node": "B2:1", "label_range": [ "100-200", "1000" @@ -452,10 +452,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B3:2", - "name": "Novi02:3", + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "name": "Novi02:2", "short_name": null, - "node": "B3:2", + "node": "B2:2", "label_range": [ "100-200", "10001" @@ -465,10 +465,10 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:B3:3", - "name": "Novi03:3", + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "name": "Novi02:3", "short_name": null, - "node": "B3:3", + "node": "B2:3", "label_range": [ "100-200", "10001" @@ -481,17 +481,17 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:node:sax:A1", - "name": "sax:Novi100", + "id": "urn:ogf:network:sdx:node:zaoxi:A1", + "name": "zaoxi:Novi100", "short_name": "A1", "location": { - "address": "Santiago", - "latitude": -70.64634765264213, - "longitude": -33.4507049233331 + "address": "Karoo", + "latitude": 22.541224555821298, + "longitude": -32.3632301851245 }, "ports": [ { - "id": "urn:ogf:network:sdx:port:sax:A1:1", + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", "name": "Novi100:1", "short_name": null, "node": "A1:1", @@ -504,7 +504,7 @@ "private_attributes": null }, { - "id": "urn:ogf:network:sdx:port:sax:A1:2", + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", "name": "Novi100:2", "short_name": null, "node": "A1:2", @@ -525,6 +525,7 @@ "id": "urn:ogf:network:sdx:link:amlight:B1-B2", "name": "amlight:B1-B2", "short_name": "Miami-BocaRaton", + "nni": null, "ports": [ { "id": "urn:sdx:port:amlight.net:B1:2", @@ -564,6 +565,7 @@ "id": "urn:ogf:network:sdx:link:amlight:A1-B1", "name": "amlight:A1-B1", "short_name": "redclara-miami", + "nni": null, "ports": [ { "id": "urn:sdx:port:amlight.net:A1:1", @@ -603,6 +605,7 @@ "id": "urn:ogf:network:sdx:link:amlight:A1-B2", "name": "amlight:A1-B2", "short_name": "redclara-BocaRaton", + "nni": null, "ports": [ { "id": "urn:sdx:port:amlight.net:A1:2", @@ -638,122 +641,11 @@ "time_stamp": null, "measurement_period": null }, - { - "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", - "name": "zaoxi:B1-B2", - "short_name": "Sangano-Capetown", - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", - "name": "Novi01:2", - "node": "urn:ogf:network:sdx:node:zaoxi:B1", - "status": "up" - }, - { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", - "label_range": [ - "100-200", - "10001" - ], - "name": "Novi02:2", - "node": "urn:ogf:network:sdx:node:zaoxi:B2", - "short_name": "B2:2", - "status": "up" - } - ], - "bandwidth": 100000, - "residual_bandwidth": 100000, - "latency": 5, - "packet_loss": 59.621339166831824, - "availability": 56.37376656633328, - "status": null, - "state": null, - "private_attributes": null, - "time_stamp": null, - "measurement_period": null - }, - { - "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", - "name": "zaoxi:A1-B1", - "short_name": "Karoo-Sangano", - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", - "label_range": [ - "100-200", - "1000" - ], - "name": "Novi100:1", - "node": "urn:ogf:network:sdx:node:zaoxi:A1", - "short_name": "A1:1", - "status": "up" - }, - { - "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", - "name": "Novi01:3", - "node": "urn:ogf:network:sdx:node:zaoxi:B1", - "short_name": "B1:3", - "label_range": [ - "100-200", - "10001" - ], - "status": "up" - } - ], - "bandwidth": 100000, - "residual_bandwidth": 100000, - "latency": 5, - "packet_loss": 59.621339166831824, - "availability": 56.37376656633328, - "status": null, - "state": null, - "private_attributes": null, - "time_stamp": null, - "measurement_period": null - }, - { - "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", - "name": "zaoxi:A1-B2", - "short_name": "Karoo-Capetown", - "ports": [ - { - "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", - "label_range": [ - "100-200", - "1000" - ], - "name": "Novi100:2", - "node": "urn:ogf:network:sdx:node:zaoxi:A1", - "short_name": "A1:2", - "status": "up" - }, - { - "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", - "label_range": [ - "100-200", - "10001" - ], - "name": "Novi02:3", - "node": "urn:ogf:network:sdx:node:zaoxi:B2", - "short_name": "B2:3", - "status": "up" - } - ], - "bandwidth": 100000, - "residual_bandwidth": 100000, - "latency": 5, - "packet_loss": 59.621339166831824, - "availability": 56.37376656633328, - "status": null, - "state": null, - "private_attributes": null, - "time_stamp": null, - "measurement_period": null - }, { "id": "urn:ogf:network:sdx:link:sax:B1-B2", "name": "sax:B1-B2", "short_name": "SaoPaulo-Fortaleza", + "nni": null, "ports": [ { "id": "urn:ogf:network:sdx:port:sax:B1:2", @@ -793,6 +685,7 @@ "id": "urn:ogf:network:sdx:link:sax:Panama-Fortaleza", "name": "sax:Panama-Fortaleza", "short_name": "Panama-Fortaleza", + "nni": null, "ports": [ { "id": "urn:ogf:network:sdx:port:sax:B3:2", @@ -832,6 +725,7 @@ "id": "urn:ogf:network:sdx:link:sax:SanPaolo-Fortaleza", "name": "nni:SanPaolo-Fortaleza", "short_name": "BocaRaton-Fortaleza", + "nni": null, "ports": [ { "id": "urn:ogf:network:sdx:port:sax:B3:3", @@ -871,6 +765,7 @@ "id": "urn:ogf:network:sdx:link:sax:A1-B1", "name": "sax:A1-B1", "short_name": "redclara-SaoPaulo", + "nni": null, "ports": [ { "id": "urn:ogf:network:sdx:port:sax:A1:1", @@ -910,6 +805,7 @@ "id": "urn:ogf:network:sdx:link:sax:A1-B2", "name": "sax:A1-B2", "short_name": "redclara-Fortaleza", + "nni": null, "ports": [ { "id": "urn:ogf:network:sdx:port:sax:A1:2", @@ -949,6 +845,7 @@ "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", "name": "nni:Miami-Sanpaolo", "short_name": "Miami-Sanpaolo", + "nni": true, "ports": [ { "id": "urn:sdx:port:amlight:B1:1", @@ -988,6 +885,7 @@ "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", "name": "nni:BocaRaton-Fortaleza", "short_name": "BocaRaton-Fortaleza", + "nni": true, "ports": [ { "id": "urn:sdx:port:amlight.net:B2:1", @@ -1023,10 +921,126 @@ "time_stamp": null, "measurement_period": null }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", + "name": "zaoxi:B1-B2", + "short_name": "Sangano-Capetown", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", + "name": "zaoxi:A1-B1", + "short_name": "Karoo-Sangano", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", + "name": "zaoxi:A1-B2", + "short_name": "Karoo-Capetown", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, { "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", "name": "nni:Fortaleza-Sangano", "short_name": "Fortaleza-Sangano", + "nni": true, "ports": [ { "id": "urn:ogf:network:sdx:port:sax:B3:1", @@ -1053,7 +1067,7 @@ ], "bandwidth": 100000, "residual_bandwidth": 100000, - "latency": 5, + "latency": 10, "packet_loss": 59.621339166831824, "availability": 56.37376656633328, "status": null, From 93e16d1d22afa4e9a55b40706807d88a1fd54c69 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 14 Jun 2022 13:22:02 -0500 Subject: [PATCH 51/67] Ignore some files --- .gitignore | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba73e00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# ignore some usual virtual environments. +venv* + +# Ignore files added by editors and OS. +*.swp +*.swo +*~ +*.bak +.*.kate-swp +*.DS_Store + +# Ignore files generated by build tools. +*.pyc +*.pyo +*.egg-info/ +*.egg +.pc + +# Ignore files generated by coverage tools. +/.coverage +/.coverage.* +/.coverage.el +/coverage.xml + +# Ignore files generated when running tests. +/tests/data/amlight.png +/tests/data/sdx.png +/tests/data/sdx.json From 49dcdcfde73c927784b7891d8e8e8817dd1ec2b8 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 14 Jun 2022 13:26:53 -0500 Subject: [PATCH 52/67] Remove files generated when running tests --- tests/data/amlight.png | Bin 19477 -> 0 bytes tests/data/sdx.json | 1081 ---------------------------------------- 2 files changed, 1081 deletions(-) delete mode 100644 tests/data/amlight.png delete mode 100644 tests/data/sdx.json diff --git a/tests/data/amlight.png b/tests/data/amlight.png deleted file mode 100644 index d38484fcc020ea30218ed61d5954b04e6cc59398..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19477 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 Date: Tue, 14 Jun 2022 13:35:21 -0500 Subject: [PATCH 53/67] Add a requirement file for tests --- test-requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test-requirements.txt diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..3875b20 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +-r requirements.txt + +matplotlib==3.5.2 From 2403a8446b3492ddd3579159f170ec25d38ec6f5 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 14 Jun 2022 13:35:42 -0500 Subject: [PATCH 54/67] Update instructions about running tests --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d4ef9c..2f483de 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,23 @@ This set of updates mainly come from the domain monitoring system which is suppo There are defined in the *schema* subfolder. Some attributes of each objects are requied (Can be found in the API definition) while some are optional. Two attributes are worth of mentioning: (1) In the *service* object, there is a *vender* attribute for the domain to list device vendors that are NOT in its domain, (2) in the topology, link, node, and port objects, there is an *private* attibute for the domain to list attributes that need to kept private.:wq ## Usage -## Unittest + +### Running tests + +Run tests with: + ``` -python -m unittest -v tests.test_topology_handler +python -m pip install -r test-requirements.txt +python -m unittest ``` + +If you want to run some specific tests: + ``` +python -m unittest -v tests.test_topology_handler python -m unittest -v tests.test_topology_validator ``` + ## Install ``` pip install -r requirements.txt From 5bfc647de733536e7742dacc219661a4456fe911 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 10:56:25 -0500 Subject: [PATCH 55/67] Remove deprecated pytest-runner Turns out that pytest-runner is deprecated, according to their documentation at https://github.com/pytest-dev/pytest-runner/. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 060a043..c7168d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,8 +27,6 @@ packages = find: install_requires = python-dateutil~=2.8 python_requires = >=3.6 -setup_requires=( - 'pytest-runner',), tests_require=( 'pytest-cov',), From 8e58768e9f9aa87153921e0442e44d334bbba2d3 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 11:01:06 -0500 Subject: [PATCH 56/67] Use correct setup.cfg syntax --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index c7168d4..b14d98e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,8 +27,8 @@ packages = find: install_requires = python-dateutil~=2.8 python_requires = >=3.6 -tests_require=( - 'pytest-cov',), +tests_require = + pytest-cov [options.packages.find] exclude = From c050dcd33bd59e9046e09c1900d9aa995fbb1c4a Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 11:04:26 -0500 Subject: [PATCH 57/67] Remove aliases from setup.cfg Firstly, it doesn't work: $ python3 setup.py test usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] or: setup.py --help [cmd1 cmd2 ...] or: setup.py --help-commands or: setup.py cmd --help error: invalid command 'pytest' Secondly, invoking setup.py is not encouraged anymore: https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index b14d98e..741abeb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[aliases] -test = pytest - [tool:pytest] addopts = --cov --cov-report html --cov-report term-missing From c805608c00ab7f37f010c5884a7dd21603953522 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 14 Jun 2022 13:54:58 -0500 Subject: [PATCH 58/67] Add some GitHub Action --- .github/workflows/test.yml | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..262da6e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,46 @@ +# Install Python dependencies, run tests and lint. +# +# For more information see: +# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Test + +on: + push: + branches: [ "main", "develop" ] + pull_request: + branches: [ "main", "develop" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: [ "3.8", "3.9", "3.10" ] + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi + + - name: Lint with flake8 + run: | + # Stop the build if there are Python syntax errors or undefined names. + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. + flake8 . --count --exit-zero --max-complexity=10 --statistics + + - name: Test with pytest + run: | + pytest From ec436031c7a889f63506211b121fbab6c64d955f Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 14 Jun 2022 15:14:24 -0500 Subject: [PATCH 59/67] Use matrix for various Python versions --- .github/workflows/test.yml | 59 ++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 262da6e..dd9ac74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,32 +15,41 @@ permissions: contents: read jobs: - build: + test: - runs-on: ubuntu-latest + runs-on: + - ubuntu-latest + + matrix: + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" steps: - - name: Check out code - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: [ "3.8", "3.9", "3.10" ] + - name: Check out code + uses: actions/checkout@v3 + + # See https://github.com/marketplace/actions/setup-python + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - - - name: Lint with flake8 - run: | - # Stop the build if there are Python syntax errors or undefined names. - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. - flake8 . --count --exit-zero --max-complexity=10 --statistics - - - name: Test with pytest - run: | - pytest + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi + + - name: Lint with flake8 + run: | + # Stop the build if there are Python syntax errors or undefined names. + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. + flake8 . --count --exit-zero --max-complexity=10 --statistics + + - name: Test with pytest + run: | + pytest From f58034fdc0ab941bb660a4ebefb9e824765a26f6 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 14 Jun 2022 15:19:16 -0500 Subject: [PATCH 60/67] "matrix" should rather be "strategy.matrix" --- .github/workflows/test.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd9ac74..4c79b23 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,12 +20,13 @@ jobs: runs-on: - ubuntu-latest - matrix: - python-version: - - "3.7" - - "3.8" - - "3.9" - - "3.10" + strategy: + matrix: + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" steps: - name: Check out code From 2baf8ddfd771458e2fca9a6975063f37e5fba2ae Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 14 Jun 2022 15:21:22 -0500 Subject: [PATCH 61/67] Use `python -m unittest` rather than `pytest` There's an error when running `pytest`: ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...] pytest: error: unrecognized arguments: --cov --cov-report --cov-report term-missing --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c79b23..2ceed96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,4 +53,5 @@ jobs: - name: Test with pytest run: | - pytest + python -m unittest + # pytest From e20da48d16368ce15bd0c72cd7708ecca855559b Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 16:18:39 -0500 Subject: [PATCH 62/67] Add tests/data/sdx.json back This file is read by the test suite, so throwing this file away entirely was not a great idea. The test suite also writes to it, thus mutating it after every run, but we can change that. --- tests/data/sdx.json | 1081 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1081 insertions(+) create mode 100644 tests/data/sdx.json diff --git a/tests/data/sdx.json b/tests/data/sdx.json new file mode 100644 index 0000000..c791d75 --- /dev/null +++ b/tests/data/sdx.json @@ -0,0 +1,1081 @@ +{ + "id": "urn:ogf:network:sdx", + "name": "AmLight-OXP", + "domain_service": { + "monitoring_capability": null, + "owner": "FIU", + "private_attributes": null, + "provisioning_system": null, + "provisioning_url": null, + "vendor": null + }, + "version": "2.1", + "time_stamp": "1970-01-01T00:00:00.000000", + "nodes": [ + { + "id": "urn:sdx:node:amlight.net:B1", + "name": "amlight:Novi01", + "short_name": "B1", + "location": { + "address": "Miami", + "latitude": -80.37676058477908, + "longitude": 25.75633040531146 + }, + "ports": [ + { + "id": "urn:sdx:port:amlight:B1:1", + "name": "Novi01:1", + "short_name": null, + "node": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B1:2", + "name": "Novi01:2", + "short_name": null, + "node": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B1:3", + "name": "Novi01:3", + "short_name": null, + "node": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:sdx:node:amlight.net:B2", + "name": "amlight:Novi02", + "short_name": "B2", + "location": { + "address": "BocaRaton", + "latitude": -80.10225977485742, + "longitude": 26.381437356374075 + }, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:B2:1", + "name": "Novi02:1", + "short_name": null, + "node": "B2:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B2:2", + "name": "Novi02:2", + "short_name": null, + "node": "B2:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:B2:3", + "name": "Novi02:3", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:sdx:node:amlight.net:A1", + "name": "amlight:Novi100", + "short_name": "A1", + "location": { + "address": "redclara", + "latitude": -81.66666016473143, + "longitude": 30.34943181039702 + }, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:A1:1", + "name": "Novi100:1", + "short_name": null, + "node": "A1:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:sdx:port:amlight.net:A1:2", + "name": "Novi100:2", + "short_name": null, + "node": "A1:2", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:B1", + "name": "sax:Novi01", + "short_name": "B1", + "location": { + "address": "SaoPaulo", + "latitude": -46.650271781410524, + "longitude": -23.5311561958366 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", + "short_name": null, + "node": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:2", + "name": "Novi01:2", + "short_name": null, + "node": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:3", + "name": "Novi01:3", + "short_name": null, + "node": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4", + "name": "Novi01:3", + "short_name": null, + "node": "B1:4", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:B2", + "name": "sax:Novi02", + "short_name": "B2", + "location": { + "address": "PanamaCity", + "latitude": -79.4947050137491, + "longitude": 8.993040465928525 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B2:1", + "name": "Novi02:1", + "short_name": null, + "node": "B2:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:2", + "name": "Novi02:2", + "short_name": null, + "node": "B2:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:3", + "name": "Novi02:3", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:4", + "name": "Novi02:4", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:B3", + "name": "sax:Novi03", + "short_name": "B3", + "location": { + "address": "Fortaleza", + "latitude": -38.52443289673026, + "longitude": -3.73163824920348 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:1", + "name": "Novi02:3", + "short_name": null, + "node": "B3:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B3:2", + "name": "Novi02:3", + "short_name": null, + "node": "B3:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:B3:3", + "name": "Novi03:3", + "short_name": null, + "node": "B3:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:sax:A1", + "name": "sax:Novi100", + "short_name": "A1", + "location": { + "address": "Santiago", + "latitude": -70.64634765264213, + "longitude": -33.4507049233331 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:1", + "name": "Novi100:1", + "short_name": null, + "node": "A1:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:sax:A1:2", + "name": "Novi100:2", + "short_name": null, + "node": "A1:2", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:B1", + "name": "zaoxi:Novi01", + "short_name": "B1", + "location": { + "address": "Sangano", + "latitude": 13.216709879405311, + "longitude": -9.533459658700743 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", + "name": "Novi01:1", + "short_name": null, + "node": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", + "short_name": null, + "node": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", + "short_name": null, + "node": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:B2", + "name": "zaoxi:Novi02", + "short_name": "B2", + "location": { + "address": "CapeTown", + "latitude": -38.52443289673026, + "longitude": -3.73163824920348 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:1", + "name": "Novi02:1", + "short_name": null, + "node": "B2:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "name": "Novi02:2", + "short_name": null, + "node": "B2:2", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "name": "Novi02:3", + "short_name": null, + "node": "B2:3", + "label_range": [ + "100-200", + "10001" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:node:zaoxi:A1", + "name": "zaoxi:Novi100", + "short_name": "A1", + "location": { + "address": "Karoo", + "latitude": 22.541224555821298, + "longitude": -32.3632301851245 + }, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "name": "Novi100:1", + "short_name": null, + "node": "A1:1", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "name": "Novi100:2", + "short_name": null, + "node": "A1:2", + "label_range": [ + "100-200", + "1000" + ], + "status": null, + "state": null, + "private_attributes": null + } + ], + "private_attributes": null + } + ], + "links": [ + { + "id": "urn:ogf:network:sdx:link:amlight:B1-B2", + "name": "amlight:B1-B2", + "short_name": "Miami-BocaRaton", + "nni": null, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:B1:2", + "name": "Novi01:2", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:sdx:port:amlight.net:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:amlight:A1-B1", + "name": "amlight:A1-B1", + "short_name": "redclara-miami", + "nni": null, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:sdx:node:amlight.net:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:sdx:port:amlight.net:B1:3", + "name": "Novi01:3", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:amlight:A1-B2", + "name": "amlight:A1-B2", + "short_name": "redclara-BocaRaton", + "nni": null, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:sdx:node:amlight.net:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:sdx:port:amlight.net:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:B1-B2", + "name": "sax:B1-B2", + "short_name": "SaoPaulo-Fortaleza", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:node:sax:B1", + "short_name": "B1:2", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:Panama-Fortaleza", + "name": "sax:Panama-Fortaleza", + "short_name": "Panama-Fortaleza", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:4", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:4", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:SanPaolo-Fortaleza", + "name": "nni:SanPaolo-Fortaleza", + "short_name": "BocaRaton-Fortaleza", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi03:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:3", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:4", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:sax:B1", + "short_name": "B1:4", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:A1-B1", + "name": "sax:A1-B1", + "short_name": "redclara-SaoPaulo", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:sax:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:sax:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:sax:A1-B2", + "name": "sax:A1-B2", + "short_name": "redclara-Fortaleza", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:sax:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:nni:Miami-Sanpaolo", + "name": "nni:Miami-Sanpaolo", + "short_name": "Miami-Sanpaolo", + "nni": true, + "ports": [ + { + "id": "urn:sdx:port:amlight:B1:1", + "name": "Novi01:1", + "node": "urn:sdx:node:amlight.net:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:node:sax:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 10, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:nni:BocaRaton-Fortaleza", + "name": "nni:BocaRaton-Fortaleza", + "short_name": "BocaRaton-Fortaleza", + "nni": true, + "ports": [ + { + "id": "urn:sdx:port:amlight.net:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:sdx:node:amlight.net:B2", + "short_name": "B2:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:sax:B2:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:1", + "node": "urn:ogf:network:sdx:node:sax:B2", + "short_name": "B2:1", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 10, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:B1-B2", + "name": "zaoxi:B1-B2", + "short_name": "Sangano-Capetown", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:2", + "name": "Novi01:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:2", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:2", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:2", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B1", + "name": "zaoxi:A1-B1", + "short_name": "Karoo-Sangano", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:1", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:3", + "name": "Novi01:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:3", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:zaoxi:A1-B2", + "name": "zaoxi:A1-B2", + "short_name": "Karoo-Capetown", + "nni": null, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:zaoxi:A1:2", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi100:2", + "node": "urn:ogf:network:sdx:node:zaoxi:A1", + "short_name": "A1:2", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B2:3", + "label_range": [ + "100-200", + "10001" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:zaoxi:B2", + "short_name": "B2:3", + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 5, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + }, + { + "id": "urn:ogf:network:sdx:link:nni:Fortaleza-Sangano", + "name": "nni:Fortaleza-Sangano", + "short_name": "Fortaleza-Sangano", + "nni": true, + "ports": [ + { + "id": "urn:ogf:network:sdx:port:sax:B3:1", + "label_range": [ + "100-200", + "1000" + ], + "name": "Novi02:3", + "node": "urn:ogf:network:sdx:node:sax:B3", + "short_name": "B3:1", + "status": "up" + }, + { + "id": "urn:ogf:network:sdx:port:zaoxi:B1:1", + "name": "Novi01:1", + "node": "urn:ogf:network:sdx:node:zaoxi:B1", + "short_name": "B1:1", + "label_range": [ + "100-200", + "10001" + ], + "status": "up" + } + ], + "bandwidth": 100000, + "residual_bandwidth": 100000, + "latency": 10, + "packet_loss": 59.621339166831824, + "availability": 56.37376656633328, + "status": null, + "state": null, + "private_attributes": null, + "time_stamp": null, + "measurement_period": null + } + ], + "private_attributes": null +} From 20420d51cee4e107f17acfd0d849b8145f5bc32e Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 16:26:25 -0500 Subject: [PATCH 63/67] Do not write to the input file Read from `./tests/data/sdx.json`, but don't write back to it. Rather, write to `./tests/data/sdx-out.json`, which we can possibly ignore. --- tests/test_topology_manager.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_topology_manager.py b/tests/test_topology_manager.py index 0831d23..16042d0 100644 --- a/tests/test_topology_manager.py +++ b/tests/test_topology_manager.py @@ -20,7 +20,8 @@ TOPOLOGY_ZAOXI = './tests/data/zaoxi.json' TOPOLOGY_png = "./tests/data/sdx.png" -TOPOLOGY = "./tests/data/sdx.json" +TOPOLOGY_IN = "./tests/data/sdx.json" +TOPOLOGY_OUT = "./tests/data/sdx-out.json" topology_file_list_3 = [TOPOLOGY_AMLIGHT,TOPOLOGY_ZAOXI,TOPOLOGY_SAX] topology_file_list_update = [TOPOLOGY_ZAOXI] @@ -44,7 +45,7 @@ def testMergeTopology(self): data = json.load(data_file) print("Adding Topology:" + topology_file) self.manager.add_topology(data) - with open(TOPOLOGY, 'w') as t_file: + with open(TOPOLOGY_OUT, 'w') as t_file: json.dump(self.manager.topology.to_dict(), t_file, indent=4) except DataModelException as e: @@ -62,7 +63,7 @@ def testUpdateTopology(self): print("Updating Topology:" + topology_file) self.manager.update_topology(data) - with open(TOPOLOGY, 'w') as t_file: + with open(TOPOLOGY_OUT, 'w') as t_file: json.dump(self.manager.topology.to_dict(), t_file, indent=4) graph = self.manager.generate_graph() #pos = nx.spring_layout(graph, seed=225) # Seed for reproducible layout @@ -104,7 +105,7 @@ def testLinkPropertyUpdate(self): self.testMergeTopology() self.manager.update_link_property(link_id,'latency',8) self.manager.update_link_property(inter_link_id,'latency',8) - with open(TOPOLOGY, 'w') as t_file: + with open(TOPOLOGY_OUT, 'w') as t_file: json.dump(self.manager.topology.to_dict(), t_file, indent=4) except DataModelException as e: print(e) @@ -114,10 +115,10 @@ def testLinkPropertyUpdate(self): def testLinkPropertyUpdateJson(self): print("Test Topology JSON Link Property Update!") try: - with open(TOPOLOGY, 'r', encoding='utf-8') as data_file: + with open(TOPOLOGY_IN, 'r', encoding='utf-8') as data_file: data = json.load(data_file) self.manager.update_element_property_json(data,'links',link_id,'latency',20) - with open(TOPOLOGY, 'w') as t_file: + with open(TOPOLOGY_OUT, 'w') as t_file: json.dump(data, t_file, indent=4) except DataModelException as e: print(e) @@ -127,4 +128,4 @@ def testLinkPropertyUpdateJson(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From b79a04ca3e2cc65c0a00d8ad231825dc94b0ed72 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 16:27:13 -0500 Subject: [PATCH 64/67] Ignore file written by test suite --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ba73e00..8cf4d52 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ venv* # Ignore files generated when running tests. /tests/data/amlight.png /tests/data/sdx.png -/tests/data/sdx.json +/tests/data/sdx-out.json From 644ff97ab6f7be8b9f3fee10814830df1a2e5eb2 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 17:00:34 -0500 Subject: [PATCH 65/67] Run pytest rather than `python -m unittest` --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ceed96..1c7af99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,6 @@ jobs: # exit-zero treats all errors as warnings. flake8 . --count --exit-zero --max-complexity=10 --statistics - - name: Test with pytest + - name: Run tests run: | - python -m unittest - # pytest + pytest From 9d4452f2807f9c375addd3c8d5c153e990a1a1d4 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 17:02:42 -0500 Subject: [PATCH 66/67] Install pytest-cov on CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c7af99..b60d20e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest + pip install flake8 pytest pytest-cov if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - name: Lint with flake8 From d742da2f2e2b525067d49d0bc49319ff0c279886 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 15 Jun 2022 17:11:35 -0500 Subject: [PATCH 67/67] Cache Python packages on CI If we cache the Python packages we use, we will hopefully hammer PyPI.org a little less frequently, and speed up testing. --- .github/workflows/test.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b60d20e..2f84aa5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,21 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - + + # See https://github.com/marketplace/actions/cache + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Set up pip cache + uses: actions/cache@v3 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies run: | python -m pip install --upgrade pip