Add rest api
authorhadik3r <hadi.razzaghi@uni-paderborn.de>
Mon, 27 Jun 2016 15:57:49 +0000 (17:57 +0200)
committerhadik3r <hadi.razzaghi@uni-paderborn.de>
Mon, 27 Jun 2016 15:57:49 +0000 (17:57 +0200)
src/emuvim/api/rest/__init__.py [new file with mode: 0644]
src/emuvim/api/rest/compute.py [new file with mode: 0644]
src/emuvim/cli/rest/__init__.py [new file with mode: 0644]
src/emuvim/cli/rest/compute.py [new file with mode: 0644]
src/emuvim/cli/rest/datacenter.py [new file with mode: 0644]
src/emuvim/test/api_base.py [new file with mode: 0644]
src/emuvim/test/unittests/test_restapi.py [new file with mode: 0644]

diff --git a/src/emuvim/api/rest/__init__.py b/src/emuvim/api/rest/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/emuvim/api/rest/compute.py b/src/emuvim/api/rest/compute.py
new file mode 100644 (file)
index 0000000..02ca9f2
--- /dev/null
@@ -0,0 +1,155 @@
+import logging
+import threading
+from flask import Flask, request
+from flask_restful import Resource,Api
+import json
+
+
+
+logging.basicConfig(level=logging.INFO)
+
+
+dcs = {}
+
+class RestApiEndpoint(object):
+
+    """
+    Simple API endpoint that offers a REST
+    interface. This interface will be used by the
+    default command line client.
+    """
+    global dcs
+
+    def __init__(self, listenip, port):
+        self.ip = listenip
+        self.port = port
+
+        # setup Flask
+        self.app = Flask(__name__)
+        self.api = Api(self.app)
+
+        # setup endpoints
+        self.api.add_resource(ComputeList, "/restapi/compute/<dc_label>")
+        self.api.add_resource(ComputeStart, "/restapi/compute/<dc_label>/<compute_name>/start")
+        self.api.add_resource(ComputeStop, "/restapi/compute/<dc_label>/<compute_name>/stop")
+        self.api.add_resource(ComputeStatus, "/restapi/compute/<dc_label>/<compute_name>")
+        self.api.add_resource(DatacenterList, "/restapi/datacenter")
+        self.api.add_resource(DatacenterStatus, "/restapi/datacenter/<dc_label>")
+
+        logging.debug("Created API endpoint %s(%s:%d)" % (self.__class__.__name__, self.ip, self.port))
+
+
+    def connectDatacenter(self, dc):
+        dcs[dc.label] = dc
+        logging.info("Connected DC(%s) to API endpoint %s(%s:%d)" % (dc.label, self.__class__.__name__, self.ip, self.port))
+
+    def start(self):
+        thread = threading.Thread(target= self._start_flask, args=())
+        thread.daemon = True
+        thread.start()
+        logging.info("Started API endpoint @ http://%s:%d" % (self.ip, self.port))
+
+
+    def _start_flask(self):
+        self.app.run(self.ip, self.port, debug=True, use_reloader=False)
+
+
+class ComputeStart(Resource):
+    """
+    Start a new compute instance: A docker container (note: zerorpc does not support keyword arguments)
+    :param dc_label: name of the DC
+    :param compute_name: compute container name
+    :param image: image name
+    :param command: command to execute
+    :param network: list of all interface of the vnf, with their parameters (id=id1,ip=x.x.x.x/x),...
+    :return: networks list({"id":"input","ip": "10.0.0.254/8"}, {"id":"output","ip": "11.0.0.254/24"})
+    """
+    global dcs
+
+    def put(self, dc_label, compute_name):
+        logging.debug("API CALL: compute start")
+        try:
+
+            image = json.loads(request.json).get("image")
+            network = json.loads(request.json).get("network")
+            command = json.loads(request.json).get("docker_command")
+            c = dcs.get(dc_label).startCompute(
+                compute_name, image= image, command= command, network= network)
+            # return docker inspect dict
+            return  c. getStatus(), 200
+        except Exception as ex:
+            logging.exception("API error.")
+            return ex.message, 500
+
+class ComputeStop(Resource):
+
+    global dcs
+
+    def get(self, dc_label, compute_name):
+        logging.debug("API CALL: compute stop")
+        try:
+            return dcs.get(dc_label).stopCompute(compute_name), 200
+        except Exception as ex:
+            logging.exception("API error.")
+            return ex.message,500
+
+
+class ComputeList(Resource):
+
+    global dcs
+
+    def get(self, dc_label):
+        logging.debug("API CALL: compute list")
+        try:
+            if dc_label == 'None':
+                # return list with all compute nodes in all DCs
+                all_containers = []
+                for dc in dcs.itervalues():
+                    all_containers += dc.listCompute()
+                return [(c.name, c.getStatus()) for c in all_containers], 200
+            else:
+                # return list of compute nodes for specified DC
+                return [(c.name, c.getStatus())
+                    for c in dcs.get(dc_label).listCompute()], 200
+        except Exception as ex:
+            logging.exception("API error.")
+            return ex.message, 500
+
+
+class ComputeStatus(Resource):
+
+    global dcs
+
+    def get(self, dc_label, compute_name):
+
+        logging.debug("API CALL: compute list")
+
+        try:
+            return dcs.get(dc_label).containers.get(compute_name).getStatus(), 200
+        except Exception as ex:
+            logging.exception("API error.")
+            return ex.message, 500
+
+class DatacenterList(Resource):
+
+    global dcs
+
+    def get(self):
+        logging.debug("API CALL: datacenter list")
+        try:
+            return [d.getStatus() for d in dcs.itervalues()], 200
+        except Exception as ex:
+            logging.exception("API error.")
+            return ex.message, 500
+
+class DatacenterStatus(Resource):
+
+    global dcs
+
+    def get(self, dc_label):
+        logging.debug("API CALL: datacenter status")
+        try:
+            return dcs.get(dc_label).getStatus(), 200
+        except Exception as ex:
+            logging.exception("API error.")
+            return ex.message, 500
diff --git a/src/emuvim/cli/rest/__init__.py b/src/emuvim/cli/rest/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/emuvim/cli/rest/compute.py b/src/emuvim/cli/rest/compute.py
new file mode 100644 (file)
index 0000000..d3ab40e
--- /dev/null
@@ -0,0 +1,135 @@
+from requests import get,put
+from tabulate import tabulate
+import pprint
+import argparse
+import json
+
+pp = pprint.PrettyPrinter(indent=4)
+
+class RestApiClient():
+
+    def __init__(self):
+        self.cmds = {}
+
+    def execute_command(self, args):
+        if getattr(self, args["command"]) is not None:
+            # call the local method with the same name as the command arg
+            getattr(self, args["command"])(args)
+        else:
+            print("Command not implemented.")
+
+    def start(self, args):
+
+        nw_list = list()
+        if args.get("network") is not None:
+            nw_list = self._parse_network(args.get("network"))
+        req = {'image':args.get("image"),
+               'command':args.get("docker_command"),
+               'network':nw_list}
+
+        responce = put("%s/restapi/compute/%s/%s/start" %
+                       (args.get("endpoint"),
+                        args.get("datacenter"),
+                        args.get("name")),
+                       json = json.dumps(req))
+        pp.pprint(responce.json())
+    def stop(self, args):
+
+        responce = get("%s/restapi/compute/%s/%s/stop" %
+                       (args.get("endpoint"),
+                        args.get("datacenter"),
+                        args.get("name")))
+        pp.pprint(responce.json())
+
+    def list(self,args):
+
+        list = get('%s/restapi/compute/%s' % (args.get("endpoint"),args.get('datacenter'))).json()
+
+        table = []
+        for c in list:
+            # for each container add a line to the output table
+            if len(c) > 1:
+                name = c[0]
+                status = c[1]
+                eth0ip = None
+                eth0status = "down"
+                if len(status.get("network")) > 0:
+                    eth0ip = status.get("network")[0].get("ip")
+                    eth0status = "up" if status.get(
+                        "network")[0].get("up") else "down"
+                table.append([status.get("datacenter"),
+                              name,
+                              status.get("image"),
+                              eth0ip,
+                              eth0status,
+                              status.get("state").get("Status")])
+
+        headers = ["Datacenter",
+                   "Container",
+                   "Image",
+                   "eth0 IP",
+                   "eth0 status",
+                   "Status"]
+        print(tabulate(table, headers=headers, tablefmt="grid"))
+
+    def status(self,args):
+
+        list = get("%s/restapi/compute/%s/%s" %
+                   (args.get("endpoint"),
+                    args.get("datacenter"),
+                    args.get("name"))).json()
+        pp.pprint(list)
+
+
+
+    def _parse_network(self, network_str):
+        '''
+        parse the options for all network interfaces of the vnf
+        :param network_str: (id=x,ip=x.x.x.x/x), ...
+        :return: list of dicts [{"id":x,"ip":"x.x.x.x/x"}, ...]
+        '''
+        nw_list = list()
+        networks = network_str[1:-1].split('),(')
+        for nw in networks:
+            nw_dict = dict(tuple(e.split('=')) for e in nw.split(','))
+            nw_list.append(nw_dict)
+
+        return nw_list
+
+
+parser = argparse.ArgumentParser(description='son-emu datacenter')
+parser.add_argument(
+    "command",
+    choices=['start', 'stop', 'list', 'status', 'profile'],
+    help="Action to be executed.")
+parser.add_argument(
+    "--datacenter", "-d", dest="datacenter",
+    help="Data center to which the command should be applied.")
+parser.add_argument(
+    "--name", "-n", dest="name",
+    help="Name of compute instance e.g. 'vnf1'.")
+parser.add_argument(
+    "--image","-i", dest="image",
+    help="Name of container image to be used e.g. 'ubuntu:trusty'")
+parser.add_argument(
+    "--dcmd", "-c", dest="docker_command",
+    help="Startup command of the container e.g. './start.sh'")
+parser.add_argument(
+    "--net", dest="network",
+    help="Network properties of a compute instance e.g. \
+          '(id=input,ip=10.0.10.3/24),(id=output,ip=10.0.10.4/24)' for multiple interfaces.")
+parser.add_argument(
+    "--input", "-in", dest="input",
+    help="input interface of the vnf to profile")
+parser.add_argument(
+    "--output", "-out", dest="output",
+    help="output interface of the vnf to profile")
+parser.add_argument(
+    "--endpoint", "-e", dest="endpoint",
+    default="http://127.0.0.1:5000",
+    help="UUID of the plugin to be manipulated.")
+
+def main(argv):
+    args = vars(parser.parse_args(argv))
+    c = RestApiClient()
+    c.execute_command(args)
\ No newline at end of file
diff --git a/src/emuvim/cli/rest/datacenter.py b/src/emuvim/cli/rest/datacenter.py
new file mode 100644 (file)
index 0000000..b43a445
--- /dev/null
@@ -0,0 +1,74 @@
+from requests import get
+from tabulate import tabulate
+import pprint
+import argparse
+
+pp = pprint.PrettyPrinter(indent=4)
+
+class RestApiClient():
+
+    def __init__(self):
+        self.cmds = {}
+
+    def execute_command(self, args):
+        if getattr(self, args["command"]) is not None:
+            # call the local method with the same name as the command arg
+            getattr(self, args["command"])(args)
+        else:
+            print("Command not implemented.")
+
+    def list(self,args):
+        list = get('%s/restapi/datacenter' % args.get('endpoint')).json()
+        table = []
+        for d in list:
+            # for each dc add a line to the output table
+            if len(d) > 0:
+                table.append([d.get("label"),
+                           d.get("internalname"),
+                           d.get("switch"),
+                           d.get("n_running_containers"),
+                           len(d.get("metadata"))])
+        headers = ["Label",
+               "Internal Name",
+               "Switch",
+               "# Containers",
+               "# Metadata Items"]
+        print (tabulate(table, headers=headers, tablefmt="grid"))
+
+    def status(self,args):
+        list = get('%s/restapi/datacenter/%s' % ( args.get("endpoint"), args.get("datacenter"))).json()
+        table = []
+        table.append([list.get('label'),
+                  list.get('internalname'),
+                  list.get('switch'),
+                  list.get('n_running_containers'),
+                  len(list.get('metadata'))])
+
+        headers = ["Label",
+               "Internal Name",
+               "Switch",
+               "# Containers",
+               "# Metadata Items"]
+
+        print (tabulate(table, headers=headers, tablefmt="grid"))
+
+
+parser = argparse.ArgumentParser(description='son-emu datacenter')
+parser.add_argument(
+    "command",
+    choices=['list', 'status'],
+    help="Action to be executed.")
+parser.add_argument(
+    "--datacenter", "-d", dest="datacenter",
+    help="Data center to which the command should be applied.")
+parser.add_argument(
+    "--endpoint", "-e", dest="endpoint",
+    default="http://127.0.0.1:5000",
+    help="UUID of the plugin to be manipulated.")
+
+
+def main(argv):
+    args = vars(parser.parse_args(argv))
+    c = RestApiClient()
+    c.execute_command(args)
+
diff --git a/src/emuvim/test/api_base.py b/src/emuvim/test/api_base.py
new file mode 100644 (file)
index 0000000..54ffde9
--- /dev/null
@@ -0,0 +1,106 @@
+"""
+Helper module that implements helpers for test implementations.
+"""
+
+import unittest
+import os
+import subprocess
+import docker
+from emuvim.dcemulator.net import DCNetwork
+from emuvim.api.rest.compute import RestApiEndpoint
+from mininet.clean import cleanup
+from mininet.node import Controller
+
+class SimpleTestTopology(unittest.TestCase):
+    """
+        Helper class to do basic test setups.
+        s1 -- s2 -- s3 -- ... -- sN
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.net = None
+        self.api = None
+        self.s = []   # list of switches
+        self.h = []   # list of hosts
+        self.d = []   # list of docker containers
+        self.dc = []  # list of data centers
+        self.docker_cli = None
+        super(SimpleTestTopology, self).__init__(*args, **kwargs)
+
+    def createNet(
+            self,
+            nswitches=0, ndatacenter=0, nhosts=0, ndockers=0,
+            autolinkswitches=False, controller=Controller, **kwargs):
+        """
+        Creates a Mininet instance and automatically adds some
+        nodes to it.
+
+        Attention, we should always use Mininet's default controller
+        for our tests. Only use other controllers if you want to test
+        specific controller functionality.
+        """
+        self.net = DCNetwork(controller=controller, **kwargs)
+        self.api = RestApiEndpoint("127.0.0.1",5000)
+        # add some switches
+        # start from s1 because ovs does not like to have dpid = 0
+        # and switch name-number is being used by mininet to set the dpid
+        for i in range(1, nswitches+1):
+            self.s.append(self.net.addSwitch('s%d' % i))
+        # if specified, chain all switches
+        if autolinkswitches:
+            for i in range(0, len(self.s) - 1):
+                self.net.addLink(self.s[i], self.s[i + 1])
+        # add some data centers
+        for i in range(0, ndatacenter):
+            self.dc.append(
+                self.net.addDatacenter(
+                    'datacenter%d' % i,
+                    metadata={"unittest_dc": i}))
+        # connect data centers to the endpoint
+        for i in range(0, ndatacenter):
+            self.api.connectDatacenter(self.dc[i])
+        # add some hosts
+        for i in range(0, nhosts):
+            self.h.append(self.net.addHost('h%d' % i))
+        # add some dockers
+        for i in range(0, ndockers):
+            self.d.append(self.net.addDocker('d%d' % i, dimage="ubuntu:trusty"))
+
+    def startApi(self):
+        self.api.start()
+
+    def startNet(self):
+        self.net.start()
+
+    def stopNet(self):
+        self.net.stop()
+
+    def getDockerCli(self):
+        """
+        Helper to interact with local docker instance.
+        """
+        if self.docker_cli is None:
+            self.docker_cli = docker.Client(
+                base_url='unix://var/run/docker.sock')
+        return self.docker_cli
+
+    def getContainernetContainers(self):
+        """
+        List the containers managed by containernet
+        """
+        return self.getDockerCli().containers(filters={"label": "com.containernet"})
+
+    @staticmethod
+    def setUp():
+        pass
+
+    @staticmethod
+    def tearDown():
+        cleanup()
+        # make sure that all pending docker containers are killed
+        with open(os.devnull, 'w') as devnull:
+            subprocess.call(
+                "sudo docker rm -f $(sudo docker ps --filter 'label=com.containernet' -a -q)",
+                stdout=devnull,
+                stderr=devnull,
+                shell=True)
\ No newline at end of file
diff --git a/src/emuvim/test/unittests/test_restapi.py b/src/emuvim/test/unittests/test_restapi.py
new file mode 100644 (file)
index 0000000..c395220
--- /dev/null
@@ -0,0 +1,59 @@
+"""
+Test suite to automatically test emulator REST API endpoints.
+"""
+
+import time
+import unittest
+from emuvim.test.api_base import SimpleTestTopology
+import subprocess
+
+
+class testRestApi( SimpleTestTopology ):
+    """
+    Tests to check the REST API endpoints of the emulator.
+    """
+
+    def testRestApi(self):
+
+        # create network
+        self.createNet(nswitches=0, ndatacenter=2, nhosts=2, ndockers=0)
+        # setup links
+        self.net.addLink(self.dc[0], self.h[0])
+        self.net.addLink(self.h[1], self.dc[1])
+        self.net.addLink(self.dc[0], self.dc[1])
+        # start api
+        self.startApi()
+        # start Mininet network
+        self.startNet()
+        print('compute start datacenter0, vnf1 ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli compute start -d datacenter0 -n vnf1", shell=True)
+        print('compute start datacenter0, vnf2 ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli compute start -d datacenter0 -n vnf2", shell=True)
+        print('compute start datacenter1, vnf3 ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli compute start -d datacenter1 -n vnf3", shell=True)
+        print('compute list ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli compute list", shell=True)
+        print('compute stop datacenter0, vnf2 ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli compute stop -d datacenter0 -n vnf2", shell=True)
+        print('compute list ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli compute list", shell=True)
+        print('compute status datacenter0, vnf1 ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli compute status -d datacenter0 -n vnf1", shell=True)
+        print('datacenter list ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli datacenter list", shell=True)
+        print('datacenter status datacenter0 ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        print('->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+        subprocess.call("son-emu-cli datacenter status -d datacenter0", shell=True)
+        self.stopNet()
+
+
+if __name__ == '__main__':
+    unittest.main()