diff --git a/bin/rpc-cli b/bin/rpc-cli new file mode 100755 index 00000000..cdb832a5 --- /dev/null +++ b/bin/rpc-cli @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation. +# Copyright (C) 2013 YAMAMOTO Takashi +# +# 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.rpc_cli import main +main() diff --git a/run_tests.sh b/run_tests.sh index e791ed94..89153a2c 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -90,7 +90,7 @@ run_tests() { run_pylint() { echo "Running pylint ..." PYLINT_OPTIONS="--rcfile=.pylintrc --output-format=parseable" - PYLINT_INCLUDE="ryu ryu/tests/bin/ryu-client" + PYLINT_INCLUDE="ryu bin/rpc-cli ryu/tests/bin/ryu-client" export PYTHONPATH=$PYTHONPATH:.ryu PYLINT_LOG=pylint.log diff --git a/ryu/cmd/rpc_cli.py b/ryu/cmd/rpc_cli.py new file mode 100755 index 00000000..05696b32 --- /dev/null +++ b/ryu/cmd/rpc_cli.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation. +# Copyright (C) 2013 YAMAMOTO Takashi +# +# 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. + +# a simple command line msgpack-rpc client +# +# a usage example: +# % PYTHONPATH=. ./bin/rpc-cli \ +# --peers=echo-server=localhost:9999,hoge=localhost:9998 +# (Cmd) request echo-server echo ["hoge"] +# RESULT hoge +# (Cmd) request echo-server notify ["notify-method", ["param1","param2"]] +# RESULT notify-method +# (Cmd) +# NOTIFICATION from echo-server ['notify-method', ['param1', 'param2']] +# (Cmd) + +import ryu.contrib + +from oslo.config import cfg + +import cmd +import signal +import socket +import sys +import termios + +from ryu.lib import rpc + + +CONF = cfg.CONF +CONF.register_cli_opts([ + # eg. rpc-cli --peers=hoge=localhost:9998,fuga=localhost:9999 + cfg.ListOpt('peers', default=[], help='list of peers') +]) + + +class Peer(object): + def __init__(self, name, addr): + self._name = name + self._addr = addr + self.client = None + try: + self.connect() + except: + pass + + def connect(self): + self.client = None + s = socket.create_connection(self._addr) + self.client = rpc.Client(s, notification_callback=self.notification) + + def try_to_connect(self, verbose=False): + if self.client: + return + try: + self.connect() + assert self.client + except Exception, e: + if verbose: + print "connection failure", e + raise EOFError + + def notification(self, n): + print "NOTIFICATION from", self._name, n + + def call(self, method, params): + return self._do(lambda: self.client.call(method, params)) + + def send_notification(self, method, params): + self._do(lambda: self.client.send_notification(method, params)) + + def _do(self, f): + def g(): + try: + return f() + except EOFError: + self.client = None + raise + + self.try_to_connect(verbose=True) + try: + return g() + except EOFError: + print "disconnected. trying to connect..." + self.try_to_connect(verbose=True) + print "connected. retrying the request..." + return g() + + +peers = {} + + +def add_peer(name, host, port): + peers[name] = Peer(name, (host, port)) + + +class Cmd(cmd.Cmd): + def __init__(self, *args, **kwargs): + self._in_onecmd = False + self._notification_check_interval = 1 # worth to be configurable? + self._saved_termios = None + cmd.Cmd.__init__(self, *args, **kwargs) + + def _request(self, line, f): + args = line.split(None, 2) + try: + peer = args[0] + method = args[1] + params = eval(args[2]) + except: + print "argument error" + return + try: + p = peers[peer] + except KeyError: + print "unknown peer", peer + return + try: + f(p, method, params) + except rpc.RPCError, e: + print "RPC ERROR", e + except EOFError: + print "disconnected" + + def _complete_peer(self, text, line, _begidx, _endidx): + if len((line + 'x').split()) >= 3: + return [] + return [name for name in peers if name.startswith(text)] + + def do_request(self, line): + """request + send a msgpack-rpc request and print a response. + is a python code snippet, it should be eval'ed to a list. + """ + + def f(p, method, params): + result = p.call(method, params) + print "RESULT", result + + self._request(line, f) + + def do_notify(self, line): + """notify + send a msgpack-rpc notification. + is a python code snippet, it should be eval'ed to a list. + """ + + def f(p, method, params): + p.send_notification(method, params) + + self._request(line, f) + + def complete_request(self, text, line, begidx, endidx): + return self._complete_peer(text, line, begidx, endidx) + + def complete_notify(self, text, line, begidx, endidx): + return self._complete_peer(text, line, begidx, endidx) + + def do_EOF(self, _line): + sys.exit(0) + + def emptyline(self): + self._peek_notification() + + def postcmd(self, _stop, _line): + self._peek_notification() + + def _peek_notification(self): + for k, p in peers.iteritems(): + if p.client: + try: + p.client.peek_notification() + except EOFError: + p.client = None + print "disconnected", k + + @staticmethod + def _save_termios(): + return termios.tcgetattr(sys.stdin.fileno()) + + @staticmethod + def _restore_termios(t): + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, t) + + def preloop(self): + self._saved_termios = self._save_termios() + signal.signal(signal.SIGALRM, self._timeout) + signal.alarm(1) + + def onecmd(self, string): + self._in_onecmd = True + try: + return cmd.Cmd.onecmd(self, string) + finally: + self._in_onecmd = False + + def _timeout(self, _sig, _frame): + if not self._in_onecmd: + # restore terminal settings. (cooked/raw, ...) + # required for pypy at least. + # this doesn't seem to be needed for cpython readline + # module but i'm not sure if it's by spec or luck. + o = self._save_termios() + self._restore_termios(self._saved_termios) + self._peek_notification() + self._restore_termios(o) + signal.alarm(self._notification_check_interval) + + +def main(): + CONF(project='rpc-cli', version='rpc-cli') + + for p_str in CONF.peers: + name, addr = p_str.split('=') + host, port = addr.rsplit(':', 1) + add_peer(name, host, port) + + Cmd().cmdloop() + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg index de434ed4..262dee2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,3 +48,4 @@ setup-hooks = [entry_points] console_scripts = ryu-manager = ryu.cmd.manager:main + rpc-cli = ryu.cmd.rpc_cli:main