diff options
-rw-r--r-- | python/automake.mk | 6 | ||||
-rw-r--r-- | python/ovs/unixctl.py | 306 | ||||
-rw-r--r-- | tests/appctl.py | 68 | ||||
-rw-r--r-- | tests/atlocal.in | 2 | ||||
-rw-r--r-- | tests/automake.mk | 3 | ||||
-rw-r--r-- | tests/test-unixctl.py | 85 | ||||
-rw-r--r-- | tests/testsuite.at | 1 | ||||
-rw-r--r-- | tests/unixctl-py.at | 159 |
8 files changed, 628 insertions, 2 deletions
diff --git a/python/automake.mk b/python/automake.mk index 7ea31186c..f9d2c575f 100644 --- a/python/automake.mk +++ b/python/automake.mk @@ -28,9 +28,11 @@ ovs_pyfiles = \ python/ovs/socket_util.py \ python/ovs/stream.py \ python/ovs/timeval.py \ + python/ovs/unixctl.py \ + python/ovs/util.py \ python/ovs/version.py \ - python/ovs/vlog.py \ - python/ovs/util.py + python/ovs/vlog.py + PYFILES = $(ovs_pyfiles) python/ovs/dirs.py $(ovstest_pyfiles) EXTRA_DIST += $(PYFILES) PYCOV_CLEAN_FILES += $(PYFILES:.py=.py,cover) diff --git a/python/ovs/unixctl.py b/python/ovs/unixctl.py new file mode 100644 index 000000000..8921ba8c2 --- /dev/null +++ b/python/ovs/unixctl.py @@ -0,0 +1,306 @@ +# Copyright (c) 2012 Nicira Networks +# +# 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. + +import copy +import errno +import os +import types + +import ovs.daemon +import ovs.dirs +import ovs.jsonrpc +import ovs.stream +import ovs.util +import ovs.version +import ovs.vlog + +Message = ovs.jsonrpc.Message +vlog = ovs.vlog.Vlog("unixctl") +commands = {} +strtypes = types.StringTypes + + +class _UnixctlCommand(object): + def __init__(self, usage, min_args, max_args, callback, aux): + self.usage = usage + self.min_args = min_args + self.max_args = max_args + self.callback = callback + self.aux = aux + + +def _unixctl_help(conn, unused_argv, unused_aux): + assert isinstance(conn, UnixctlConnection) + reply = "The available commands are:\n" + command_names = sorted(commands.keys()) + for name in command_names: + reply += " " + usage = commands[name].usage + if usage: + reply += "%-23s %s" % (name, usage) + else: + reply += name + reply += "\n" + conn.reply(reply) + + +def _unixctl_version(conn, unused_argv, unused_aux): + assert isinstance(conn, UnixctlConnection) + version = "%s (Open vSwitch) %s %s" % (ovs.util.PROGRAM_NAME, + ovs.version.VERSION, + ovs.version.BUILDNR) + conn.reply(version) + + +def command_register(name, usage, min_args, max_args, callback, aux): + """ Registers a command with the given 'name' to be exposed by the + UnixctlServer. 'usage' describes the arguments to the command; it is used + only for presentation to the user in "help" output. + + 'callback' is called when the command is received. It is passed a + UnixctlConnection object, the list of arguments as unicode strings, and + 'aux'. Normally 'callback' should reply by calling + UnixctlConnection.reply() or UnixctlConnection.reply_error() before it + returns, but if the command cannot be handled immediately, then it can + defer the reply until later. A given connection can only process a single + request at a time, so a reply must be made eventually to avoid blocking + that connection.""" + + assert isinstance(name, strtypes) + assert isinstance(usage, strtypes) + assert isinstance(min_args, int) + assert isinstance(max_args, int) + assert isinstance(callback, types.FunctionType) + + if name not in commands: + commands[name] = _UnixctlCommand(usage, min_args, max_args, callback, + aux) + + +def socket_name_from_target(target): + assert isinstance(target, strtypes) + + if target.startswith("/"): + return 0, target + + pidfile_name = "%s/%s.pid" % (ovs.dirs.RUNDIR, target) + pid = ovs.daemon.read_pidfile(pidfile_name) + if pid < 0: + return -pid, "cannot read pidfile \"%s\"" % pidfile_name + + return 0, "%s/%s.%d.ctl" % (ovs.dirs.RUNDIR, target, pid) + + +class UnixctlConnection(object): + def __init__(self, rpc): + assert isinstance(rpc, ovs.jsonrpc.Connection) + self._rpc = rpc + self._request_id = None + + def run(self): + self._rpc.run() + error = self._rpc.get_status() + if error or self._rpc.get_backlog(): + return error + + for _ in range(10): + if error or self._request_id: + break + + error, msg = self._rpc.recv() + if msg: + if msg.type == Message.T_REQUEST: + self._process_command(msg) + else: + # XXX: rate-limit + vlog.warn("%s: received unexpected %s message" + % (self._rpc.name, + Message.type_to_string(msg.type))) + error = errno.EINVAL + + if not error: + error = self._rpc.get_status() + + return error + + def reply(self, body): + self._reply_impl(True, body) + + def reply_error(self, body): + self._reply_impl(False, body) + + # Called only by unixctl classes. + def _close(self): + self._rpc.close() + self._request_id = None + + def _wait(self, poller): + self._rpc.wait(poller) + if not self._rpc.get_backlog(): + self._rpc.recv_wait(poller) + + def _reply_impl(self, success, body): + assert isinstance(success, bool) + assert body is None or isinstance(body, strtypes) + + assert self._request_id is not None + + if body is None: + body = "" + + if body and not body.endswith("\n"): + body += "\n" + + if success: + reply = Message.create_reply(body, self._request_id) + else: + reply = Message.create_error(body, self._request_id) + + self._rpc.send(reply) + self._request_id = None + + def _process_command(self, request): + assert isinstance(request, ovs.jsonrpc.Message) + assert request.type == ovs.jsonrpc.Message.T_REQUEST + + self._request_id = request.id + + error = None + params = request.params + method = request.method + command = commands.get(method) + if command is None: + error = '"%s" is not a valid command' % method + elif len(params) < command.min_args: + error = '"%s" command requires at least %d arguments' \ + % (method, command.min_args) + elif len(params) > command.max_args: + error = '"%s" command takes at most %d arguments' \ + % (method, command.max_args) + else: + for param in params: + if not isinstance(param, strtypes): + error = '"%s" command has non-string argument' % method + break + + if error is None: + unicode_params = [unicode(p) for p in params] + command.callback(self, unicode_params, command.aux) + + if error: + self.reply_error(error) + + +class UnixctlServer(object): + def __init__(self, listener): + assert isinstance(listener, ovs.stream.PassiveStream) + self._listener = listener + self._conns = [] + + def run(self): + for _ in range(10): + error, stream = self._listener.accept() + if not error: + rpc = ovs.jsonrpc.Connection(stream) + self._conns.append(UnixctlConnection(rpc)) + elif error == errno.EAGAIN: + break + else: + # XXX: rate-limit + vlog.warn("%s: accept failed: %s" % (self._listener.name, + os.strerror(error))) + + for conn in copy.copy(self._conns): + error = conn.run() + if error and error != errno.EAGAIN: + conn._close() + self._conns.remove(conn) + + def wait(self, poller): + self._listener.wait(poller) + for conn in self._conns: + conn._wait(poller) + + def close(self): + for conn in self._conns: + conn._close() + self._conns = None + + self._listener.close() + self._listener = None + + @staticmethod + def create(path): + assert path is None or isinstance(path, strtypes) + + if path is not None: + path = "punix:%s" % ovs.util.abs_file_name(ovs.dirs.RUNDIR, path) + else: + path = "punix:%s/%s.%d.ctl" % (ovs.dirs.RUNDIR, + ovs.util.PROGRAM_NAME, os.getpid()) + + error, listener = ovs.stream.PassiveStream.open(path) + if error: + ovs.util.ovs_error(error, "could not initialize control socket %s" + % path) + return error, None + + command_register("help", "", 0, 0, _unixctl_help, None) + command_register("version", "", 0, 0, _unixctl_version, None) + + return 0, UnixctlServer(listener) + + +class UnixctlClient(object): + def __init__(self, conn): + assert isinstance(conn, ovs.jsonrpc.Connection) + self._conn = conn + + def transact(self, command, argv): + assert isinstance(command, strtypes) + assert isinstance(argv, list) + for arg in argv: + assert isinstance(arg, strtypes) + + request = Message.create_request(command, argv) + error, reply = self._conn.transact_block(request) + + if error: + vlog.warn("error communicating with %s: %s" + % (self._conn.name, os.strerror(error))) + return error, None, None + + if reply.error is not None: + return 0, str(reply.error), None + else: + assert reply.result is not None + return 0, None, str(reply.result) + + def close(self): + self._conn.close() + self.conn = None + + @staticmethod + def create(path): + assert isinstance(path, str) + + unix = "unix:%s" % ovs.util.abs_file_name(ovs.dirs.RUNDIR, path) + error, stream = ovs.stream.Stream.open_block( + ovs.stream.Stream.open(unix)) + + if error: + vlog.warn("failed to connect to %s" % path) + return error, None + + return 0, UnixctlClient(ovs.jsonrpc.Connection(stream)) diff --git a/tests/appctl.py b/tests/appctl.py new file mode 100644 index 000000000..bc694819d --- /dev/null +++ b/tests/appctl.py @@ -0,0 +1,68 @@ +# Copyright (c) 2012 Nicira Networks. +# +# 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. + +import argparse +import sys + +import ovs.daemon +import ovs.unixctl +import ovs.util +import ovs.vlog + + +def connect_to_target(target): + error, str_result = ovs.unixctl.socket_name_from_target(target) + if error: + ovs.util.ovs_fatal(error, str_result) + else: + socket_name = str_result + + error, client = ovs.unixctl.UnixctlClient.create(socket_name) + if error: + ovs.util.ovs_fatal(error, "cannot connect to \"%s\"" % socket_name) + + return client + + +def main(): + parser = argparse.ArgumentParser(description="Python Implementation of" + " ovs-appctl.") + parser.add_argument("-t", "--target", default="ovs-vswitchd", + help="pidfile or socket to contact") + + parser.add_argument("command", metavar="COMMAND", + help="Command to run.") + parser.add_argument("argv", metavar="ARG", nargs="*", + help="Arguments to the command.") + args = parser.parse_args() + + ovs.vlog.Vlog.init() + target = args.target + client = connect_to_target(target) + err_no, error, result = client.transact(args.command, args.argv) + client.close() + + if err_no: + ovs.util.ovs_fatal(err_no, "%s: transaction error" % target) + elif error is not None: + sys.stderr.write(error) + ovs.util.ovs_error(0, "%s: server returned an error" % target) + sys.exit(2) + else: + assert result is not None + sys.stdout.write(result) + + +if __name__ == '__main__': + main() diff --git a/tests/atlocal.in b/tests/atlocal.in index 1d37b59a7..400a5c586 100644 --- a/tests/atlocal.in +++ b/tests/atlocal.in @@ -1,4 +1,6 @@ # -*- shell-script -*- +VERSION='@VERSION@' +BUILDNR='@BUILDNR@' HAVE_OPENSSL='@HAVE_OPENSSL@' HAVE_PYTHON='@HAVE_PYTHON@' PERL='@PERL@' diff --git a/tests/automake.mk b/tests/automake.mk index a2ed7d768..77281579f 100644 --- a/tests/automake.mk +++ b/tests/automake.mk @@ -24,6 +24,7 @@ TESTSUITE_AT = \ tests/vconn.at \ tests/file_name.at \ tests/aes128.at \ + tests/unixctl-py.at \ tests/uuid.at \ tests/json.at \ tests/jsonrpc.at \ @@ -353,12 +354,14 @@ EXTRA_DIST += tests/choose-port.pl # Python tests. CHECK_PYFILES = \ + tests/appctl.py \ tests/test-daemon.py \ tests/test-json.py \ tests/test-jsonrpc.py \ tests/test-ovsdb.py \ tests/test-reconnect.py \ tests/MockXenAPI.py \ + tests/test-unixctl.py \ tests/test-vlog.py EXTRA_DIST += $(CHECK_PYFILES) PYCOV_CLEAN_FILES += $(CHECK_PYFILES:.py=.py,cover) .coverage diff --git a/tests/test-unixctl.py b/tests/test-unixctl.py new file mode 100644 index 000000000..cb9fed226 --- /dev/null +++ b/tests/test-unixctl.py @@ -0,0 +1,85 @@ +# Copyright (c) 2012 Nicira Networks. +# +# 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. + +import argparse +import sys + +import ovs.daemon +import ovs.unixctl + +vlog = ovs.vlog.Vlog("test-unixctl") +exiting = False + +def unixctl_exit(conn, unused_argv, aux): + assert aux == "aux_exit" + global exiting + + exiting = True + conn.reply(None) + + +def unixctl_echo(conn, argv, aux): + assert aux == "aux_echo" + conn.reply(str(argv)) + + +def unixctl_echo_error(conn, argv, aux): + assert aux == "aux_echo_error" + conn.reply_error(str(argv)) + + +def main(): + parser = argparse.ArgumentParser( + description="Open vSwitch unixctl test program for Python") + parser.add_argument("--unixctl", help="UNIXCTL socket location or 'none'.") + + ovs.daemon.add_args(parser) + ovs.vlog.add_args(parser) + args = parser.parse_args() + ovs.daemon.handle_args(args) + ovs.vlog.handle_args(args) + + ovs.daemon.daemonize_start() + error, server = ovs.unixctl.UnixctlServer.create(args.unixctl) + if error: + ovs.util.ovs_fatal(error, "could not create unixctl server at %s" + % args.unixctl, vlog) + + ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, "aux_exit") + ovs.unixctl.command_register("echo", "[arg ...]", 1, 2, unixctl_echo, + "aux_echo") + ovs.unixctl.command_register("echo_error", "[arg ...]", 1, 2, + unixctl_echo_error, "aux_echo_error") + ovs.daemon.daemonize_complete() + + vlog.info("Entering run loop.") + poller = ovs.poller.Poller() + while not exiting: + server.run() + server.wait(poller) + if exiting: + poller.immediate_wake() + poller.block() + server.close() + + +if __name__ == '__main__': + try: + main() + except SystemExit: + # Let system.exit() calls complete normally + raise + except: + vlog.exception("traceback") + sys.exit(ovs.daemon.RESTART_EXIT_CODE) diff --git a/tests/testsuite.at b/tests/testsuite.at index 7711ba304..c9561e77d 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -55,6 +55,7 @@ m4_include([tests/learn.at]) m4_include([tests/vconn.at]) m4_include([tests/file_name.at]) m4_include([tests/aes128.at]) +m4_include([tests/unixctl-py.at]) m4_include([tests/uuid.at]) m4_include([tests/json.at]) m4_include([tests/jsonrpc.at]) diff --git a/tests/unixctl-py.at b/tests/unixctl-py.at new file mode 100644 index 000000000..1d435baab --- /dev/null +++ b/tests/unixctl-py.at @@ -0,0 +1,159 @@ +AT_BANNER([unixctl]) + +AT_SETUP([unixctl ovs-vswitchd exit - Python]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +OVS_VSWITCHD_START + +AT_CHECK([$PYTHON $srcdir/appctl.py -t ovs-vswitchd exit], [0], []) +OVS_WAIT_WHILE([test -s ovs-vswitchd.pid]) + +AT_CHECK([$PYTHON $srcdir/appctl.py -t ovsdb-server exit], [0], []) +OVS_WAIT_WHILE([test -s ovsdb-server.pid]) +AT_CLEANUP + +AT_SETUP([unixctl ovs-vswitchd help - Python]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +OVS_VSWITCHD_START + +AT_CHECK([ovs-appctl help], [0], [stdout]) +AT_CHECK([head -1 stdout], [0], [dnl +The available commands are: +]) +mv stdout expout +AT_CHECK([$PYTHON $srcdir/appctl.py help], [0], [expout]) + +OVS_VSWITCHD_STOP +AT_CLEANUP + + +AT_SETUP([unixctl ovs-vswitchd arguments - Python]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +OVS_VSWITCHD_START + +AT_CHECK([ovs-appctl bond/hash], [2], [], [stderr]) +AT_CHECK([head -1 stderr], [0], [dnl +"bond/hash" command requires at least 1 arguments +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py bond/hash], [2], [], [experr]) + +AT_CHECK([ovs-appctl bond/hash mac], [2], [], [stderr]) +AT_CHECK([head -1 stderr], [0], [dnl +invalid mac +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py bond/hash mac], [2], [], [experr]) + +AT_CHECK([ovs-appctl bond/hash mac vlan], [2], [], [stderr]) +AT_CHECK([head -1 stderr], [0], [dnl +invalid vlan +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py bond/hash mac vlan], [2], [], [experr]) + +AT_CHECK([ovs-appctl bond/hash mac vlan basis], [2], [], [stderr]) +AT_CHECK([head -1 stderr], [0], [dnl +invalid vlan +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py bond/hash vlan basis], [2], [], [experr]) + +AT_CHECK([ovs-appctl bond/hash mac vlan basis extra], [2], [], [stderr]) +AT_CHECK([head -1 stderr], [0], [dnl +"bond/hash" command takes at most 3 arguments +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py bond/hash mac vlan basis extra], [2], [], [experr]) + +OVS_VSWITCHD_STOP +AT_CLEANUP + +AT_SETUP([unixctl bad target - Python]) +OVS_RUNDIR=$PWD; export OVS_RUNDIR +AT_SKIP_IF([test $HAVE_PYTHON = no]) + +AT_CHECK([$PYTHON $srcdir/appctl.py -t bogus doit], [1], [], [stderr]) +AT_CHECK_UNQUOTED([tail -1 stderr], [0], [dnl +appctl.py: cannot read pidfile "$PWD/bogus.pid" (No such file or directory) +]) + +AT_CHECK([$PYTHON $srcdir/appctl.py -t /bogus/path.pid doit], [1], [], [stderr]) +AT_CHECK([tail -1 stderr], [0], [dnl +appctl.py: cannot connect to "/bogus/path.pid" (No such file or directory) +]) + +AT_CLEANUP + +AT_SETUP([unixctl server - Python]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +OVS_RUNDIR=$PWD; export OVS_RUNDIR +OVS_LOGDIR=$PWD; export OVS_LOGDIR +OVS_SYSCONFDIR=$PWD; export OVS_SYSCONFDIR +trap 'kill `cat test-unixctl.py.pid`' 0 +AT_CAPTURE_FILE([$PWD/test-unixctl.py.log]) +AT_CHECK([$PYTHON $srcdir/test-unixctl.py --log-file --pidfile --detach]) + +AT_CHECK([ovs-appctl -t test-unixctl.py help], [0], [stdout]) +AT_CHECK([cat stdout], [0], [dnl +The available commands are: + echo [[arg ...]] + echo_error [[arg ...]] + exit + help + version +]) +mv stdout expout +AT_CHECK([$PYTHON $srcdir/appctl.py -t test-unixctl.py help], [0], [expout]) + +AT_CHECK([echo "test-unixctl.py (Open vSwitch) $VERSION $BUILDNR" > expout]) +AT_CHECK([ovs-appctl -t test-unixctl.py version], [0], [expout]) +AT_CHECK([$PYTHON $srcdir/appctl.py -t test-unixctl.py version], [0], [expout]) + +AT_CHECK([ovs-appctl -t test-unixctl.py echo robot ninja], [0], [stdout]) +AT_CHECK([cat stdout], [0], [dnl +[[u'robot', u'ninja']] +]) +mv stdout expout +AT_CHECK([$PYTHON $srcdir/appctl.py -t test-unixctl.py echo robot ninja], [0], [expout]) + +AT_CHECK([ovs-appctl -t test-unixctl.py echo_error robot ninja], [2], [], [stderr]) +AT_CHECK([cat stderr], [0], [dnl +[[u'robot', u'ninja']] +ovs-appctl: test-unixctl.py: server returned an error +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py -t test-unixctl.py echo_error robot ninja], [2], [], [experr]) + +AT_CHECK([ovs-appctl -t test-unixctl.py echo], [2], [], [stderr]) +AT_CHECK([cat stderr], [0], [dnl +"echo" command requires at least 1 arguments +ovs-appctl: test-unixctl.py: server returned an error +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py -t test-unixctl.py echo], [2], [], [experr]) + +AT_CHECK([ovs-appctl -t test-unixctl.py echo robot ninja pirates], [2], [], [stderr]) +AT_CHECK([cat stderr], [0], [dnl +"echo" command takes at most 2 arguments +ovs-appctl: test-unixctl.py: server returned an error +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py -t test-unixctl.py echo robot ninja pirates], [2], [], [experr]) + +AT_CHECK([ovs-appctl -t test-unixctl.py bogus], [2], [], [stderr]) +AT_CHECK([cat stderr], [0], [dnl +"bogus" is not a valid command +ovs-appctl: test-unixctl.py: server returned an error +]) +sed 's/ovs-appctl/appctl.py/' stderr > experr +AT_CHECK([$PYTHON $srcdir/appctl.py -t test-unixctl.py bogus], [2], [], [experr]) + +AT_CHECK([ovs-appctl -t test-unixctl.py exit]) +trap '' 0] +AT_CLEANUP + + +AT_SETUP([unixctl server errors - Python]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_CHECK($PYTHON $srcdir/test-unixctl.py --unixctl $PWD/bogus/path, [1], [], [ignore]) +AT_CLEANUP |