Hi We add more features for this patch, this patch provide these 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 4. bricks: Show service bricks from app manager 5. topology: Display topology 2015-08-20 15:59 GMT+08:00 Yi Tseng <a86487...@gmail.com>: > 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 > -- Yi Tseng (a.k.a Takeshi) Taiwan National Chiao Tung University Department of Computer Science W2CNLab http://blog.takeshi.tw
From 2d68b0dd16ee5f7ad92136935a7f3306fc4d9def Mon Sep 17 00:00:00 2001 From: Takeshi <a86487...@gmail.com> Date: Wed, 26 Aug 2015 18:02:53 +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 4. bricks: Show service bricks from app manager 5. topology: Display topology Contributors: @TakeshiTseng : develop plugin and cli @John-Lin : add error handling and prompt Signed-off-by: Takeshi <a86487...@gmail.com> --- bin/ryu-cli | 19 ++++ debian/ryu-bin.install | 1 + ryu/app/dal_plugin.py | 267 +++++++++++++++++++++++++++++++++++++++++++++ ryu/cmd/ryu_cli.py | 291 +++++++++++++++++++++++++++++++++++++++++++++++++ ryu/lib/ascii_topo.py | 90 +++++++++++++++ ryu/lib/dal_lib.py | 77 +++++++++++++ setup.cfg | 1 + 7 files changed, 746 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/ascii_topo.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/debian/ryu-bin.install b/debian/ryu-bin.install index e101cdb..209369c 100644 --- a/debian/ryu-bin.install +++ b/debian/ryu-bin.install @@ -1,4 +1,5 @@ usr/bin/ryu-manager usr/bin usr/bin/ryu usr/bin +usr/bin/ryu-cli usr/bin debian/ryu.conf etc/ryu debian/log.conf etc/ryu diff --git a/ryu/app/dal_plugin.py b/ryu/app/dal_plugin.py new file mode 100644 index 0000000..8f540f1 --- /dev/null +++ b/ryu/app/dal_plugin.py @@ -0,0 +1,267 @@ +# -*- codeing: utf-8 -*- +import logging +import pkgutil +import inspect + +from ryu import app as ryu_app +from ryu.lib import hub +from ryu.app.wsgi import WSGIApplication +from ryu.base import app_manager +from ryu.controller.handler import MAIN_DISPATCHER +from ryu.controller.handler import set_ev_cls +from ryu.base.app_manager import RyuApp, AppManager +from ryu.topology import api as topo_api + +from ryu.lib.dal_lib import DLController + + +_REQUIRED_APP = ['ryu.controller.ofp_handler', 'ryu.topology.switches'] +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' % (_attr.__module__,) + 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', '/installed', controller=DLController, + action='list_installed_app', + conditions=dict(method=['GET'])) + + mapper.connect('list', '/uninstall', controller=DLController, + action='uninstall_app', + conditions=dict(method=['POST'])) + + mapper.connect('list', '/bricks', controller=DLController, + action='report_brick', + conditions=dict(method=['GET'])) + + mapper.connect('list', '/switches', controller=DLController, + action='list_switches', + conditions=dict(method=['GET'])) + + mapper.connect('list', '/links', controller=DLController, + action='list_links', + conditions=dict(method=['GET'])) + + mapper.connect('list', '/hosts', controller=DLController, + action='list_hosts', + conditions=dict(method=['GET'])) + + 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_cls): + 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') + ex = ValueError('Application already installed') + raise ex + + 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? + + def uninstall_app(self, path): + app_cls = self.ryu_mgr.load_app(path) + app = None + + for _app in self.ryu_mgr.applications.values(): + if isinstance(_app, app_cls): + app = _app + break + + else: + raise ValueError('Can\'t find application') + + self.ryu_mgr.uninstantiate(app.name) + app.stop() + + # after we stoped application, chack it context + 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 = [] + + def install_app(self, path): + context_modules = [x.__module__ for x in self.ryu_mgr.contexts_cls.values()] + + if path in context_modules: + ex = ValueError('Application already exist in contexts') + raise ex + + LOG.info('loading app %s', path) + app_cls = self.ryu_mgr.load_app(path) + + if app_cls is None: + ex = ValueError('Can\'t find application module') + raise ex + + self._install_app(app_cls) + + def list_installed_apps(self): + res = [] + installed_apps = self.ryu_mgr.applications.values() + res = [installed_app.__module__ for installed_app in installed_apps] + return res + + def report_brick(self): + res = {} + + for name, app in app_manager.SERVICE_BRICKS.items(): + res.setdefault(name, {'provide': [], 'consume': []}) + for ev_cls, list_ in app.observers.items(): + res[name]['provide'].append((ev_cls.__name__, [capp for capp in list_])) + for ev_cls in app.event_handlers.keys(): + res[name]['consume'].append((ev_cls.__name__)) + + return res + + def list_switches(self): + switches = topo_api.get_all_switch(self) + return [switch.to_dict() for switch in switches] + + def list_links(self): + links = topo_api.get_all_link(self) + return [link.to_dict() for link in links] + + def list_hosts(self): + hosts = topo_api.get_all_host(self) + return [host.to_dict() for host in hosts] diff --git a/ryu/cmd/ryu_cli.py b/ryu/cmd/ryu_cli.py new file mode 100755 index 0000000..baced47 --- /dev/null +++ b/ryu/cmd/ryu_cli.py @@ -0,0 +1,291 @@ +#! /usr/bin/env python +# -*- codeing: utf-8 -*- +from __future__ import print_function + +import cmd +import six +import json + +from ryu.lib.ascii_topo import print_topo + +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_INSTALLED_PATH = '/installed' +CLI_BRICKS_PATH = '/bricks' +CLI_UNINSTALL_PATH = '/uninstall' + +# for topology +CLI_SWITCHES_PATH = '/switches' +CLI_LINKS_PATH = '/links' +CLI_HOSTS_PATH = '/hosts' + + +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 Bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + +class DlCli(cmd.Cmd): + """ + Ryu dynamic loader command line + """ + + _msg = 'Welcome to the Ryu CLI. Type help or ? to list commands.\n' + + _anscii_art = """ + ____ + / __ \__ ____ __ + / /_/ / / / / / / / + / _, _/ /_/ / /_/ / + /_/ |_|\__, /\__,_/ + /____/ + """ + + _hint_msg = """ + \n\nHit '{0}<Tab>{1}' key to auto-complete the commands \ + \nand '{0}<ctrl-d>{1}' or type '{0}exit{1}' to exit Ryu CLI.\n + """.format(Bcolors.BOLD, Bcolors.ENDC) + + intro = (_msg + Bcolors.OKGREEN + _anscii_art + Bcolors.ENDC + + _hint_msg + Bcolors.ENDC) + + prompt = Bcolors.WARNING + 'ryu-cli> ' + Bcolors.ENDC + + def __init__(self): + cmd.Cmd.__init__(self) + self.app_list = None + + def do_list(self, line): + ''' + List all available applications. + ''' + print('{}Available built-in Ryu applications:{}'.format(Bcolors.OKBLUE, Bcolors.ENDC)) + self.app_list = http_get(CLI_BASE_URL + CLI_LIST_PATH) + + if not type(self.app_list) == list: + print(self.app_list) + return False + + for app_info in self.app_list: + + if not app_info['installed']: + print('{}'.format(app_info['name'])) + + print('\n{}Installed Ryu applications:{}'.format(Bcolors.OKBLUE, Bcolors.ENDC)) + installed_list = http_get(CLI_BASE_URL + CLI_INSTALLED_PATH) + + if not type(installed_list) == list: + print(installed_list) + return False + + for install_mod in installed_list: + print('{}'.format(install_mod)) + + def do_install(self, line): + ''' + Install ryu application by using module path + Usage: + install [app path] + Example: + install ryu.app.simple_switch + ''' + req_body = json.dumps({'path': line}) + result = http_post(CLI_BASE_URL + CLI_INSTALL_PATH, req_body) + + if not type(result) == dict: + print(result) + return + + if result['result'] == 'ok': + print('Successfully installed!') + + else: + print(result['details']) + + def complete_install(self, text, line, begidx, endidx): + + self.app_list = http_get(CLI_BASE_URL + CLI_LIST_PATH) + + if not text: + completions = [app_info['name'] for app_info in self.app_list] + + else: + completions = [app_info['name'] + for app_info in self.app_list + if app_info['name'].startswith(text) + ] + + return completions + + def do_uninstall(self, line): + ''' + Uninstall ryu application + Usage: + uninstall [application module path] + Example: + uninstall ryu.app.simple_switch + ''' + req_body = json.dumps({'path': line}) + result = http_post(CLI_BASE_URL + CLI_UNINSTALL_PATH, req_body) + + if not type(result) == dict: + print(result) + return + + if result['result'] == 'ok': + print('Successfully uninstalled!') + + else: + print(result['details']) + + def complete_uninstall(self, text, line, begidx, endidx): + + installed_list = http_get(CLI_BASE_URL + CLI_INSTALLED_PATH) + + if not type(installed_list) == list: + return [] + + if not text: + completions = [installed_mod + for installed_mod in installed_list + ] + + else: + completions = [installed_mod + for installed_mod in installed_list + if installed_mod.startswith(text) + ] + + return completions + + def do_bricks(self, line): + ''' + Show service bricks from app manager + ''' + bricks = http_get(CLI_BASE_URL + CLI_BRICKS_PATH) + + print('{}Bricks:{}'.format(Bcolors.OKBLUE, Bcolors.ENDC)) + for name in bricks: + brick = bricks[name] + print('BRICK {}'.format(name)) + + for provide_ev in brick['provide']: + print('PROVIDES {} to {}'.format(provide_ev[0], provide_ev[1])) + + for consume_ev in brick['consume']: + print('CONSUMES {}'.format(consume_ev)) + + def do_topology(self, line): + ''' + Display current topology + ''' + switches = http_get(CLI_BASE_URL + CLI_SWITCHES_PATH) + + if type(switches) != list: + print('Error to fetching topology data, {}'.format(switches)) + return False + + links = http_get(CLI_BASE_URL + CLI_LINKS_PATH) + hosts = http_get(CLI_BASE_URL + CLI_HOSTS_PATH) + + print_topo(switches, links, hosts) + + def default(self, line): + fail_msg = Bcolors.FAIL + 'Command not found: ' + line + Bcolors.ENDC + print (fail_msg) + + def do_exit(self, line): + ''' + Exit from command line + ''' + return True + + def do_EOF(self, line): + ''' + Use ctrl + D to exit + ''' + return True + + +def main(args=None): + ''' + 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] + + try: + DlCli().cmdloop() + except KeyboardInterrupt: + pass + +if __name__ == '__main__': + main() diff --git a/ryu/lib/ascii_topo.py b/ryu/lib/ascii_topo.py new file mode 100644 index 0000000..4e509b7 --- /dev/null +++ b/ryu/lib/ascii_topo.py @@ -0,0 +1,90 @@ +from __future__ import print_function +import json + +colors = [ + '\033[90m', + '\033[91m', + '\033[92m', + '\033[93m', + '\033[94m', + '\033[95m', + '\033[96m', +] +end_color = '\033[0m' +num_colors = len(colors) + + +def print_topo(switches=[], links=[], hosts=[]): + tlinks = [] + thosts = [] + + for switch in switches: + print('{}┐{:>8}'.format(switch['dpid'], ' '), end='') + + for t in tlinks: + cindex = tlinks.index(t) + if t == None: + print(' ', end='') + else: + print('{}|{}'.format(colors[cindex % num_colors], end_color), end='') + + print() + + for port in switch['ports']: + if port != switch['ports'][-1]: + print('{:>19}{}'.format('├', port['port_no']), end='') + + else: + print('{:>19}{}'.format('└', port['port_no']), end='') + + for link in links: + if port['hw_addr'] == link['src']['hw_addr'] and\ + int(link['src']['dpid'], 16) < int(link['dst']['dpid'], 16): + + if None in tlinks: + nindex = tlinks.index(None) + tlinks[nindex] = link + + else: + tlinks.append(link) + + for host in hosts: + if port['hw_addr'] == host['port']['hw_addr']: + thosts.append(host) + + for t in tlinks: + if len(thosts) > 0: + print('-' * (len(tlinks) + 1), end='') + print('-'.join([th['mac'] for th in thosts]), end='') + thosts = [] + break + + cindex = tlinks.index(t) + src_ports = [l['src'] if l != None else None for l in tlinks] + dst_ports = [l['dst'] if l != None else None for l in tlinks] + + if t == None: + if (port in dst_ports) and dst_ports.index(port) > cindex: + dindex = dst_ports.index(port) + print('{}-{}'.format(colors[dindex % num_colors], end_color), end='') + + elif (port in src_ports) and src_ports.index(port) > cindex: + sindex = src_ports.index(port) + print('{}-{}'.format(colors[sindex % num_colors], end_color), end='') + else: + print(' ', end='') + + elif port['hw_addr'] == t['src']['hw_addr']: + print('{}┐{}'.format(colors[cindex % num_colors], end_color), end='') + + elif port['hw_addr'] == t['dst']['hw_addr']: + print('{}┘{}'.format(colors[cindex % num_colors], end_color), end='') + tlinks[cindex] = None + + while len(tlinks) > 0 and tlinks[-1] == None: + del tlinks[-1] + + else: + print('{}|{}'.format(colors[cindex % num_colors], end_color), end='') + + print() diff --git a/ryu/lib/dal_lib.py b/ryu/lib/dal_lib.py new file mode 100644 index 0000000..4fe34a0 --- /dev/null +++ b/ryu/lib/dal_lib.py @@ -0,0 +1,77 @@ +# -*- codeing: utf-8 -*- +import logging +import json +from webob import Response +from ryu.app.wsgi import ControllerBase +from ryu.app.wsgi import WSGIApplication + +LOG = logging.getLogger('DLController') + + +# 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 + + +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 list_installed_app(self, req, **_kwargs): + return self.ryu_app.list_installed_apps() + + @rest_command + def report_brick(self, req, **_kwargs): + return self.ryu_app.report_brick() + + @rest_command + def install_app(self, req, **_kwargs): + body = json.loads(req.body) + path = body['path'] + + self.ryu_app.install_app(path) + return {'result': 'ok'} + + @rest_command + def uninstall_app(self, req, **_kwargs): + body = json.loads(req.body) + path = body['path'] + + self.ryu_app.uninstall_app(path) + return {'result': 'ok'} + + @rest_command + def list_switches(self, req, **_kwargs): + return self.ryu_app.list_switches() + + @rest_command + def list_links(self, req, **_kwargs): + return self.ryu_app.list_links() + + @rest_command + def list_hosts(self, req, **_kwargs): + return self.ryu_app.list_hosts() 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