Original repo: https://github.com/TakeshiTseng/ryu-dynamic-loader
We developed a ryu application as a plugin and use this plugin to control ryu application manager(AppManager). This plugin allow users to install/uninstall ryu application dynamically without restart ryu-manager. To use this plugin, start ryu application with dal_plugin $ ryu-manager ryu.app.dal_plugin ryu.controller.ofp_handler And start ryu-cli $ ryu-cli Now command line provide 3 commands: 1. list: list all available ryu application from ryu.app module 2. install: install a ryu application by app id 3. uninstall: uninstall a ryu application by app id Contributors: @TakeshiTseng : develop plugin and cli @John-Lin : add error handling and prompt -- Yi Tseng (a.k.a Takeshi) Taiwan National Chiao Tung University Department of Computer Science W2CNLab http://blog.takeshi.tw
From 68360c99d4f0d1ffff6692466629098c023e2ea6 Mon Sep 17 00:00:00 2001 From: Takeshi <a86487...@gmail.com> Date: Thu, 20 Aug 2015 15:53:26 +0800 Subject: [PATCH] Add dynamic application loader for ryu Original repo: https://github.com/TakeshiTseng/ryu-dynamic-loader We developed a ryu application as a plugin and use this plugin to control ryu application manager(AppManager). This plugin allow users to install/uninstall ryu application dynamically without restart ryu-manager. To use this plugin, start ryu application with dal_plugin $ ryu-manager ryu.app.dal_plugin ryu.controller.ofp_handler And start ryu-cli $ ryu-cli Now command line provide 3 commands: 1. list: list all available ryu application from ryu.app module 2. install: install a ryu application by app id 3. uninstall: uninstall a ryu application by app id Contributors: @TakeshiTseng : develop plugin and cli @John-Lin : add error handling and prompt Signed-off-by: Takeshi <a86487...@gmail.com> Signed-off-by: John-Lin <linton...@gmail.com> --- bin/ryu-cli | 19 +++++ ryu/app/dal_plugin.py | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++ ryu/cmd/ryu_cli.py | 174 +++++++++++++++++++++++++++++++++++++++++++ ryu/lib/dal_lib.py | 63 ++++++++++++++++ setup.cfg | 1 + 5 files changed, 460 insertions(+) create mode 100755 bin/ryu-cli create mode 100644 ryu/app/dal_plugin.py create mode 100755 ryu/cmd/ryu_cli.py create mode 100644 ryu/lib/dal_lib.py diff --git a/bin/ryu-cli b/bin/ryu-cli new file mode 100755 index 0000000..61ccfde --- /dev/null +++ b/bin/ryu-cli @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# Copyright (C) 2011, 2012 Nippon Telegraph and Telephone Corporation. +# +# 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. + +from ryu.cmd.ryu_cli import main +main() diff --git a/ryu/app/dal_plugin.py b/ryu/app/dal_plugin.py new file mode 100644 index 0000000..8be1573 --- /dev/null +++ b/ryu/app/dal_plugin.py @@ -0,0 +1,203 @@ +# -*- codeing: utf-8 -*- +import logging +import pkgutil +import inspect + +from ryu import app as ryu_app +from ryu.lib import hub +from ryu.lib.dal_lib import DLController +from ryu.app.wsgi import WSGIApplication +from ryu.base import app_manager +from ryu.base.app_manager import RyuApp, AppManager +from ryu.controller.handler import MAIN_DISPATCHER +from ryu.controller.handler import set_ev_cls + + +LOG = logging.getLogger('DynamicLoader') + +def deep_import(mod_name): + mod = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + +class DynamicLoader(RyuApp): + + def __init__(self, *args, **kwargs): + super(DynamicLoader, self).__init__(*args, **kwargs) + self.ryu_mgr = AppManager.get_instance() + self.available_app = [] + self.init_apps() + wsgi = self.create_wsgi_app('0.0.0.0', 5566) + mapper = wsgi.mapper + wsgi.registory['DLController'] = self + + self.init_mapper(mapper) + + def create_wsgi_app(self, host, port): + wsgi = WSGIApplication() + webapp = hub.WSGIServer((host, port), wsgi) + hub.spawn(webapp.serve_forever) + return wsgi + + + def init_apps(self): + # init all available apps + for _, name, is_pkg in pkgutil.walk_packages(ryu_app.__path__): + LOG.debug( + 'Find %s : %s', + 'package' if is_pkg else 'module', + name) + + if is_pkg: + continue + + try: + _app_module = deep_import('ryu.app.' + name) + + for _attr_name in dir(_app_module): + _attr = getattr(_app_module, _attr_name) + + if inspect.isclass(_attr) and _attr.__bases__[0] == RyuApp: + LOG.debug('\tFind ryu app : %s.%s', + _attr.__module__, + _attr.__name__) + _full_name = '%s.%s' % (_attr.__module__, _attr.__name__) + self.available_app.append((_full_name, _attr)) + + except ImportError: + LOG.debug('Import Error') + + def init_mapper(self, mapper): + mapper.connect('list', '/list', controller=DLController, + action='list_all_apps', + conditions=dict(method=['GET'])) + + mapper.connect('list', '/install', controller=DLController, + action='install_app', + conditions=dict(method=['POST'])) + + mapper.connect('list', '/uninstall', controller=DLController, + action='uninstall_app', + conditions=dict(method=['POST'])) + + def create_context(self, key, cls): + context = None + + if issubclass(cls, RyuApp): + context = self.ryu_mgr._instantiate(None, cls) + else: + context = cls() + + LOG.info('creating context %s', key) + + if key in self.ryu_mgr.contexts: + return None + + self.ryu_mgr.contexts.setdefault(key, context) + return context + + + def list_all_apps(self): + res = [] + installed_apps = self.ryu_mgr.applications + + for app_info in self.available_app: + _cls = app_info[1] + installed_apps_cls =\ + [obj.__class__ for obj in installed_apps.values()] + + if _cls in installed_apps_cls: + res.append({'name': app_info[0], 'installed': True}) + + else: + res.append({'name': app_info[0], 'installed': False}) + + return res + + + def install_app(self, app_id): + try: + app_cls = self.available_app[app_id][1] + app_contexts = app_cls._CONTEXTS + installed_apps = self.ryu_mgr.applications + installed_apps_cls =\ + [obj.__class__ for obj in installed_apps.values()] + + if app_cls in installed_apps_cls: + # app was installed + LOG.debug('Application already installed') + return + new_contexts = [] + + for k in app_contexts: + context_cls = app_contexts[k] + ctx = self.create_context(k, context_cls) + + if ctx and issubclass(context_cls, RyuApp): + new_contexts.append(ctx) + + app = self.ryu_mgr.instantiate(app_cls, **self.ryu_mgr.contexts) + new_contexts.append(app) + + for ctx in new_contexts: + t = ctx.start() + # t should be join to some where? + + except IndexError: + LOG.debug('Can\'t find application with id %d', app_id) + ex = IndexError('Can\'t find application with id %d' % (app_id, )) + raise ex + + except ValueError: + LOG.debug('ryu-app-id must be number') + + except Exception, ex: + LOG.debug('Import error for id: %d', ex.app_id) + raise ex + + + def uninstall_app(self, app_id): + app_info = self.available_app[app_id] + # TODO: such dirty, fix it! + app_name = app_info[0].split('.')[-1] + if app_name not in self.ryu_mgr.applications: + raise ValueError('Can\'t find application') + + app = self.ryu_mgr.applications[app_name] + self.ryu_mgr.uninstantiate(app_name) + app.stop() + + # after we stoped application, chack it context + app_cls = app_info[1] + app_contexts = app_cls._CONTEXTS + installed_apps = self.ryu_mgr.applications + installed_apps_cls =\ + [obj.__class__ for obj in installed_apps.values()] + + for ctx_name in app_contexts: + for app_cls in installed_apps_cls: + if ctx_name in app_cls._CONTEXTS: + break; + + else: + # remove this context + ctx_cls = app_contexts[ctx_name] + ctx = self.ryu_mgr.contexts[ctx_name] + if issubclass(ctx_cls, RyuApp): + ctx.stop() + + if ctx.name in self.ryu_mgr.applications: + del self.ryu_mgr.applications[ctx.name] + + if ctx_name in self.ryu_mgr.contexts: + del self.ryu_mgr.contexts[ctx_name] + + if ctx.name in app_manager.SERVICE_BRICKS: + del app_manager.SERVICE_BRICKS[ctx.name] + + ctx.logger.info('Uninstall app %s successfully', ctx.name) + + # handler hacking, remove all stream handler to avoid it log many times! + ctx.logger.handlers = [] diff --git a/ryu/cmd/ryu_cli.py b/ryu/cmd/ryu_cli.py new file mode 100755 index 0000000..bf371e3 --- /dev/null +++ b/ryu/cmd/ryu_cli.py @@ -0,0 +1,174 @@ +#! /usr/bin/env python +# -*- codeing: utf-8 -*- +from __future__ import print_function + +import cmd +import six +import json + +if six.PY2: + import urllib2 as urllib + +else: + import urllib3 as urllib + +CLI_BASE_URL = 'http://127.0.0.1:5566' +CLI_LIST_PATH = '/list' +CLI_INSTALL_PATH = '/install' +CLI_UNINSTALL_PATH = '/uninstall' + +def http_get(url): + ''' + do http GET method + return python dictionary data + + param: + url: url for http GET, string type + ''' + result = None + + if six.PY2: + try: + response = urllib.urlopen(url) + except urllib.URLError as e: + return e.reason + result = json.load(response) + + else: + http = urllib.PoolManager() + response = http.request('GET', url) + result = json.loads(response.data.decode('utf8')) + + return result + +def http_post(url, req_body): + ''' + do http POST method + return python dictionary data + + param: + url: url for http GET, string type + req_body: data to send, string type + ''' + result = None + + if six.PY2: + try: + response = urllib.urlopen(url, data=req_body) + except urllib.URLError as e: + return e.reason + result = json.load(response) + + else: + http = urllib.PoolManager() + response = http.urlopen('POST', url, body=req_body) + result = json.load(response) + + return result + +class DlCli(cmd.Cmd): + """ + Ryu dynamic loader command line + """ + intro = 'Welcome to the Ryu CLI. Type help or ? to list commands.\n' + prompt = '(ryu-cli) ' + + def do_list(self, line): + ''' + List all available applications. + ''' + + app_list = http_get(CLI_BASE_URL + CLI_LIST_PATH) + + if not type(app_list) == list: + print(app_list) + return False + + app_id = 0 + + for app_info in app_list: + print('[%02d] %s' % (app_id, app_info['name']), end='') + + if app_info['installed']: + print(' [\033[92minstalled\033[0m]') + else: + print('') + + app_id += 1 + + + def do_install(self, line): + ''' + Install ryu application + Usage: + install [app_id] + ''' + try: + app_id = int(line) + except ValueError: + print('Application id must be integer') + return + + req_body = json.dumps({'app_id':app_id}) + result = http_post(CLI_BASE_URL + CLI_INSTALL_PATH, req_body) + + if not type(result) == dict: + print(result) + return + + if result['result'] == 'ok': + print('Install successfully') + + else: + print(result['details']) + + + def do_uninstall(self, line): + ''' + Uninstall ryu application + Usage: + uninstall [app_id] + ''' + try: + app_id = int(line) + except ValueError: + print('Application id must be integer') + return + + req_body = json.dumps({'app_id':app_id}) + result = http_post(CLI_BASE_URL + CLI_UNINSTALL_PATH, req_body) + + if not type(result) == dict: + print(result) + return + + if result['result'] == 'ok': + print('Uninstall successfully') + + else: + print(result['details']) + + def do_exit(self, line): + ''' + Exit from command line + ''' + return True + + def do_EOF(self, line): + ''' + Use ctrl + D to exit + ''' + return True + +if __name__ == '__main__': + ''' + Usage: + ./cli [Base url] + + Base url: RESTful API server base url, default is http://127.0.0.1:5566 + ''' + import sys + if len(sys.argv) >= 2: + CLI_BASE_URL = sys.argv[1] + + DlCli().cmdloop() diff --git a/ryu/lib/dal_lib.py b/ryu/lib/dal_lib.py new file mode 100644 index 0000000..2ca84d3 --- /dev/null +++ b/ryu/lib/dal_lib.py @@ -0,0 +1,63 @@ +# -*- codeing: utf-8 -*- +import logging +import json +from webob import Response +from ryu.app.wsgi import ControllerBase +from ryu.app.wsgi import WSGIApplication + +# REST command template +def rest_command(func): + def _rest_command(*args, **kwargs): + try: + msg = func(*args, **kwargs) + return Response(content_type='application/json', + body=json.dumps(msg)) + + except SyntaxError as e: + details = e.msg + except (ValueError, NameError) as e: + details = e.message + except IndexError as e: + details = e.args[0] + + msg = {'result': 'failure', + 'details': details} + return Response(body=json.dumps(msg)) + + return _rest_command + +LOG = logging.getLogger('DLController') +class DLController(ControllerBase): + + def __init__(self, req, link, data, **config): + super(DLController, self).__init__(req, link, data, **config) + self.ryu_app = data + + @rest_command + def list_all_apps(self, req, **_kwargs): + return self.ryu_app.list_all_apps() + + + @rest_command + def install_app(self, req, **_kwargs): + body = json.loads(req.body) + app_id = int(body['app_id']) + + if app_id < 0: + e = ValueError('app id must grater than 0') + raise e + + self.ryu_app.install_app(app_id) + return {'result': 'ok'} + + @rest_command + def uninstall_app(self, req, **_kwargs): + body = json.loads(req.body) + app_id = int(body['app_id']) + + if app_id < 0: + e = ValueError('app id must grater than 0') + raise e + + self.ryu_app.uninstall_app(app_id) + return {'result': 'ok'} diff --git a/setup.cfg b/setup.cfg index 626e0f8..af91500 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,3 +53,4 @@ setup-hooks = console_scripts = ryu-manager = ryu.cmd.manager:main ryu = ryu.cmd.ryu_base:main + ryu-cli = ryu.cmd.ryu_cli:main -- 2.3.2 (Apple Git-55)
------------------------------------------------------------------------------
_______________________________________________ Ryu-devel mailing list Ryu-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/ryu-devel