2
0
mirror of https://github.com/openvswitch/ovs synced 2025-10-25 15:07:05 +00:00

python: Port unixctl to Python.

Many of the currently implemented Python daemons, and likely many
daemons to be implemented in the future, could benefit from unixctl
support even if only to implement "exit" and "version" commands.
This patch implements unixctl in Python.

Signed-off-by: Ethan Jackson <ethan@nicira.com>
This commit is contained in:
Ethan Jackson
2012-02-16 19:15:01 -08:00
parent ffc86c0db7
commit 0a68ffd234
8 changed files with 628 additions and 2 deletions

View File

@@ -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)

306
python/ovs/unixctl.py Normal file
View File

@@ -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))

68
tests/appctl.py Normal file
View File

@@ -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()

View File

@@ -1,4 +1,6 @@
# -*- shell-script -*-
VERSION='@VERSION@'
BUILDNR='@BUILDNR@'
HAVE_OPENSSL='@HAVE_OPENSSL@'
HAVE_PYTHON='@HAVE_PYTHON@'
PERL='@PERL@'

View File

@@ -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

85
tests/test-unixctl.py Normal file
View File

@@ -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)

View File

@@ -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])

159
tests/unixctl-py.at Normal file
View File

@@ -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