Soft Patch Panel
 help / color / mirror / Atom feed
From: oda@valinux.co.jp
To: spp@dpdk.org
Subject: [spp] [PATCH v3 08/13] spp-ctl: add web API handler
Date: Fri,  5 Oct 2018 10:37:50 +0900	[thread overview]
Message-ID: <20181005013755.19838-9-oda@valinux.co.jp> (raw)
In-Reply-To: <20181005013755.19838-1-oda@valinux.co.jp>

From: Itsuro Oda <oda@valinux.co.jp>

Add WebServer class and handler classes to accept the request and route
for each of SPP processes.

Signed-off-by: Itsuro Oda <oda@valinux.co.jp>
---
 src/spp-ctl/spp_webapi.py | 441 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 441 insertions(+)
 create mode 100644 src/spp-ctl/spp_webapi.py

diff --git a/src/spp-ctl/spp_webapi.py b/src/spp-ctl/spp_webapi.py
new file mode 100644
index 0000000..435c4b7
--- /dev/null
+++ b/src/spp-ctl/spp_webapi.py
@@ -0,0 +1,441 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2018 Nippon Telegraph and Telephone Corporation
+
+import bottle
+import errno
+import json
+import logging
+import netaddr
+import re
+import socket
+import subprocess
+import sys
+
+import spp_proc
+
+
+LOG = logging.getLogger(__name__)
+
+
+class KeyRequired(bottle.HTTPError):
+
+    def __init__(self, key):
+        msg = "key(%s) required." % key
+        super(KeyRequired, self).__init__(400, msg)
+
+
+class KeyInvalid(bottle.HTTPError):
+
+    def __init__(self, key, value):
+        msg = "invalid key(%s): %s." % (key, value)
+        super(KeyRequired, self).__init__(400, msg)
+
+
+class BaseHandler(bottle.Bottle):
+    """Define common methods for each handler."""
+
+    def __init__(self, controller):
+        super(BaseHandler, self).__init__()
+        self.ctrl = controller
+
+        self.default_error_handler = self._error_handler
+        bottle.response.default_status = 404
+
+    def _error_handler(self, res):
+        # use "text/plain" as content_type rather than bottle's default
+        # "html".
+        res.content_type = "text/plain"
+        return res.body
+
+    def _validate_port(self, port):
+        try:
+            if_type, if_num = port.split(":")
+            if if_type not in ["phy", "vhost", "ring"]:
+                raise
+            int(if_num)
+        except:
+            raise KeyInvalid('port', port)
+
+    def log_url(self):
+        LOG.info("%s %s called", bottle.request.method, bottle.request.path)
+
+    def log_response(self):
+        LOG.info("response: %s", bottle.response.status)
+
+    # following three decorators do common works for each API.
+    # each handler 'install' appropriate decorators.
+    #
+    def get_body(self, func):
+        """Get body and set it to method argument.
+        content-type is OK whether application/json or plain text.
+        """
+        def wrapper(*args, **kwargs):
+            req = bottle.request
+            if req.method in ["POST", "PUT"]:
+                if req.get_header('Content-Type') == "application/json":
+                    body = req.json
+                else:
+                    body = json.loads(req.body.read().decode())
+                kwargs['body'] = body
+                LOG.info("body: %s", body)
+            return func(*args, **kwargs)
+        return wrapper
+
+    def check_sec_id(self, func):
+        """Get and check proc and set it to method argument."""
+        def wrapper(*args, **kwargs):
+            sec_id = kwargs.pop('sec_id', None)
+            if sec_id is not None:
+                proc = self.ctrl.procs.get(sec_id)
+                if proc is None or proc.type != self.type:
+                    raise bottle.HTTPError(404,
+                                           "sec_id %d not found." % sec_id)
+                kwargs['proc'] = proc
+            return func(*args, **kwargs)
+        return wrapper
+
+    def make_response(self, func):
+        """Convert plain response to bottle.HTTPResponse."""
+        def wrapper(*args, **kwargs):
+            ret = func(*args, **kwargs)
+            if ret is None:
+                return bottle.HTTPResponse(status=204)
+            else:
+                r = bottle.HTTPResponse(status=200, body=json.dumps(ret))
+                r.content_type = "application/json"
+                return r
+        return wrapper
+
+
+class WebServer(BaseHandler):
+    """Top level handler.
+
+    handlers are hierarchized using 'mount' as follows:
+    /          WebServer
+    /v1          V1Handler
+       /vfs        V1VFHandler
+       /nfvs       V1NFVHandler
+       /primary    V1PrimaryHandler
+    """
+
+    def __init__(self, controller, api_port):
+        super(WebServer, self).__init__(controller)
+        self.api_port = api_port
+
+        self.mount("/v1", V1Handler(controller))
+
+        # request and response logging.
+        self.add_hook("before_request", self.log_url)
+        self.add_hook("after_request", self.log_response)
+
+    def start(self):
+        self.run(server='eventlet', host='localhost', port=self.api_port,
+                 quiet=True)
+
+
+class V1Handler(BaseHandler):
+    def __init__(self, controller):
+        super(V1Handler, self).__init__(controller)
+
+        self.set_route()
+
+        self.mount("/vfs", V1VFHandler(controller))
+        self.mount("/nfvs", V1NFVHandler(controller))
+        self.mount("/primary", V1PrimaryHandler(controller))
+
+        self.install(self.make_response)
+
+    def set_route(self):
+        self.route('/processes', 'GET', callback=self.get_processes)
+
+    def get_processes(self):
+        LOG.info("get processes called.")
+        return self.ctrl.get_processes()
+
+
+class V1VFHandler(BaseHandler):
+
+    def __init__(self, controller):
+        super(V1VFHandler, self).__init__(controller)
+        self.type = spp_proc.TYPE_VF
+
+        self.set_route()
+
+        self.install(self.check_sec_id)
+        self.install(self.get_body)
+        self.install(self.make_response)
+
+    def set_route(self):
+        self.route('/<sec_id:int>', 'GET', callback=self.vf_get)
+        self.route('/<sec_id:int>/components', 'POST',
+                   callback=self.vf_comp_start)
+        self.route('/<sec_id:int>/components/<name>', 'DELETE',
+                   callback=self.vf_comp_stop)
+        self.route('/<sec_id:int>/components/<name>', 'PUT',
+                   callback=self.vf_comp_port)
+        self.route('/<sec_id:int>/classifier_table', 'PUT',
+                   callback=self.vf_classifier)
+
+    def convert_vf_info(self, data):
+        info = data["info"]
+        vf = {}
+        vf["client-id"] = info["client-id"]
+        vf["ports"] = []
+        for key in ["phy", "vhost", "ring"]:
+            for idx in info[key]:
+                vf["ports"].append(key + ":" + str(idx))
+        vf["components"] = info["core"]
+        vf["classifier_table"] = info["classifier_table"]
+
+        return vf
+
+    def vf_get(self, proc):
+        return self.convert_vf_info(proc.get_status())
+
+    def _validate_vf_comp_start(self, body):
+        for key in ['name', 'core', 'type']:
+            if key not in body:
+                raise KeyRequired(key)
+        if not isinstance(body['name'], str):
+            raise KeyInvalid('name', body['name'])
+        if not isinstance(body['core'], int):
+            raise KeyInvalid('core', body['core'])
+        if body['type'] not in ["forward", "merge", "classifier_mac"]:
+            raise KeyInvalid('type', body['type'])
+
+    def vf_comp_start(self, proc, body):
+        self._validate_vf_comp_start(body)
+        proc.start_component(body['name'], body['core'], body['type'])
+
+    def vf_comp_stop(self, proc, name):
+        proc.stop_component(name)
+
+    def _validate_vf_comp_port(self, body):
+        for key in ['action', 'port', 'dir']:
+            if key not in body:
+                raise KeyRequired(key)
+        if body['action'] not in ["attach", "detach"]:
+            raise KeyInvalid('action', body['action'])
+        if body['dir'] not in ["rx", "tx"]:
+            raise KeyInvalid('dir', body['dir'])
+        self._validate_port(body['port'])
+
+        if body['action'] == "attach":
+            vlan = body.get('vlan')
+            if vlan:
+                try:
+                    if vlan['operation'] not in ["none", "add", "del"]:
+                        raise
+                    if vlan['operation'] == "add":
+                        int(vlan['id'])
+                        int(vlan['pcp'])
+                except:
+                    raise KeyInvalid('vlan', vlan)
+
+    def vf_comp_port(self, proc, name, body):
+        self._validate_vf_comp_port(body)
+
+        if body['action'] == "attach":
+            op = "none"
+            vlan_id = 0
+            pcp = 0
+            vlan = body.get('vlan')
+            if vlan:
+                if vlan['operation'] == "add":
+                    op = "add_vlantag"
+                    vlan_id = vlan['id']
+                    pcp = vlan['pcp']
+                elif vlan['operation'] == "del":
+                    op = "del_vlantag"
+            proc.port_add(body['port'], body['dir'],
+                          name, op, vlan_id, pcp)
+        else:
+            proc.port_del(body['port'], body['dir'], name)
+
+    def _validate_mac(self, mac_address):
+        try:
+            netaddr.EUI(mac_address)
+        except:
+            raise KeyInvalid('mac_address', mac_address)
+
+    def _validate_vf_classifier(self, body):
+        for key in ['action', 'type', 'port', 'mac_address']:
+            if key not in body:
+                raise KeyRequired(key)
+        if body['action'] not in ["add", "del"]:
+            raise KeyInvalid('action', body['action'])
+        if body['type'] not in ["mac", "vlan"]:
+            raise KeyInvalid('type', body['type'])
+        self._validate_port(body['port'])
+        self._validate_mac(body['mac_address'])
+
+        if body['type'] == "vlan":
+            try:
+                int(body['vlan'])
+            except:
+                raise KeyInvalid('vlan', body.get('vlan'))
+
+    def vf_classifier(self, proc, body):
+        self._validate_vf_classifier(body)
+
+        port = body['port']
+        mac_address = body['mac_address']
+
+        if body['action'] == "add":
+            if body['type'] == "mac":
+                proc.set_classifier_table(mac_address, port)
+            else:
+                proc.set_classifier_table_with_vlan(
+                    mac_address, port, body['vlan'])
+        else:
+            if body['type'] == "mac":
+                proc.clear_classifier_table(mac_address, port)
+            else:
+                proc.clear_classifier_table_with_vlan(
+                    mac_address, port, body['vlan'])
+
+
+class V1NFVHandler(BaseHandler):
+
+    def __init__(self, controller):
+        super(V1NFVHandler, self).__init__(controller)
+        self.type = spp_proc.TYPE_NFV
+
+        self.set_route()
+
+        self.install(self.check_sec_id)
+        self.install(self.get_body)
+        self.install(self.make_response)
+
+    def set_route(self):
+        self.route('/<sec_id:int>', 'GET', callback=self.nfv_get)
+        self.route('/<sec_id:int>/forward', 'PUT',
+                   callback=self.nfv_forward)
+        self.route('/<sec_id:int>/ports', 'PUT',
+                   callback=self.nfv_port)
+        self.route('/<sec_id:int>/patches', 'PUT',
+                   callback=self.nfv_patch_add)
+        self.route('/<sec_id:int>/patches', 'DELETE',
+                   callback=self.nfv_patch_del)
+
+    def convert_nfv_info(self, data):
+        nfv = {}
+        lines = data.splitlines()
+        if len(lines) < 3:
+            return {}
+        p = re.compile("Client ID (\d+) (\w+)")
+        m = p.match(lines[0])
+        if m:
+            nfv['client_id'] = int(m.group(1))
+            nfv['status'] = m.group(2)
+
+        ports = {}
+        outs = []
+        for line in lines[2:]:
+            if not line.startswith("port_id"):
+                break
+            arg1, _, arg2, arg3 = line.split(",")
+            _, port_id = arg1.split(":")
+            if arg2 == "PHY":
+                port = "phy:" + port_id
+            else:
+                if_type, rest = arg2.split("(")
+                if_num = rest.rstrip(")")
+                if if_type == "RING":
+                    port = "ring:" + if_num
+                elif if_type == "VHOST":
+                    port = "vhost:" + if_num
+                else:
+                    port = if_type + ":" + if_num
+            ports[port_id] = port
+            _, out_id = arg3.split(":")
+            if out_id != "none":
+                outs.append((port_id, out_id))
+        nfv['ports'] = list(ports.values())
+        patches = []
+        if outs:
+            for src_id, dst_id in outs:
+                patches.append({"src": ports[src_id], "dst": ports[dst_id]})
+        nfv['patches'] = patches
+
+        return nfv
+
+    def nfv_get(self, proc):
+        return self.convert_nfv_info(proc.get_status())
+
+    def _validate_nfv_forward(self, body):
+        if 'action' not in body:
+            raise KeyRequired('action')
+        if body['action'] not in ["start", "stop"]:
+            raise KeyInvalid('action', body['action'])
+
+    def nfv_forward(self, proc, body):
+        if body['action'] == "start":
+            proc.forward()
+        else:
+            proc.stop()
+
+    def _validate_nfv_port(self, body):
+        for key in ['action', 'port']:
+            if key not in body:
+                raise KeyRequired(key)
+        if body['action'] not in ["add", "del"]:
+            raise KeyInvalid('action', body['action'])
+        self._validate_port(body['port'])
+
+    def nfv_port(self, proc, body):
+        self._validate_nfv_port(body)
+
+        if_type, if_num = body['port'].split(":")
+        if body['action'] == "add":
+            proc.port_add(if_type, if_num)
+        else:
+            proc.port_del(if_type, if_num)
+
+    def _validate_nfv_patch(self, body):
+        for key in ['src', 'dst']:
+            if key not in body:
+                raise KeyRequired(key)
+        self._validate_port(body['src'])
+        self._validate_port(body['dst'])
+
+    def nfv_patch_add(self, proc, body):
+        self._validate_nfv_patch(body)
+        proc.patch_add(body['src'], body['dst'])
+
+    def nfv_patch_del(self, proc):
+        proc.patch_reset()
+
+
+class V1PrimaryHandler(BaseHandler):
+
+    def __init__(self, controller):
+        super(V1PrimaryHandler, self).__init__(controller)
+
+        self.set_route()
+
+        self.install(self.make_response)
+
+    def set_route(self):
+        self.route('/status', 'GET', callback=self.get_status)
+        self.route('/status', 'DELETE', callback=self.clear_status)
+
+    def _get_proc(self):
+        proc = self.ctrl.procs.get(spp_proc.ID_PRIMARY)
+        if proc is None:
+            raise bottle.HTTPError(404, "primary not found.")
+        return proc
+
+    def convert_status(self, data):
+        # no data returned at the moment.
+        # some data will be returned when the primary becomes to
+        # return statistical information.
+        return {}
+
+    def get_status(self):
+        proc = self._get_proc()
+        return self.convert_status(proc.status())
+
+    def clear_status(self):
+        proc = self._get_proc()
+        proc.clear()
-- 
2.17.1

  parent reply	other threads:[~2018-10-05  1:37 UTC|newest]

Thread overview: 33+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-09-12 23:25 [spp] [PATCH] spp-ctl: SPP controller with Web API Itsuro ODA
2018-09-18 10:00 ` Yasufumi Ogawa
2018-09-18 21:40   ` Itsuro ODA
2018-10-05  1:37 ` [spp] [PATCH v3 00/13] " oda
2018-10-05  1:37   ` [spp] [PATCH v3 01/13] docs: add overview of spp-ctl oda
2018-10-05  1:37   ` [spp] [PATCH v3 02/13] docs: add API reference " oda
2018-10-05  1:37   ` [spp] [PATCH v3 03/13] docs: add index " oda
2018-10-05  1:37   ` [spp] [PATCH v3 04/13] project: add requirements.txt for spp-ctl oda
2018-10-05  1:37   ` [spp] [PATCH v3 05/13] docs: add spp-ctl to index of doc root oda
2018-10-05  1:37   ` [spp] [PATCH v3 06/13] spp-ctl: add entry point oda
2018-10-05  1:37   ` [spp] [PATCH v3 07/13] spp-ctl: add Controller class oda
2018-10-05  1:37   ` oda [this message]
2018-10-05  1:37   ` [spp] [PATCH v3 09/13] spp-ctl: add spp command interfaces oda
2018-10-05  1:37   ` [spp] [PATCH v3 10/13] spp-ctl: update parsing spp_nfv status oda
2018-10-05  1:37   ` [spp] [PATCH v3 11/13] docs: add request examples of spp-ctl oda
2018-10-05  1:37   ` [spp] [PATCH v3 12/13] docs: correct directives " oda
2018-10-05  1:37   ` [spp] [PATCH v3 13/13] docs: add labels and captions for tables oda
2018-10-05  3:57 ` [spp] [PATCH v4 00/14] spp-ctl: SPP controller with Web API oda
2018-10-05  3:57   ` [spp] [PATCH v4 01/14] docs: add overview of spp-ctl oda
2018-10-05  3:57   ` [spp] [PATCH v4 02/14] docs: add API reference " oda
2018-10-05  3:57   ` [spp] [PATCH v4 03/14] docs: add index " oda
2018-10-05  3:57   ` [spp] [PATCH v4 04/14] project: add requirements.txt for spp-ctl oda
2018-10-05  3:57   ` [spp] [PATCH v4 05/14] docs: add spp-ctl to index of doc root oda
2018-10-05  3:57   ` [spp] [PATCH v4 06/14] spp-ctl: add entry point oda
2018-10-05  3:57   ` [spp] [PATCH v4 07/14] spp-ctl: add Controller class oda
2018-10-05  3:57   ` [spp] [PATCH v4 08/14] spp-ctl: add web API handler oda
2018-10-05  3:57   ` [spp] [PATCH v4 09/14] spp-ctl: add spp command interfaces oda
2018-10-05  3:57   ` [spp] [PATCH v4 10/14] spp-ctl: update parsing spp_nfv status oda
2018-10-05  3:57   ` [spp] [PATCH v4 11/14] docs: add request examples of spp-ctl oda
2018-10-05  3:57   ` [spp] [PATCH v4 12/14] docs: correct directives " oda
2018-10-05  3:57   ` [spp] [PATCH v4 13/14] docs: add labels and captions for tables oda
2018-10-05  3:57   ` [spp] [PATCH v4 14/14] spp-ctl: fix incorrect URL oda
2018-10-09  2:01   ` [spp] [PATCH v4 00/14] spp-ctl: SPP controller with Web API Yasufumi Ogawa

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20181005013755.19838-9-oda@valinux.co.jp \
    --to=oda@valinux.co.jp \
    --cc=spp@dpdk.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).