Very sorry about the delay, On Thu, 20 Aug 2015 15:59:25 +0800 Yi Tseng <a86487...@gmail.com> wrote:
> 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. dynamic loading and unloading is always tricky (we do the similar inside VRRP app though). Do you have any use case? > 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 I prefer to avoid installing a new command. How about integrating the existing 'ryu' command (ryu/cmd/ryu_base.py)? > 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