Commit a7482569 authored by Leigh Stoller's avatar Leigh Stoller

Here is a fun little change. Lacking native perl SSL XMLRPC tools, I

put together a little library that provides the illusion of nativeness.

sslxmlrpc_client.py.in: New "rawmode" (-r) option. Instead of the
usual command line operation, input raw XMLRPC goo and send that over
to the server. The raw XMLRPC reply goo is spit out on stdout. In
other words, it is up to the caller to generate the XML stuff, and
convert back from XML to a reply structure.

libxmlrpc.pm.in: A new perl library that exports one real method
called, interestingly enough, CallMethod($$$). The first and second
arguments are the module and method to invoke in the RPC server. The
third argument is an arbitrary perl data structure to convert into XML
and pass to the server. For example:

	libxmlrpc::CallMethod("experiment", "state",
	                      {"proj" => "testbed", "exp" => "myemulab"});

The return value of CallMethod is whatever data structure the server
returned, or undef if there is an internal error or if the RPC fails
with a transport error (one of the errors in emulabclient.py).

In case it is not obvious, CallMethod converts the argument to XML
using the RPC:XML perl module, forks off a child to run
sslxmlrpc_client.py.in in rawmode, sends it the XML on its stdin,
reads back the XML for the reply from its stdout, and converts that to
a perl data structure to return to the caller.

The more interesting use of this new goo is to invoke the new
"elabinelab" module in the RPC server, which exports some new methods
to support elabinelab. The idea is that the inner boss will invoke
routines (like setup/destroy vlans, or power cycle) using the RPC
server, and the SSL key of the creator of the inner emulab. This will
be described in more detail when I check in those changes.

There is also a Config() method that is used to set the SSL cert path,
debugging, verbosity, etc. You can take a look if you are interested.

This can be arbitrarily fancy, but I don't need this for many things.
parent 1188089f
......@@ -14,7 +14,7 @@ include $(OBJDIR)/Makeconf
BIN_SCRIPTS = sshxmlrpc_client.py sslxmlrpc_client.py
SBIN_SCRIPTS = sshxmlrpc_server.py xmlrpcbag sslxmlrpc_server.py
LIB_STUFF = sshxmlrpc.py emulabserver.py emulabclient.py
LIB_STUFF = sshxmlrpc.py emulabserver.py emulabclient.py libxmlrpc.pm
LIBEXEC_STUFF = webxmlrpc
WWW_STUFF = xmlrpcapi.php3
DOWNLOAD_STUFF = sshxmlrpc.py sshxmlrpc_client.py emulabclient.py README \
......
......@@ -33,7 +33,9 @@ MAXNSFILESIZE = (1024 * 512)
#
# Note that XMLRPC does not actually return a "class" to the caller; It gets
# converted to a hashed array (Python Dictionary), but using a class gives
# us a ready made constructor.
# us a ready made constructor.
#
# WARNING: If you change this stuff, also change libxmlrpc.pm in this dir.
#
RESPONSE_SUCCESS = 0
RESPONSE_BADARGS = 1
......
#!/usr/bin/perl -w
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2004 University of Utah and the Flux Group.
# All rights reserved.
#
#
# A library of useful DB stuff. Mostly things that get done a lot.
# Saves typing.
#
# XXX: The notion of "uid" is a tad confused. A unix uid is a number,
# while in the DB a user uid is a string (equiv to unix login).
# Needs to be cleaned up.
#
package libxmlrpc;
use strict;
use Exporter;
use vars qw(@ISA @EXPORT);
@ISA = "Exporter";
@EXPORT = qw ( RESPONSE_SUCCESS RESPONSE_BADARGS RESPONSE_ERROR
RESPONSE_FORBIDDEN RESPONSE_BADVERSION RESPONSE_SERVERERROR
RESPONSE_TOOBIG RESPONSE_REFUSED RESPONSE_TIMEDOUT
ParseResponse CallMethod Config
);
# Must come after package declaration!
use lib '@prefix@/lib';
use English;
use XML::Parser;
use RPC::XML;
use RPC::XML::Parser;
use Socket;
use IO::Handle; # thousands of lines just for autoflush :-(
# Configure variables
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $BOSSNODE = "@BOSSNODE@";
# Need this on the path!
$ENV{'PATH'} = $ENV{'PATH'} . ":/usr/local/bin";
#
# Configuration. The importer of this library should set these values
# accordingly.
#
my %config =
( "debug" => 0,
"verbose", => 0,
"server" => undef,
"portnum" => undef,
"version" => undef,
"cert" => undef,
);
my $debug = 0;
#
# Emulab XMLRPC defs.
#
# WARNING: If you change this stuff, also change emulabclient.py in this dir.
#
sub RESPONSE_SUCCESS() { 0; }
sub RESPONSE_BADARGS() { 1; }
sub RESPONSE_ERROR() { 2; }
sub RESPONSE_FORBIDDEN() { 3; }
sub RESPONSE_BADVERSION() { 4; }
sub RESPONSE_SERVERERROR() { 5; }
sub RESPONSE_TOOBIG() { 6; }
sub RESPONSE_REFUSED() { 7; }
sub RESPONSE_TIMEDOUT() { 8; }
##
# The package version number
#
my $PACKAGE_VERSION = 0.1;
#
# This is the "structure" returned by the RPC server. It gets converted into
# a perl hash by the unmarshaller, and we return that directly to the caller
# (as a reference).
#
# class EmulabResponse:
# def __init__(self, code, value=0, output=""):
# self.code = code # A RESPONSE code
# self.value = value # A return value; any valid XML type.
# self.output = output # Pithy output to print
# return
#
sub ParseResponse($)
{
my ($xmlgoo) = @_;
my $parser = RPC::XML::Parser->new();
my $goo = $parser->parse($xmlgoo);
my $value;
my $output;
my $code;
if ($goo->is_fault()) {
$code = $goo->value()->{"faultCode"}->value;
$value = $code;
$output = $goo->value()->{"faultString"}->value;
}
else {
$code = $goo->value()->{"code"}->value;
$value = $goo->value()->{"value"}->value;
$output = $goo->value()->{"output"}->value;
}
return {"code" => $code,
"value" => $value,
"output" => $output};
}
#
# Caller uses this routine to set configuration of this library
#
sub Config($)
{
my ($opthash) = @_;
foreach my $opt (keys(%{ $opthash })) {
my $val = $opthash->{$opt};
if (!exists($config{$opt})) {
print STDERR "*** $0:\n".
" Invalid libxmlrpc option: $opt/$val\n";
return -1;
}
$config{$opt} = $val;
}
return 0;
}
#
# Internal routine to convert the config hash to an option string.
#
sub optionstring()
{
my $options = "";
if ($config{"debug"}) {
$options .= " -d";
}
if (defined($config{"server"})) {
$options .= " -s " . $config{"server"};
}
if (defined($config{"portnum"})) {
$options .= " -p " . $config{"portnum"};
}
if (defined($config{"cert"})) {
if (! -r $config{"cert"}) {
die("*** $0:\n".
" No such certificate: " . $config{"cert"});
}
$options .= " --cert=" . $config{"cert"};
}
return $options;
}
sub CallMethod($$$)
{
my ($module, $method, $arghash) = @_;
my $request = new RPC::XML::request("${module}.${method}",
($PACKAGE_VERSION, $arghash));
pipe(PARENT_RDR, CHILD_WTR) or
die("Error creating parent pipe pair");
pipe(CHILD_RDR, PARENT_WTR) or
die("Error creating child pipe pair");
if ($debug) {
print STDERR $request->as_string();
print STDERR "\n";
}
CHILD_WTR->autoflush(1);
PARENT_WTR->autoflush(1);
my $childpid = fork();
if (! $childpid) {
close(CHILD_RDR);
close(CHILD_WTR);
#
# Dup our descriptors to the parent, and exec the program.
# The parent then talks to it read/write.
#
open(STDIN, "<&PARENT_RDR") || die "Can't redirect stdin";
open(STDOUT, ">&PARENT_WTR") || die "Can't redirect stdout";
# open(STDERR, ">&PARENT") || die "Can't redirect stderr";
# print STDERR "$TB/bin/sslxmlrpc_client.py -r " . optionstring() . "\n";
exec("$TB/bin/sslxmlrpc_client.py -r " . optionstring());
die("*** $0:\n".
" exec sslxmlrpc_client.py failed: $!\n");
}
close(PARENT_RDR);
close(PARENT_WTR);
#
# Okay, send the xmlgoo to the child and close the pipe to give child
# the go ahead.
#
print CHILD_WTR $request->as_string();
close(CHILD_WTR);
#
# Read back the xmlgoo from the child.
#
my $xmlgoo = "";
while (<CHILD_RDR>) {
$xmlgoo .= $_;
}
close(CHILD_RDR);
waitpid($childpid, 0);
if ($?) {
die("SSL XMLRPC client exited with $?\n");
}
if ($debug) {
print STDERR $xmlgoo;
print STDERR "\n";
}
#
# Convert the xmlgoo to Perl and return it.
#
my $response = ParseResponse($xmlgoo);
if (($config{"verbose"} || $response->{"code"}) &&
defined($response->{"output"}) && $response->{"output"} ne "") {
print $response->{"output"};
print "\n";
}
if ($response->{"code"}) {
return undef;
}
return $response->{"value"};
}
# _Always_ make sure that this 1 is at the end of the file...
1;
......@@ -114,9 +114,9 @@ def do_method(server, method_and_args):
# If the first character of the argument looks like a dictionary,
# try to evaluate it.
#
# if value[0] == '{':
# value = eval(value);
# pass
if value.startswith("{"):
value = eval(value);
pass
params[plist[0]] = value
pass
......
......@@ -47,12 +47,16 @@ xmlrpc_port = XMLRPC_PORT
# The default RPC module to invoke.
module = "experiment"
# Where to find the default certificate.
certificate = os.environ["HOME"] + "/.ssl/emulab.pem"
# Where to find the default certificate in the users home dir.
default_cert = "/.ssl/emulab.pem"
certificate = None;
# Debugging output.
debug = 0
# Raw XML mode
rawmode = 0
##
# Print the usage statement to stdout.
#
......@@ -113,9 +117,9 @@ def do_method(server, method_and_args):
# If the first character of the argument looks like a dictionary,
# try to evaluate it.
#
# if value[0] == '{':
# value = eval(value);
# pass
if value.startswith("{"):
value = eval(value);
pass
params[plist[0]] = value
pass
......@@ -164,8 +168,8 @@ def do_method(server, method_and_args):
try:
# Parse the options,
opts, req_args = getopt.getopt(sys.argv[1:],
"dhVs:m:p:",
[ "help", "version", "server=", "module=",
"dhVs:m:p:r",
[ "help", "version", "rawmode", "server=", "module=",
"cert=", "port="])
# ... act on them appropriately, and
for opt, val in opts:
......@@ -198,6 +202,9 @@ try:
elif opt in ("-d", "--debug"):
debug = 1
pass
elif opt in ("-r", "--rawmode"):
rawmode = 1
pass
pass
pass
except getopt.error, e:
......@@ -206,9 +213,50 @@ except getopt.error, e:
sys.exit(2)
pass
class MySSL_Transport(SSL_Transport):
def _parse_response(self, file, sock):
stuff = ""
while 1:
if sock:
response = sock.recv(1024)
else:
response = file.read(1024)
if not response:
break
stuff += response
return stuff
pass
pass
class MyServerProxy(xmlrpclib.ServerProxy):
def raw_request(self, xmlgoo):
#
# I could probably play tricks with the getattr method, but
# not sure what those tricks would be! If I try to access the
# members by name, the getattr definition in the ServerProxy class
# tries to turn that into a method lookup at the other end.
#
transport = self.__dict__["_ServerProxy__transport"]
host = self.__dict__["_ServerProxy__host"]
handler = self.__dict__["_ServerProxy__handler"]
response = transport.request(host, handler, xmlgoo)
if len(response) == 1:
response = response[0]
return response
pass
#
# Vanilla SSL CTX initialization.
#
if certificate == None:
certificate = os.environ["HOME"] + default_cert
pass
if not os.access(certificate, os.R_OK):
print "Certificate cannot be accessed: " + certificate
sys.exit(-1);
......@@ -222,16 +270,42 @@ ctx.set_allow_unknown_ca(0)
# This is parsed by the Proxy object.
URI = "https://" + xmlrpc_server + ":" + str(xmlrpc_port) + TBROOT
if debug:
print URI
print >>sys.stderr, URI
pass
# Get a handle on the server,
server = xmlrpclib.ServerProxy(URI, SSL_Transport(ctx));
if rawmode:
# Get a handle on the server,
server = MyServerProxy(URI, MySSL_Transport(ctx));
stuff = ""
while (True):
foo = sys.stdin.read(1024 * 16)
if foo == "":
break
stuff += foo
pass
#
# Make the call.
#
try:
response = server.raw_request(stuff)
pass
except xmlrpclib.Fault, e:
print e.faultString
sys.exit(-1);
pass
if len(req_args):
print str(response);
sys.exit(0);
elif len(req_args):
# Get a handle on the server,
server = xmlrpclib.ServerProxy(URI, SSL_Transport(ctx));
# Method and args are on the command line.
sys.exit(do_method(server, req_args))
else:
# Get a handle on the server,
server = xmlrpclib.ServerProxy(URI, SSL_Transport(ctx));
# Prompt the user for input.
try:
while True:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment