From 237d3f52963ce50fed3914df993ac114a8661c91 Mon Sep 17 00:00:00 2001 From: hadik3r Date: Mon, 27 Jun 2016 17:57:49 +0200 Subject: [PATCH] Add rest api --- src/emuvim/api/rest/__init__.py | 0 src/emuvim/api/rest/compute.py | 155 ++++++++++++++++++++++ src/emuvim/cli/rest/__init__.py | 0 src/emuvim/cli/rest/compute.py | 135 +++++++++++++++++++ src/emuvim/cli/rest/datacenter.py | 74 +++++++++++ src/emuvim/test/api_base.py | 106 +++++++++++++++ src/emuvim/test/unittests/test_restapi.py | 59 ++++++++ 7 files changed, 529 insertions(+) create mode 100644 src/emuvim/api/rest/__init__.py create mode 100644 src/emuvim/api/rest/compute.py create mode 100644 src/emuvim/cli/rest/__init__.py create mode 100644 src/emuvim/cli/rest/compute.py create mode 100644 src/emuvim/cli/rest/datacenter.py create mode 100644 src/emuvim/test/api_base.py create mode 100644 src/emuvim/test/unittests/test_restapi.py diff --git a/src/emuvim/api/rest/__init__.py b/src/emuvim/api/rest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/emuvim/api/rest/compute.py b/src/emuvim/api/rest/compute.py new file mode 100644 index 0000000..02ca9f2 --- /dev/null +++ b/src/emuvim/api/rest/compute.py @@ -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/") + self.api.add_resource(ComputeStart, "/restapi/compute///start") + self.api.add_resource(ComputeStop, "/restapi/compute///stop") + self.api.add_resource(ComputeStatus, "/restapi/compute//") + self.api.add_resource(DatacenterList, "/restapi/datacenter") + self.api.add_resource(DatacenterStatus, "/restapi/datacenter/") + + 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 index 0000000..e69de29 diff --git a/src/emuvim/cli/rest/compute.py b/src/emuvim/cli/rest/compute.py new file mode 100644 index 0000000..d3ab40e --- /dev/null +++ b/src/emuvim/cli/rest/compute.py @@ -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 index 0000000..b43a445 --- /dev/null +++ b/src/emuvim/cli/rest/datacenter.py @@ -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 index 0000000..54ffde9 --- /dev/null +++ b/src/emuvim/test/api_base.py @@ -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 index 0000000..c395220 --- /dev/null +++ b/src/emuvim/test/unittests/test_restapi.py @@ -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() -- 2.25.1