Implemented Openstack Neutron SFC API
authorsplietker <malte.splietker@uni-paderborn.de>
Wed, 28 Jun 2017 15:24:01 +0000 (17:24 +0200)
committersplietker <malte.splietker@uni-paderborn.de>
Wed, 28 Jun 2017 15:24:01 +0000 (17:24 +0200)
Signed-off-by: Malte Splietker <malte.splietker@gmail.com>
16 files changed:
src/emuvim/api/openstack/compute.py
src/emuvim/api/openstack/openstack_dummies/NeutronSFC.md [new file with mode: 0644]
src/emuvim/api/openstack/openstack_dummies/glance_dummy_api.py
src/emuvim/api/openstack/openstack_dummies/neutron_dummy_api.py
src/emuvim/api/openstack/openstack_dummies/neutron_sfc_dummy_api.py [new file with mode: 0644]
src/emuvim/api/openstack/openstack_dummies/nova_dummy_api.py
src/emuvim/api/openstack/resources/__init__.py
src/emuvim/api/openstack/resources/flow_classifier.py [new file with mode: 0644]
src/emuvim/api/openstack/resources/port_chain.py [new file with mode: 0644]
src/emuvim/api/openstack/resources/port_pair.py [new file with mode: 0644]
src/emuvim/api/openstack/resources/port_pair_group.py [new file with mode: 0644]
src/emuvim/api/openstack/resources/server.py
src/emuvim/dcemulator/net.py
src/emuvim/dcemulator/node.py
src/emuvim/examples/openstack_single_dc.py [new file with mode: 0644]
src/emuvim/test/unittests/test_openstack.py

index fd9b1ac..6d4d864 100755 (executable)
@@ -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 (file)
index 0000000..4acd322
--- /dev/null
@@ -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)
index 7819fb9..d9ca091 100755 (executable)
@@ -30,6 +30,10 @@ class GlanceDummyApi(BaseOpenstackDummy):
                               "/v1/images/<id>",
                               "/v2/images/<id>",
                               resource_class_kwargs={'api': self})
+        self.api.add_resource(GlanceImageByDockerNameApi,
+                              "/v1/images/<owner>/<container>",
+                              "/v2/images/<owner>/<container>",
+                              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')
index ca1ff3d..9835bcd 100755 (executable)
@@ -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/<pair_id>.json",
+                              "/v2.0/sfc/port_pairs/<pair_id>",
+                              resource_class_kwargs={'api': self})
+        self.api.add_resource(SFC.PortPairsDelete, "/v2.0/sfc/port_pairs/<pair_id>.json",
+                              "/v2.0/sfc/port_pairs/<pair_id>",
+                              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/<pair_id>.json",
+                              "/v2.0/sfc/port_pairs/<pair_id>",
+                              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/<group_id>.json",
+                              "/v2.0/sfc/port_pair_groups/<group_id>",
+                              resource_class_kwargs={'api': self})
+        self.api.add_resource(SFC.PortPairGroupDelete, "/v2.0/sfc/port_pair_groups/<group_id>.json",
+                              "/v2.0/sfc/port_pair_groups/<group_id>",
+                              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/<group_id>.json",
+                              "/v2.0/sfc/port_pair_groups/<group_id>",
+                              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/<flow_classifier_id>.json",
+                              "/v2.0/sfc/flow_classifiers/<flow_classifier_id>",
+                              resource_class_kwargs={'api': self})
+        self.api.add_resource(SFC.FlowClassifierDelete, "/v2.0/sfc/flow_classifiers/<flow_classifier_id>.json",
+                              "/v2.0/sfc/flow_classifiers/<flow_classifier_id>",
+                              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/<flow_classifier_id>.json",
+                              "/v2.0/sfc/flow_classifiers/<flow_classifier_id>",
+                              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/<chain_id>.json",
+                              "/v2.0/sfc/port_chains/<chain_id>",
+                              resource_class_kwargs={'api': self})
+        self.api.add_resource(SFC.PortChainDelete, "/v2.0/sfc/port_chains/<chain_id>.json",
+                              "/v2.0/sfc/port_chains/<chain_id>",
+                              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/<chain_id>.json",
+                              "/v2.0/sfc/port_chains/<chain_id>",
+                              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 (file)
index 0000000..93121c3
--- /dev/null
@@ -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')
index f294617..1d5a9ac 100755 (executable)
@@ -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', ''):
index ffddf54..fca6c6c 100755 (executable)
@@ -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 (file)
index 0000000..dff6638
--- /dev/null
@@ -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 (file)
index 0000000..2e91159
--- /dev/null
@@ -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 (file)
index 0000000..38b983e
--- /dev/null
@@ -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 (file)
index 0000000..9514689
--- /dev/null
@@ -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
index d20a2d4..654521a 100755 (executable)
@@ -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
index 7efb3f0..faa090b 100755 (executable)
@@ -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'):
index 18febb4..8612096 100755 (executable)
@@ -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 (file)
index 0000000..b12079d
--- /dev/null
@@ -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()
index 6c2bf5a..6ed7efe 100755 (executable)
@@ -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()