From 7b38ee1ed94ec3e9124e8e5b9c21cf224d39edc3 Mon Sep 17 00:00:00 2001 From: splietker Date: Wed, 28 Jun 2017 17:24:01 +0200 Subject: [PATCH] Implemented Openstack Neutron SFC API Signed-off-by: Malte Splietker --- src/emuvim/api/openstack/compute.py | 216 ++++++++- .../openstack/openstack_dummies/NeutronSFC.md | 92 ++++ .../openstack_dummies/glance_dummy_api.py | 49 ++- .../openstack_dummies/neutron_dummy_api.py | 62 ++- .../neutron_sfc_dummy_api.py | 412 ++++++++++++++++++ .../openstack_dummies/nova_dummy_api.py | 2 + .../api/openstack/resources/__init__.py | 4 + .../openstack/resources/flow_classifier.py | 52 +++ .../api/openstack/resources/port_chain.py | 71 +++ .../api/openstack/resources/port_pair.py | 23 + .../openstack/resources/port_pair_group.py | 22 + src/emuvim/api/openstack/resources/server.py | 1 + src/emuvim/dcemulator/net.py | 2 +- src/emuvim/dcemulator/node.py | 7 +- src/emuvim/examples/openstack_single_dc.py | 59 +++ src/emuvim/test/unittests/test_openstack.py | 197 +++++++++ 16 files changed, 1259 insertions(+), 12 deletions(-) create mode 100644 src/emuvim/api/openstack/openstack_dummies/NeutronSFC.md create mode 100644 src/emuvim/api/openstack/openstack_dummies/neutron_sfc_dummy_api.py create mode 100644 src/emuvim/api/openstack/resources/flow_classifier.py create mode 100644 src/emuvim/api/openstack/resources/port_chain.py create mode 100644 src/emuvim/api/openstack/resources/port_pair.py create mode 100644 src/emuvim/api/openstack/resources/port_pair_group.py create mode 100644 src/emuvim/examples/openstack_single_dc.py diff --git a/src/emuvim/api/openstack/compute.py b/src/emuvim/api/openstack/compute.py index fd9b1ac..6d4d864 100755 --- a/src/emuvim/api/openstack/compute.py +++ b/src/emuvim/api/openstack/compute.py @@ -1,4 +1,5 @@ from mininet.link import Link + from resources import * from docker import DockerClient import logging @@ -41,6 +42,10 @@ class OpenstackCompute(object): self._images = dict() self.nets = dict() self.ports = dict() + self.port_pairs = dict() + self.port_pair_groups = dict() + self.flow_classifiers = dict() + self.port_chains = dict() self.compute_nets = dict() self.dcli = DockerClient(base_url='unix://var/run/docker.sock') @@ -416,7 +421,8 @@ class OpenstackCompute(object): network.append(network_dict) self.compute_nets[server.name] = network c = self.dc.startCompute(server.name, image=server.image, command=server.command, - network=network, flavor_name=server.flavor) + network=network, flavor_name=server.flavor, + properties=server.properties) server.emulator_compute = c for intf in c.intfs.values(): @@ -645,6 +651,214 @@ class OpenstackCompute(object): for stack in self.stacks.values(): stack.ports.pop(port.name, None) + def create_port_pair(self, name, stack_operation=False): + """ + Creates a new port pair with the given name. Raises an exception when a port pair with the given name already + exists! + + :param name: Name of the new port pair. + :type name: ``str`` + :param stack_operation: Allows the heat parser to create modules without adapting the current emulation. + :type stack_operation: ``bool`` + :return: Returns the created port pair. + :rtype: :class:`openstack.resources.port_pair` + """ + port_pair = self.find_port_pair_by_name_or_id(name) + if port_pair is not None and not stack_operation: + logging.warning("Creating port pair with name %s failed, as it already exists" % name) + raise Exception("Port pair with name %s already exists." % name) + logging.debug("Creating port pair with name %s" % name) + port_pair = PortPair(name) + if not stack_operation: + self.port_pairs[port_pair.id] = port_pair + return port_pair + + def find_port_pair_by_name_or_id(self, name_or_id): + """ + Tries to find the port pair by ID and if this does not succeed then tries to find it via name. + + :param name_or_id: UUID or name of the port pair. + :type name_or_id: ``str`` + :return: Returns the port pair reference if it was found or None + :rtype: :class:`openstack.resources.port_pair` + """ + if name_or_id in self.port_pairs: + return self.port_pairs[name_or_id] + for port_pair in self.port_pairs.values(): + if port_pair.name == name_or_id: + return port_pair + + return None + + def delete_port_pair(self, name_or_id): + """ + Deletes the given port pair. Raises an exception when the port pair was not found! + + :param name_or_id: UUID or name of the port pair. + :type name_or_id: ``str`` + """ + port_pair = self.find_port_pair_by_name_or_id(name_or_id) + if port_pair is None: + raise Exception("Port pair with name or id %s does not exists." % name_or_id) + + self.port_pairs.pop(port_pair.id, None) + + def create_port_pair_group(self, name, stack_operation=False): + """ + Creates a new port pair group with the given name. Raises an exception when a port pair group + with the given name already exists! + + :param name: Name of the new port pair group. + :type name: ``str`` + :param stack_operation: Allows the heat parser to create modules without adapting the current emulation. + :type stack_operation: ``bool`` + :return: Returns the created port pair group . + :rtype: :class:`openstack.resources.port_pair_group` + """ + port_pair_group = self.find_port_pair_group_by_name_or_id(name) + if port_pair_group is not None and not stack_operation: + logging.warning("Creating port pair group with name %s failed, as it already exists" % name) + raise Exception("Port pair group with name %s already exists." % name) + logging.debug("Creating port pair group with name %s" % name) + port_pair_group = PortPairGroup(name) + if not stack_operation: + self.port_pair_groups[port_pair_group.id] = port_pair_group + return port_pair_group + + def find_port_pair_group_by_name_or_id(self, name_or_id): + """ + Tries to find the port pair group by ID and if this does not succeed then tries to find it via name. + + :param name_or_id: UUID or name of the port pair group. + :type name_or_id: ``str`` + :return: Returns the port pair group reference if it was found or None + :rtype: :class:`openstack.resources.port_pair_group` + """ + if name_or_id in self.port_pair_groups: + return self.port_pair_groups[name_or_id] + for port_pair_group in self.port_pair_groups.values(): + if port_pair_group.name == name_or_id: + return port_pair_group + + return None + + def delete_port_pair_group(self, name_or_id): + """ + Deletes the given port pair group. Raises an exception when the port pair group was not found! + + :param name_or_id: UUID or name of the port pair group. + :type name_or_id: ``str`` + """ + port_pair_group = self.find_port_pair_group_by_name_or_id(name_or_id) + if port_pair_group is None: + raise Exception("Port pair with name or id %s does not exists." % name_or_id) + + self.port_pair_groups.pop(port_pair_group.id, None) + + def create_port_chain(self, name, stack_operation=False): + """ + Creates a new port chain with the given name. Raises an exception when a port chain with the given name already + exists! + + :param name: Name of the new port chain + :type name: ``str`` + :param stack_operation: Allows the heat parser to create modules without adapting the current emulation. + :type stack_operation: ``bool`` + :return: Returns the created port chain. + :rtype: :class:`openstack.resources.port_chain.PortChain` + """ + port_chain = self.find_port_chain_by_name_or_id(name) + if port_chain is not None and not stack_operation: + logging.warning("Creating port chain with name %s failed, as it already exists" % name) + raise Exception("Port chain with name %s already exists." % name) + logging.debug("Creating port chain with name %s" % name) + port_chain = PortChain(name) + if not stack_operation: + self.port_chains[port_chain.id] = port_chain + return port_chain + + def find_port_chain_by_name_or_id(self, name_or_id): + """ + Tries to find the port chain by ID and if this does not succeed then tries to find it via name. + + :param name_or_id: UUID or name of the port chain. + :type name_or_id: ``str`` + :return: Returns the port chain reference if it was found or None + :rtype: :class:`openstack.resources.port_chain.PortChain` + """ + if name_or_id in self.port_chains: + return self.port_chains[name_or_id] + for port_chain in self.port_chains.values(): + if port_chain.name == name_or_id: + return port_chain + return None + + def delete_port_chain(self, name_or_id): + """ + Deletes the given port chain. Raises an exception when the port chain was not found! + + :param name_or_id: UUID or name of the port chain. + :type name_or_id: ``str`` + """ + port_chain = self.find_port_chain_by_name_or_id(name_or_id) + port_chain.uninstall(self) + if port_chain is None: + raise Exception("Port chain with name or id %s does not exists." % name_or_id) + + self.port_chains.pop(port_chain.id, None) + + def create_flow_classifier(self, name, stack_operation=False): + """ + Creates a new flow classifier with the given name. Raises an exception when a flow classifier with the given name already + exists! + + :param name: Name of the new flow classifier. + :type name: ``str`` + :param stack_operation: Allows the heat parser to create modules without adapting the current emulation. + :type stack_operation: ``bool`` + :return: Returns the created flow classifier. + :rtype: :class:`openstack.resources.flow_classifier` + """ + flow_classifier = self.find_flow_classifier_by_name_or_id(name) + if flow_classifier is not None and not stack_operation: + logging.warning("Creating flow classifier with name %s failed, as it already exists" % name) + raise Exception("Flow classifier with name %s already exists." % name) + logging.debug("Creating flow classifier with name %s" % name) + flow_classifier = FlowClassifier(name) + if not stack_operation: + self.flow_classifiers[flow_classifier.id] = flow_classifier + return flow_classifier + + def find_flow_classifier_by_name_or_id(self, name_or_id): + """ + Tries to find the flow classifier by ID and if this does not succeed then tries to find it via name. + + :param name_or_id: UUID or name of the flow classifier. + :type name_or_id: ``str`` + :return: Returns the flow classifier reference if it was found or None + :rtype: :class:`openstack.resources.flow_classifier` + """ + if name_or_id in self.flow_classifiers: + return self.flow_classifiers[name_or_id] + for flow_classifier in self.flow_classifiers.values(): + if flow_classifier.name == name_or_id: + return flow_classifier + + return None + + def delete_flow_classifier(self, name_or_id): + """ + Deletes the given flow classifier. Raises an exception when the flow classifier was not found! + + :param name_or_id: UUID or name of the flow classifier. + :type name_or_id: ``str`` + """ + flow_classifier = self.find_flow_classifier_by_name_or_id(name_or_id) + if flow_classifier is None: + raise Exception("Flow classifier with name or id %s does not exists." % name_or_id) + + self.flow_classifiers.pop(flow_classifier.id, None) + def _add_link(self, node_name, ip_address, link_name, net_name): """ Adds a new link between datacenter switch and the node with the given name. diff --git a/src/emuvim/api/openstack/openstack_dummies/NeutronSFC.md b/src/emuvim/api/openstack/openstack_dummies/NeutronSFC.md new file mode 100644 index 0000000..4acd322 --- /dev/null +++ b/src/emuvim/api/openstack/openstack_dummies/NeutronSFC.md @@ -0,0 +1,92 @@ +A complete description of the Service Function Chaining API can be found under +https://docs.openstack.org/developer/networking-sfc/api.html + +### Working Example +This section describes a complete, working example of the SFC API with Sonata. +The following diagram shows the structure of the service chain that will be created. + +``` ++-------+ +------+ +------+ +-------+ +| Host1 | | VNF1 | | VNF2 | | Host2 | ++-------+ +------+ +------+ +-------+ + |p1 p2| |p3 p4| |p5 p6| + | | | | | | PC1 = {{p1, p2}, {p3, p4}, {p5, p6}} + +------>----+ +---->----+ +---->-----+ + | | + ^ v PC2 = {{p6, p1}} + +-------------------<------------------+ +``` +Two port chains, PC1 and PC2, are created. PC1 chains packets from Host1 over VNF1 and VNF2 to Host2. +Both network functions, VNF1 and VNF2, simply forward all packets from ingress to egress. PC2 creates a +direct chain from Host2 to Host1, such that replies can be routed send back. +(Note: Port chains are unidirectional) + +#### Prerequisites +The following python packages required in order for the Openstack CLI commands to work. +``` +sudo pip install python-openstackclient networking-sfc +``` +Also the docker images `ubuntu:trusty` and `sonatanfv/sonata-snort-ids-vnf` have to be locally available +(otherwise Sonata cannot start the containers). + +#### Execution +First, start the DCEmulator: +``` +sudo python src/emuvim/examples/openstack_single_dc.py +``` +It creates a single data center and connects it to an instance of the Openstack API listening on `http://0.0.0.0:6001`. + +Then execute the following script. It starts the containers and sets up the port chains as described above. +Finally a ping from Host1 to Host2 is executed to check the connection established by the port chains. + +``` +export OS_USERNAME="admin" +export OS_PASSWORD="nope" +export OS_PROJECT_NAME="nope" +export OS_AUTH_URL="http://0.0.0.0:6001" + +# 1. Create ports +openstack port create --network default p1 +openstack port create --network default p2 +openstack port create --network default p3 +openstack port create --network default p4 +openstack port create --network default p5 +openstack port create --network default p6 + +# 2. Start servers +openstack server create --image ubuntu:trusty --flavor m1.tiny --port p1 Host1 +openstack server create --image sonatanfv/sonata-snort-ids-vnf --flavor m1.tiny \ + --port p2 --port p3 --property IFIN="p2-0" --property IFOUT="p3-0" VNF1 +openstack server create --image sonatanfv/sonata-snort-ids-vnf --flavor m1.tiny \ + --port p4 --port p5 --property IFIN="p4-0" --property IFOUT="p5-0" VNF2 +openstack server create --image ubuntu:trusty --flavor m1.tiny --port p6 Host2 + +# 3. Create port pairs +openstack sfc port pair create --ingress p1 --egress p2 PP1 +openstack sfc port pair create --ingress p3 --egress p4 PP2 +openstack sfc port pair create --ingress p5 --egress p6 PP3 +openstack sfc port pair create --ingress p6 --egress p1 PP4 # for direct ping reply Host2->Host1 + +# 4. Create port groups +openstack sfc port pair group create --port-pair PP1 --port-pair PP2 --port-pair PP3 PPG1 +openstack sfc port pair group create --port-pair PP4 PPG2 + +# 5. Create port chain +openstack sfc port chain create --port-pair-group PPG1 PC1 +openstack sfc port chain create --port-pair-group PPG2 PC2 + +# 6. Test the port chain +export HOST1_DOCKER=$(openstack server list | grep "Host1" | awk '{print "mn."$4}') +export HOST2_IP=$(openstack port show p6 | grep fixed_ips \ + | awk 'match($4, "\x27,") {print substr($4, 13, RSTART - 13)}') +sudo docker exec -it ${HOST1_DOCKER} ping -c 5 ${HOST2_IP} +``` + +To verify that the port chains actually works, commenting out the creation of either port chain (step 5) +will result in the ping packets not getting through. + +### Unimplemented Features +While all functions of the API are implemented and can be called, some of the internal functionality +remains unimplemented at the moment: +* Updating/deleting port chains (the metadata is updated, but the implemented chain is not) +* FlowClassifiers (can be called, metadata is handles but not actually implemented with the chain) diff --git a/src/emuvim/api/openstack/openstack_dummies/glance_dummy_api.py b/src/emuvim/api/openstack/openstack_dummies/glance_dummy_api.py index 7819fb9..d9ca091 100755 --- a/src/emuvim/api/openstack/openstack_dummies/glance_dummy_api.py +++ b/src/emuvim/api/openstack/openstack_dummies/glance_dummy_api.py @@ -30,6 +30,10 @@ class GlanceDummyApi(BaseOpenstackDummy): "/v1/images/", "/v2/images/", resource_class_kwargs={'api': self}) + self.api.add_resource(GlanceImageByDockerNameApi, + "/v1/images//", + "/v2/images//", + resource_class_kwargs={'api': self}) def _start_flask(self): LOG.info("Starting %s endpoint @ http://%s:%d" % ("GlanceDummyApi", self.ip, self.port)) @@ -112,7 +116,7 @@ class GlanceListImagesApi(Resource): f['virtual_size'] = 1 f['marker'] = None resp['images'].append(f) - c+=1 + c += 1 if c > limit: # ugly hack to stop buggy glance client to do infinite requests break if "marker" in request.args: # ugly hack to fix pageination of openstack client @@ -145,7 +149,7 @@ class GlanceListImagesApi(Resource): img_is_public = True if "public" in body_data.get("visibility") else False img_container_format = body_data.get("container_format") # try to find ID of already existing image (matched by name) - img_id=None + img_id = None for image in self.api.compute.images.values(): if str(img_name) in image.name: img_id = image.id @@ -187,9 +191,22 @@ class GlanceImageByIdApi(Resource): def get(self, id): LOG.debug("API CALL: %s GET" % str(self.__class__.__name__)) - from emuvim.api.heat.openstack_dummies.nova_dummy_api import NovaListImages - nova = NovaListImages(self.api) - return nova.get(id) + try: + resp = dict() + for image in self.api.compute.images.values(): + if image.id == id or image.name == id: + resp['id'] = image.id + resp['name'] = image.name + + return Response(json.dumps(resp), status=200, mimetype="application/json") + + response = Response("Image with id or name %s does not exists." % id, status=404) + response.headers['Access-Control-Allow-Origin'] = '*' + return response + + except Exception as ex: + LOG.exception(u"%s: Could not retrieve image with id %s." % (__name__, id)) + return Response(ex.message, status=500, mimetype='application/json') def put(self, id): LOG.debug("API CALL: %s " % str(self.__class__.__name__)) @@ -197,3 +214,25 @@ class GlanceImageByIdApi(Resource): return None +class GlanceImageByDockerNameApi(Resource): + def __init__(self, api): + self.api = api + + def get(self, owner, container): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + try: + name = "%s/%s" % (owner, container) + if name in self.api.compute.images: + image = self.api.compute.images[name] + resp = dict() + resp['id'] = image.id + resp['name'] = image.name + return Response(json.dumps(resp), status=200, mimetype="application/json") + + response = Response("Image with id or name %s does not exists." % id, status=404) + response.headers['Access-Control-Allow-Origin'] = '*' + return response + + except Exception as ex: + logging.exception(u"%s: Could not retrieve image with id %s." % (__name__, id)) + return Response(ex.message, status=500, mimetype='application/json') diff --git a/src/emuvim/api/openstack/openstack_dummies/neutron_dummy_api.py b/src/emuvim/api/openstack/openstack_dummies/neutron_dummy_api.py index ca1ff3d..9835bcd 100755 --- a/src/emuvim/api/openstack/openstack_dummies/neutron_dummy_api.py +++ b/src/emuvim/api/openstack/openstack_dummies/neutron_dummy_api.py @@ -2,12 +2,12 @@ from flask_restful import Resource from flask import request, Response from emuvim.api.openstack.openstack_dummies.base_openstack_dummy import BaseOpenstackDummy from datetime import datetime +import neutron_sfc_dummy_api as SFC import logging import json import uuid import copy - LOG = logging.getLogger("api.openstack.neutron") @@ -52,6 +52,63 @@ class NeutronDummyApi(BaseOpenstackDummy): self.api.add_resource(NeutronAddFloatingIp, "/v2.0/floatingips.json", "/v2.0/floatingips", resource_class_kwargs={'api': self}) + # Service Function Chaining (SFC) API + self.api.add_resource(SFC.PortPairsCreate, "/v2.0/sfc/port_pairs.json", "/v2.0/sfc/port_pairs", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairsUpdate, "/v2.0/sfc/port_pairs/.json", + "/v2.0/sfc/port_pairs/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairsDelete, "/v2.0/sfc/port_pairs/.json", + "/v2.0/sfc/port_pairs/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairsList, "/v2.0/sfc/port_pairs.json", "/v2.0/sfc/port_pairs", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairsShow, "/v2.0/sfc/port_pairs/.json", + "/v2.0/sfc/port_pairs/", + resource_class_kwargs={'api': self}) + + self.api.add_resource(SFC.PortPairGroupCreate, "/v2.0/sfc/port_pair_groups.json", "/v2.0/sfc/port_pair_groups", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairGroupUpdate, "/v2.0/sfc/port_pair_groups/.json", + "/v2.0/sfc/port_pair_groups/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairGroupDelete, "/v2.0/sfc/port_pair_groups/.json", + "/v2.0/sfc/port_pair_groups/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairGroupList, "/v2.0/sfc/port_pair_groups.json", "/v2.0/sfc/port_pair_groups", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortPairGroupShow, "/v2.0/sfc/port_pair_groups/.json", + "/v2.0/sfc/port_pair_groups/", + resource_class_kwargs={'api': self}) + + self.api.add_resource(SFC.FlowClassifierCreate, "/v2.0/sfc/flow_classifiers.json", "/v2.0/sfc/flow_classifiers", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.FlowClassifierUpdate, "/v2.0/sfc/flow_classifiers/.json", + "/v2.0/sfc/flow_classifiers/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.FlowClassifierDelete, "/v2.0/sfc/flow_classifiers/.json", + "/v2.0/sfc/flow_classifiers/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.FlowClassifierList, "/v2.0/sfc/flow_classifiers.json", "/v2.0/sfc/flow_classifiers", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.FlowClassifierShow, "/v2.0/sfc/flow_classifiers/.json", + "/v2.0/sfc/flow_classifiers/", + resource_class_kwargs={'api': self}) + + self.api.add_resource(SFC.PortChainCreate, "/v2.0/sfc/port_chains.json", "/v2.0/sfc/port_chains", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortChainUpdate, "/v2.0/sfc/port_chains/.json", + "/v2.0/sfc/port_chains/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortChainDelete, "/v2.0/sfc/port_chains/.json", + "/v2.0/sfc/port_chains/", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortChainList, "/v2.0/sfc/port_chains.json", "/v2.0/sfc/port_chains", + resource_class_kwargs={'api': self}) + self.api.add_resource(SFC.PortChainShow, "/v2.0/sfc/port_chains/.json", + "/v2.0/sfc/port_chains/", + resource_class_kwargs={'api': self}) + def _start_flask(self): LOG.info("Starting %s endpoint @ http://%s:%d" % (__name__, self.ip, self.port)) if self.app is not None: @@ -848,7 +905,7 @@ class NeutronAddFloatingIp(Resource): resp["floatingips"] = list() # create a list of floting IP definitions and return it for i in range(100, 110): - ip=dict() + ip = dict() ip["router_id"] = "router_id" ip["description"] = "hardcoded in api" ip["created_at"] = "router_id" @@ -865,7 +922,6 @@ class NeutronAddFloatingIp(Resource): resp["floatingips"].append(ip) return Response(json.dumps(resp), status=200, mimetype='application/json') - def post(self): """ Adds a floating IP to neutron. diff --git a/src/emuvim/api/openstack/openstack_dummies/neutron_sfc_dummy_api.py b/src/emuvim/api/openstack/openstack_dummies/neutron_sfc_dummy_api.py new file mode 100644 index 0000000..93121c3 --- /dev/null +++ b/src/emuvim/api/openstack/openstack_dummies/neutron_sfc_dummy_api.py @@ -0,0 +1,412 @@ +from flask_restful import Resource +from flask import request, Response +import logging +import json +import uuid + +from emuvim.api.openstack.resources.port_chain import PortChain + + +class SFC(Resource): + def __init__(self, api): + self.api = api + + +############################################################################### +# Port Pair +############################################################################### + +class PortPairsCreate(SFC): + def post(self): + logging.debug("API CALL: %s POST" % str(self.__class__.__name__)) + + try: + request_dict = json.loads(request.data).get("port_pair") + name = request_dict["name"] + + ingress_port = self.api.compute.find_port_by_name_or_id(request_dict["ingress"]) + egress_port = self.api.compute.find_port_by_name_or_id(request_dict["egress"]) + + port_pair = self.api.compute.create_port_pair(name) + port_pair.ingress = ingress_port + port_pair.egress = egress_port + if "description" in request_dict: + port_pair.description = request_dict["description"] + if "service_function_parameters" in request_dict: + port_pair.service_function_parameters = request_dict["service_function_parameters"] + + resp = { + "port_pair": port_pair.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=201, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairsUpdate(SFC): + def put(self, pair_id): + logging.debug("API CALL: %s PUT" % str(self.__class__.__name__)) + + try: + request_dict = json.loads(request.data).get("port_pair") + port_pair = self.api.compute.find_port_pair_by_name_or_id(pair_id) + if "name" in request_dict: + port_pair.name = request_dict["name"] + if "description" in request_dict: + port_pair.description = request_dict["description"] + + resp = { + "port_pair": port_pair.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairsDelete(SFC): + def delete(self, pair_id): + logging.debug("API CALL: %s DELETE" % str(self.__class__.__name__)) + try: + self.api.compute.delete_port_pair(pair_id) + + return Response("Port pair %s deleted.\n" % pair_id, status=204, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairsList(SFC): + def get(self): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + try: + port_pair_list = [] + for port_pair in self.api.compute.port_pairs.values(): + port_pair_list.append(port_pair.create_dict(self.api.compute)) + resp = {"port_pairs": port_pair_list} + + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairsShow(SFC): + def get(self, pair_id): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + + try: + port_pair = self.api.compute.find_port_pair_by_name_or_id(pair_id) + resp = { + "port_pair": port_pair.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +############################################################################### +# Port Pair Group +############################################################################### + +class PortPairGroupCreate(SFC): + def post(self): + logging.debug("API CALL: %s POST" % str(self.__class__.__name__)) + + try: + request_dict = json.loads(request.data).get("port_pair_group") + + port_pair_group = self.api.compute.create_port_pair_group(request_dict["name"]) + port_pair_group.port_pairs = request_dict["port_pairs"] + if "description" in request_dict: + port_pair_group.description = request_dict["description"] + if "port_pair_group_parameters" in request_dict: + port_pair_group.port_pair_group_parameters = request_dict["port_pair_group_parameters"] + + resp = { + "port_pair_group": port_pair_group.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=201, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairGroupUpdate(SFC): + def put(self, group_id): + logging.debug("API CALL: %s PUT" % str(self.__class__.__name__)) + + try: + request_dict = json.loads(request.data).get("port_pair_group") + port_pair_group = self.api.compute.find_port_pair_group_by_name_or_id(group_id) + if "name" in request_dict: + port_pair_group.name = request_dict["name"] + if "description" in request_dict: + port_pair_group.description = request_dict["description"] + if "port_pairs" in request_dict: + port_pair_group.port_pairs = request_dict["port_pairs"] + + resp = { + "port_pair_group": port_pair_group.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairGroupDelete(SFC): + def delete(self, group_id): + logging.debug("API CALL: %s DELETE" % str(self.__class__.__name__)) + try: + self.api.compute.delete_port_pair_group(group_id) + + return Response("Port pair group %s deleted.\n" % group_id, status=204, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairGroupList(SFC): + def get(self): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + try: + port_pair_group_list = [] + for port_pair_group in self.api.compute.port_pair_groups.values(): + port_pair_group_list.append(port_pair_group.create_dict(self.api.compute)) + resp = {"port_pair_groups": port_pair_group_list} + + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortPairGroupShow(SFC): + def get(self, group_id): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + + try: + port_pair_group = self.api.compute.find_port_pair_group_by_name_or_id(group_id) + resp = { + "port_pair_group": port_pair_group.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +############################################################################### +# Flow Classifier +############################################################################### + +class FlowClassifierCreate(SFC): + def post(self): + logging.debug("API CALL: %s POST" % str(self.__class__.__name__)) + + try: + request_dict = json.loads(request.data).get("flow_classifier") + + flow_classifier = self.api.compute.create_flow_classifier(request_dict["name"]) + if "description" in request_dict: + flow_classifier.description = request_dict["description"] + if "ethertype" in request_dict: + flow_classifier.ethertype = request_dict["ethertype"] + if "protocol" in request_dict: + flow_classifier.protocol = request_dict["protocol"] + if "source_port_range_min" in request_dict: + flow_classifier.source_port_range_min = request_dict["source_port_range_min"] + if "source_port_range_max" in request_dict: + flow_classifier.source_port_range_max = request_dict["source_port_range_max"] + if "destination_port_range_min" in request_dict: + flow_classifier.destination_port_range_min = request_dict["destination_port_range_min"] + if "destination_port_range_max" in request_dict: + flow_classifier.destination_port_range_max = request_dict["destination_port_range_max"] + if "source_ip_prefix" in request_dict: + flow_classifier.source_ip_prefix = request_dict["source_ip_prefix"] + if "destination_ip_prefix" in request_dict: + flow_classifier.destination_ip_prefix = request_dict["destination_ip_prefix"] + if "logical_source_port" in request_dict: + flow_classifier.logical_source_port = request_dict["logical_source_port"] + if "logical_destination_port" in request_dict: + flow_classifier.logical_destination_port = request_dict["logical_destination_port"] + if "l7_parameters" in request_dict: + flow_classifier.l7_parameters = request_dict["l7_parameters"] + + resp = { + "flow_classifier": flow_classifier.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=201, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class FlowClassifierUpdate(SFC): + def put(self, flow_classifier_id): + logging.debug("API CALL: %s PUT" % str(self.__class__.__name__)) + + try: + request_dict = json.loads(request.data).get("flow_classifier") + flow_classifier = self.api.compute.find_flow_classifier_by_name_or_id(flow_classifier_id) + if "name" in request_dict: + flow_classifier.name = request_dict["name"] + if "description" in request_dict: + flow_classifier.description = request_dict["description"] + + resp = { + "flow_classifier": flow_classifier.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class FlowClassifierDelete(SFC): + def delete(self, flow_classifier_id): + logging.debug("API CALL: %s DELETE" % str(self.__class__.__name__)) + try: + self.api.compute.delete_flow_classifier(flow_classifier_id) + + return Response("Port pair group %s deleted.\n" % flow_classifier_id, status=204, + mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class FlowClassifierList(SFC): + def get(self): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + try: + flow_classifier_list = [] + for flow_classifier in self.api.compute.flow_classifiers.values(): + flow_classifier_list.append(flow_classifier.create_dict(self.api.compute)) + resp = {"flow_classifiers": flow_classifier_list} + + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class FlowClassifierShow(SFC): + def get(self, flow_classifier_id): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + + try: + flow_classifier = self.api.compute.find_flow_classifier_by_name_or_id(flow_classifier_id) + resp = { + "flow_classifier": flow_classifier.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +############################################################################### +# Port Chain +############################################################################### + +class PortChainCreate(SFC): + def post(self): + logging.debug("API CALL: %s POST" % str(self.__class__.__name__)) + + try: + request_dict = json.loads(request.data).get("port_chain") + + port_chain = self.api.compute.create_port_chain(request_dict["name"]) + port_chain.port_pair_groups = request_dict["port_pair_groups"] + if "description" in request_dict: + port_chain.description = request_dict["description"] + if "flow_classifiers" in request_dict: + port_chain.flow_classifiers = request_dict["flow_classifiers"] + if "chain_parameters" in request_dict: + port_chain.chain_parameters = request_dict["chain_parameters"] + + port_chain.install(self.api.compute) + + resp = { + "port_chain": port_chain.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=201, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortChainUpdate(SFC): + def put(self, chain_id): + logging.debug("API CALL: %s PUT" % str(self.__class__.__name__)) + request_dict = json.loads(request.data).get("port_chain") + + port_chain = self.api.compute.find_port_chain_by_name_or_id(chain_id) + if "name" in request_dict: + port_chain.name = request_dict["name"] + if "description" in request_dict: + port_chain.description = request_dict["description"] + if "flow_classfiers" in request_dict: + # TODO: update chain implementation + port_chain.description = request_dict["flow_classifiers"] + if "no_flow_classfiers" in request_dict: + port_chain.description = [] + if "port_pair_groups" in request_dict: + # TODO: update chain implementation + port_chain.port_pair_groups = request_dict["port_pair_groups"] + + port_chain.update() + try: + resp = { + "port_chain": port_chain.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortChainDelete(SFC): + def delete(self, chain_id): + logging.debug("API CALL: %s DELETE" % str(self.__class__.__name__)) + + self.api.compute.delete_port_chain(chain_id) + try: + return Response("Port chain %s deleted.\n" % chain_id, status=204, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortChainList(SFC): + def get(self): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + try: + port_chain_list = [] + for port_chain in self.api.compute.port_chains.values(): + port_chain_list.append(port_chain.create_dict(self.api.compute)) + resp = {"port_chains": port_chain_list} + + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') + + +class PortChainShow(SFC): + def get(self, chain_id): + logging.debug("API CALL: %s GET" % str(self.__class__.__name__)) + + try: + port_chain = self.api.compute.find_port_chain_by_name_or_id(chain_id) + resp = { + "port_chain": port_chain.create_dict(self.api.compute) + } + return Response(json.dumps(resp), status=200, mimetype='application/json') + except Exception as ex: + logging.exception("Neutron SFC: %s Exception." % str(self.__class__.__name__)) + return Response(ex.message, status=500, mimetype='application/json') diff --git a/src/emuvim/api/openstack/openstack_dummies/nova_dummy_api.py b/src/emuvim/api/openstack/openstack_dummies/nova_dummy_api.py index f294617..1d5a9ac 100755 --- a/src/emuvim/api/openstack/openstack_dummies/nova_dummy_api.py +++ b/src/emuvim/api/openstack/openstack_dummies/nova_dummy_api.py @@ -226,6 +226,8 @@ class NovaListServersApi(Resource): server = self.api.compute.create_server(name) server.full_name = str(self.api.compute.dc.label) + "_man_" + server_dict["name"] server.template_name = server_dict["name"] + if "metadata" in server_dict: + server.properties = server_dict["metadata"] for flavor in self.api.compute.flavors.values(): if flavor.id == server_dict.get('flavorRef', ''): diff --git a/src/emuvim/api/openstack/resources/__init__.py b/src/emuvim/api/openstack/resources/__init__.py index ffddf54..fca6c6c 100755 --- a/src/emuvim/api/openstack/resources/__init__.py +++ b/src/emuvim/api/openstack/resources/__init__.py @@ -2,6 +2,10 @@ from instance_flavor import InstanceFlavor from model import Model from net import Net from port import Port +from port_pair import PortPair +from port_pair_group import PortPairGroup +from flow_classifier import FlowClassifier +from port_chain import PortChain from resource import Resource from router import Router from server import Server diff --git a/src/emuvim/api/openstack/resources/flow_classifier.py b/src/emuvim/api/openstack/resources/flow_classifier.py new file mode 100644 index 0000000..dff6638 --- /dev/null +++ b/src/emuvim/api/openstack/resources/flow_classifier.py @@ -0,0 +1,52 @@ +import uuid + + +class FlowClassifier(object): + def __init__(self, name): + self.id = str(uuid.uuid4()) + self.tenant_id = "abcdefghijklmnopqrstuvwxyz123456" + self.name = name + self.description = "" + self.ethertype = "IPv4" + self.protocol = None + self.source_port_range_min = 0 + self.source_port_range_max = 0 + self.destination_port_range_min = 0 + self.destination_port_range_max = 0 + self.source_ip_prefix = None + self.destination_ip_prefix = None + self.logical_source_port = "" + self.logical_destination_port = "" + self.l7_parameters = dict() + + def create_dict(self, compute): + representation = { + "name": self.name, + "tenant_id": self.tenant_id, + "description": self.description, + "id": self.id, + } + if self.ethertype: + representation["ethertype"] = self.ethertype + if self.protocol: + representation["protocol"] = self.protocol + if self.source_port_range_min: + representation["source_port_range_min"] = self.source_port_range_min + if self.source_port_range_max: + representation["source_port_range_max"] = self.source_port_range_max + if self.destination_port_range_min: + representation["destination_port_range_min"] = self.destination_port_range_min + if self.destination_port_range_max: + representation["destination_port_range_max"] = self.destination_port_range_max + if self.source_ip_prefix: + representation["source_ip_prefix"] = self.source_ip_prefix + if self.destination_ip_prefix: + representation["destination_ip_prefix"] = self.destination_ip_prefix + if len(self.logical_source_port): + representation["logical_source_port"] = self.logical_source_port + if len(self.logical_destination_port): + representation["logical_destination_port"] = self.logical_destination_port + if len(self.l7_parameters.items()): + representation["l7_parameters"] = self.l7_parameters + + return representation diff --git a/src/emuvim/api/openstack/resources/port_chain.py b/src/emuvim/api/openstack/resources/port_chain.py new file mode 100644 index 0000000..2e91159 --- /dev/null +++ b/src/emuvim/api/openstack/resources/port_chain.py @@ -0,0 +1,71 @@ +import random +import uuid +import logging + + +class PortChain(object): + def __init__(self, name): + self.id = str(uuid.uuid4()) + self.tenant_id = "abcdefghijklmnopqrstuvwxyz123456" + self.name = name + self.description = "" + self.port_pair_groups = list() + self.flow_classifiers = list() + self.chain_parameters = dict() + + # Cookie for internal identification of installed flows (e.g. to delete them) + self.cookie = random.randint(1, 0xffffffff) + + def create_dict(self, compute): + representation = { + "name": self.name, + "tenant_id": self.tenant_id, + "description": self.description, + "flow_classifiers": self.flow_classifiers, + "port_pair_groups": self.port_pair_groups, + "id": self.id + } + return representation + + def install(self, compute): + for flow_classifier_id in self.flow_classifiers: + flow_classifier = compute.find_flow_classifier_by_name_or_id(flow_classifier_id) + if flow_classifier: + pass + # TODO: for every flow classifier create match and pass it to setChain + + for group_id in self.port_pair_groups: + port_pair_group = compute.find_port_pair_group_by_name_or_id(group_id) + for port_pair_id in port_pair_group.port_pairs: + port_pair = compute.find_port_pair_by_name_or_id(port_pair_id) + + server_ingress = None + server_egress = None + for server in compute.computeUnits.values(): + if port_pair.ingress.name in server.port_names: + server_ingress = server + elif port_pair.egress.name in server.port_names: + server_egress = server + + # TODO: Not sure, if this should throw an error + if not server_ingress: + logging.warn("Neutron SFC: ingress port %s not connected." % str(port_pair.ingress.name)) + continue + if not server_egress: + logging.warn("Neutron SFC: egress port %s not connected." % str(port_pair.egress.name)) + continue + + compute.dc.net.setChain( + server_ingress.name, server_egress.name, + port_pair.ingress.intf_name, port_pair.egress.intf_name, + cmd="add-flow", cookie=self.cookie, priority=10, bidirectional=False, + monitor=False + ) + + def uninstall(self, compute): + # TODO: implement + logging.warn("Removing flows is currently not implemented.") + + def update(self): + # TODO: implement + logging.warn("Updating flows is currently not implemented.") diff --git a/src/emuvim/api/openstack/resources/port_pair.py b/src/emuvim/api/openstack/resources/port_pair.py new file mode 100644 index 0000000..38b983e --- /dev/null +++ b/src/emuvim/api/openstack/resources/port_pair.py @@ -0,0 +1,23 @@ +import uuid + + +class PortPair(object): + def __init__(self, name): + self.id = str(uuid.uuid4()) + self.tenant_id = "abcdefghijklmnopqrstuvwxyz123456" + self.name = name + self.description = "" + self.ingress = None + self.egress = None + self.service_function_parameters = dict() + + def create_dict(self, compute): + representation = { + "name": self.name, + "tenant_id": self.tenant_id, + "description": self.description, + "ingress": self.ingress.id, + "egress": self.egress.id, + "id": self.id + } + return representation diff --git a/src/emuvim/api/openstack/resources/port_pair_group.py b/src/emuvim/api/openstack/resources/port_pair_group.py new file mode 100644 index 0000000..9514689 --- /dev/null +++ b/src/emuvim/api/openstack/resources/port_pair_group.py @@ -0,0 +1,22 @@ +import uuid + + +class PortPairGroup(object): + def __init__(self, name): + self.id = str(uuid.uuid4()) + self.tenant_id = "abcdefghijklmnopqrstuvwxyz123456" + self.name = name + self.description = "" + self.port_pairs = list() + self.port_pair_group_parameters = dict() + + def create_dict(self, compute): + representation = { + "name": self.name, + "tenant_id": self.tenant_id, + "description": self.description, + "port_pairs": self.port_pairs, + "port_pair_group_parameters": self.port_pair_group_parameters, + "id": self.id + } + return representation diff --git a/src/emuvim/api/openstack/resources/server.py b/src/emuvim/api/openstack/resources/server.py index d20a2d4..654521a 100755 --- a/src/emuvim/api/openstack/resources/server.py +++ b/src/emuvim/api/openstack/resources/server.py @@ -7,6 +7,7 @@ class Server(object): self.image = image self.command = command self.port_names = list() + self.properties = dict() self.flavor = flavor self.son_emu_command = None self.emulator_compute = None diff --git a/src/emuvim/dcemulator/net.py b/src/emuvim/dcemulator/net.py index 7efb3f0..faa090b 100755 --- a/src/emuvim/dcemulator/net.py +++ b/src/emuvim/dcemulator/net.py @@ -568,7 +568,7 @@ class DCNetwork(Containernet): pass - cmd = kwargs.get('cmd') + cmd = kwargs.get('cmd', 'add-flow') if cmd == 'add-flow' or cmd == 'del-flows': ret = self._chainAddFlow(vnf_src_name, vnf_dst_name, vnf_src_interface, vnf_dst_interface, **kwargs) if kwargs.get('bidirectional'): diff --git a/src/emuvim/dcemulator/node.py b/src/emuvim/dcemulator/node.py index 18febb4..8612096 100755 --- a/src/emuvim/dcemulator/node.py +++ b/src/emuvim/dcemulator/node.py @@ -208,7 +208,7 @@ class Datacenter(object): def start(self): pass - def startCompute(self, name, image=None, command=None, network=None, flavor_name="tiny", **params): + def startCompute(self, name, image=None, command=None, network=None, flavor_name="tiny", properties=dict(), **params): """ Create a new container as compute resource and connect it to this data center. @@ -217,6 +217,7 @@ class Datacenter(object): :param command: command (string) :param network: networks list({"ip": "10.0.0.254/8"}, {"ip": "11.0.0.254/24"}) :param flavor_name: name of the flavor for this compute container + :param properties: dictionary of properties (key-value) that will be passed as environment variables :return: """ assert name is not None @@ -240,6 +241,8 @@ class Datacenter(object): params['cpu_period'] = self.net.cpu_period params['cpu_quota'] = self.net.cpu_period * float(cpu_percentage) + env = properties + properties['VNF_NAME'] = name # create the container d = self.net.addDocker( "%s" % (name), @@ -247,7 +250,7 @@ class Datacenter(object): dcmd=command, datacenter=self, flavor_name=flavor_name, - environment = {'VNF_NAME':name}, + environment = env, **params ) diff --git a/src/emuvim/examples/openstack_single_dc.py b/src/emuvim/examples/openstack_single_dc.py new file mode 100644 index 0000000..b12079d --- /dev/null +++ b/src/emuvim/examples/openstack_single_dc.py @@ -0,0 +1,59 @@ +""" +Copyright (c) 2015 SONATA-NFV +ALL RIGHTS RESERVED. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION] +nor the names of its contributors may be used to endorse or promote +products derived from this software without specific prior written +permission. + +This work has been performed in the framework of the SONATA project, +funded by the European Commission under Grant number 671517 through +the Horizon 2020 and 5G-PPP programmes. The authors would like to +acknowledge the contributions of their colleagues of the SONATA +partner consortium (www.sonata-nfv.eu). +""" +import logging +from mininet.log import setLogLevel +from emuvim.dcemulator.net import DCNetwork + +from emuvim.api.openstack.openstack_api_endpoint import OpenstackApiEndpoint + +logging.basicConfig(level=logging.INFO) + + +def create_topology(): + net = DCNetwork(monitor=False, enable_learning=False) + + dc1 = net.addDatacenter("dc1") + + api1 = OpenstackApiEndpoint("0.0.0.0", 6001) + api1.connect_datacenter(dc1) + api1.start() + api1.connect_dc_network(net) + + net.start() + net.CLI() + # when the user types exit in the CLI, we stop the emulator + net.stop() + + +def main(): + setLogLevel('info') # set Mininet loglevel + create_topology() + + +if __name__ == '__main__': + main() diff --git a/src/emuvim/test/unittests/test_openstack.py b/src/emuvim/test/unittests/test_openstack.py index 6c2bf5a..6ed7efe 100755 --- a/src/emuvim/test/unittests/test_openstack.py +++ b/src/emuvim/test/unittests/test_openstack.py @@ -1166,6 +1166,203 @@ class testRestApi(ApiBaseOpenStack): self.assertEqual(getintfs.status_code, 202) print(" ") + def testNeutronSFC(self): + """ + Tests the Neutron Service Function Chaining implementation. As Some functions build up on others, a + complete environment is created here: + + Ports: p1, p2, p3, p4 + Port Pairs: pp1(p1, p2), pp2(p3, p4) + Port Pair Groups: ppg1(pp1, pp2) + Flow Classifiers: fc1 + Port Chain: pc1(ppg1, fc1) + """ + + headers = {'Content-type': 'application/json'} + + print('->>>>>>> Create ports p1 - p4 ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + # Get network id + network_resp = requests.get("http://0.0.0.0:19696/v2.0/networks?name=default", headers=headers) + self.assertEqual(network_resp.status_code, 200) + network_id = json.loads(network_resp.content)["networks"][0]["id"] + + url = "http://0.0.0.0:19696/v2.0/ports" + port_request = '{"port": {"name": "%s", "network_id": "%s"}}' + p1_resp = requests.post(url, data=port_request % ("p1", network_id), headers=headers) + self.assertEqual(p1_resp.status_code, 201) + p2_resp = requests.post(url, data=port_request % ("p2", network_id), headers=headers) + self.assertEqual(p2_resp.status_code, 201) + p3_resp = requests.post(url, data=port_request % ("p3", network_id), headers=headers) + self.assertEqual(p3_resp.status_code, 201) + p4_resp = requests.post(url, data=port_request % ("p4", network_id), headers=headers) + self.assertEqual(p4_resp.status_code, 201) + + p1_id = json.loads(p1_resp.content)["port"]["id"] + p2_id = json.loads(p2_resp.content)["port"]["id"] + p3_id = json.loads(p3_resp.content)["port"]["id"] + p4_id = json.loads(p4_resp.content)["port"]["id"] + + print('->>>>>>> test Neutron SFC Port Pair Create ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pairs" + pp1_resp = requests.post(url, data='{"port_pair": {"name": "pp1", "ingress": "%s", "egress": "%s"}}' % (p1_id, p2_id), headers=headers) + self.assertEqual(pp1_resp.status_code, 201) + pp2_resp = requests.post(url, data='{"port_pair": {"name": "pp2", "ingress": "%s", "egress": "%s"}}' % (p3_id, p4_id), headers=headers) + self.assertEqual(pp2_resp.status_code, 201) + pp3_resp = requests.post(url, data='{"port_pair": {"name": "pp3", "ingress": "%s", "egress": "%s"}}' % (p3_id, p4_id), headers=headers) + self.assertEqual(pp3_resp.status_code, 201) + + pp1_id = json.loads(pp1_resp.content)["port_pair"]["id"] + pp2_id = json.loads(pp2_resp.content)["port_pair"]["id"] + pp3_id = json.loads(pp3_resp.content)["port_pair"]["id"] + + print('->>>>>>> test Neutron SFC Port Pair Update ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pairs/%s" % pp3_id + pp3_update_resp = requests.put(url, data='{"port_pair": {"description": "port_pair_update"}}', headers=headers) + self.assertEqual(pp3_update_resp.status_code, 200) + self.assertEqual(json.loads(pp3_update_resp.content)["port_pair"]["description"], "port_pair_update") + + print('->>>>>>> test Neutron SFC Port Pair Delete ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pairs/%s" % pp3_id + pp3_delete_resp = requests.delete(url, headers=headers) + self.assertEqual(pp3_delete_resp.status_code, 204) + + print('->>>>>>> test Neutron SFC Port Pair List ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pairs" + pp_list_resp = requests.get(url, headers=headers) + self.assertEqual(pp_list_resp.status_code, 200) + self.assertEqual(len(json.loads(pp_list_resp.content)["port_pairs"]), 2) # only pp1 and pp2 should be left + + print('->>>>>>> test Neutron SFC Port Pair Show ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pairs/%s" % pp2_id + pp2_show_resp = requests.get(url, headers=headers) + self.assertEqual(pp2_show_resp.status_code, 200) + self.assertEqual(json.loads(pp2_show_resp.content)["port_pair"]["name"], "pp2") + + + print('->>>>>>> test Neutron SFC Port Pair Group Create ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pair_groups" + ppg1_resp = requests.post(url, data='{"port_pair_group": {"name": "ppg1", "port_pairs": ["%s"]}}' % (pp1_id), headers=headers) + self.assertEqual(ppg1_resp.status_code, 201) + ppg2_resp = requests.post(url, data='{"port_pair_group": {"name": "ppg2", "port_pairs": ["%s"]}}' % (pp2_id), headers=headers) + self.assertEqual(ppg2_resp.status_code, 201) + ppg3_resp = requests.post(url, data='{"port_pair_group": {"name": "ppg3", "port_pairs": ["%s"]}}' % (pp2_id), headers=headers) + self.assertEqual(ppg3_resp.status_code, 201) + + ppg1_id = json.loads(ppg1_resp.content)["port_pair_group"]["id"] + ppg2_id = json.loads(ppg2_resp.content)["port_pair_group"]["id"] + ppg3_id = json.loads(ppg3_resp.content)["port_pair_group"]["id"] + + print('->>>>>>> test Neutron SFC Port Pair Group Update ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pair_groups/%s" % ppg3_id + ppg3_update_resp = requests.put(url, data='{"port_pair_group": {"description": "port_pair_group_update"}}', headers=headers) + self.assertEqual(ppg3_update_resp.status_code, 200) + self.assertEqual(json.loads(ppg3_update_resp.content)["port_pair_group"]["description"], "port_pair_group_update") + + print('->>>>>>> test Neutron SFC Port Pair Group Delete ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pair_groups/%s" % ppg3_id + ppg3_delete_resp = requests.delete(url, headers=headers) + self.assertEqual(ppg3_delete_resp.status_code, 204) + + print('->>>>>>> test Neutron SFC Port Pair Group List ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pair_groups" + ppg_list_resp = requests.get(url, headers=headers) + self.assertEqual(ppg_list_resp.status_code, 200) + self.assertEqual(len(json.loads(ppg_list_resp.content)["port_pair_groups"]), 2) # only ppg1 and ppg2 should be left + + print('->>>>>>> test Neutron SFC Port Pair Group Show ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_pair_groups/%s" % ppg2_id + ppg2_show_resp = requests.get(url, headers=headers) + self.assertEqual(ppg2_show_resp.status_code, 200) + self.assertEqual(json.loads(ppg2_show_resp.content)["port_pair_group"]["name"], "ppg2") + + + print('->>>>>>> test Neutron SFC Flow Classifier Create ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/flow_classifiers" + fc1_resp = requests.post(url, data='{"flow_classifier": {"name": "fc1", "source_port_range_min": 22, "source_port_range_max": 4000}}', headers=headers) + self.assertEqual(fc1_resp.status_code, 201) + fc2_resp = requests.post(url, data='{"flow_classifier": {"name": "fc2", "source_port_range_min": 22, "source_port_range_max": 4000}}', headers=headers) + self.assertEqual(fc2_resp.status_code, 201) + + fc1_id = json.loads(fc1_resp.content)["flow_classifier"]["id"] + fc2_id = json.loads(fc2_resp.content)["flow_classifier"]["id"] + + print('->>>>>>> test Neutron SFC Flow Classifier Update ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/flow_classifiers/%s" % fc2_id + fc2_update_resp = requests.put(url, data='{"flow_classifier": {"description": "flow_classifier_update"}}', headers=headers) + self.assertEqual(fc2_update_resp.status_code, 200) + self.assertEqual(json.loads(fc2_update_resp.content)["flow_classifier"]["description"], "flow_classifier_update") + + print('->>>>>>> test Neutron SFC Flow Classifier Delete ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/flow_classifiers/%s" % fc2_id + fc2_delete_resp = requests.delete(url, headers=headers) + self.assertEqual(fc2_delete_resp.status_code, 204) + + print('->>>>>>> test Neutron SFC Flow Classifier List ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/flow_classifiers" + fc_list_resp = requests.get(url, headers=headers) + self.assertEqual(fc_list_resp.status_code, 200) + self.assertEqual(len(json.loads(fc_list_resp.content)["flow_classifiers"]), 1) # only fc1 + + print('->>>>>>> test Neutron SFC Flow Classifier Show ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/flow_classifiers/%s" % fc1_id + fc1_show_resp = requests.get(url, headers=headers) + self.assertEqual(fc1_show_resp.status_code, 200) + self.assertEqual(json.loads(fc1_show_resp.content)["flow_classifier"]["name"], "fc1") + + + print('->>>>>>> test Neutron SFC Port Chain Create ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_chains" + pc1_resp = requests.post(url, data='{"port_chain": {"name": "pc1", "port_pair_groups": ["%s"], "flow_classifiers": ["%s"]}}' % (ppg1_id, fc1_id), headers=headers) + self.assertEqual(pc1_resp.status_code, 201) + pc2_resp = requests.post(url, data='{"port_chain": {"name": "pc2", "port_pair_groups": ["%s"], "flow_classifiers": ["%s"]}}' % (ppg1_id, fc1_id), headers=headers) + self.assertEqual(pc2_resp.status_code, 201) + + pc1_id = json.loads(pc1_resp.content)["port_chain"]["id"] + pc2_id = json.loads(pc2_resp.content)["port_chain"]["id"] + + print('->>>>>>> test Neutron SFC Port Chain Update ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_chains/%s" % pc2_id + pc2_update_resp = requests.put(url, data='{"port_chain": {"description": "port_chain_update"}}', headers=headers) + self.assertEqual(pc2_update_resp.status_code, 200) + self.assertEqual(json.loads(pc2_update_resp.content)["port_chain"]["description"], "port_chain_update") + + print('->>>>>>> test Neutron SFC Port Chain Delete ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_chains/%s" % pc2_id + pc2_delete_resp = requests.delete(url, headers=headers) + self.assertEqual(pc2_delete_resp.status_code, 204) + + print('->>>>>>> test Neutron SFC Port Chain List ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_chains" + pc_list_resp = requests.get(url, headers=headers) + self.assertEqual(pc_list_resp.status_code, 200) + self.assertEqual(len(json.loads(pc_list_resp.content)["port_chains"]), 1) # only pc1 + + print('->>>>>>> test Neutron SFC Port Chain Show ->>>>>>>>>>>>>>>') + print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + url = "http://0.0.0.0:19696/v2.0/sfc/port_chains/%s" % pc1_id + pc1_show_resp = requests.get(url, headers=headers) + self.assertEqual(pc1_show_resp.status_code, 200) + self.assertEqual(json.loads(pc1_show_resp.content)["port_chain"]["name"], "pc1") if __name__ == '__main__': unittest.main() -- 2.25.1