Commit df2b09b3 authored by Leigh B. Stoller's avatar Leigh B. Stoller

First version that people can probably play with safely, I think.

parent d8fad8b2
......@@ -22,7 +22,7 @@ include Makeconf
#
SUBDIRS = lib db assign @optional_subdirs@ ipod security sensors \
pxe tbsetup account tmcd utils www tip capture ipod vis \
sensors os install/newnode_sshkeys
sensors os xmlrpc install/newnode_sshkeys
all: all-subdirs
......@@ -64,6 +64,7 @@ ops-install:
@$(MAKE) -C tbsetup control-install
@$(MAKE) -C utils control-install
@$(MAKE) -C event control-install
@$(MAKE) -C xmlrpc control-install
install-mkdirs:
-mkdir -p $(INSTALL_TOPDIR)/opsdir
......
......@@ -1462,7 +1462,9 @@ outfiles="$outfiles Makeconf GNUmakefile \
$eventfiles \
$winfiles \
apache/GNUmakefile apache/httpd.conf \
xmlrpc/GNUmakefile \
xmlrpc/GNUmakefile xmlrpc/emulabclient.py xmlrpc/emulabserver.py \
xmlrpc/sshxmlrpc_client.py xmlrpc/sshxmlrpc_server.py \
xmlrpc/webxmlrpc \
install/ops-install install/boss-install \
install/newnode_sshkeys/GNUmakefile "
......
......@@ -507,7 +507,9 @@ outfiles="$outfiles Makeconf GNUmakefile \
$eventfiles \
$winfiles \
apache/GNUmakefile apache/httpd.conf \
xmlrpc/GNUmakefile \
xmlrpc/GNUmakefile xmlrpc/emulabclient.py xmlrpc/emulabserver.py \
xmlrpc/sshxmlrpc_client.py xmlrpc/sshxmlrpc_server.py \
xmlrpc/webxmlrpc \
install/ops-install install/boss-install \
install/newnode_sshkeys/GNUmakefile "
......
......@@ -17,24 +17,30 @@ SBIN_SCRIPTS = sshxmlrpc_server.py
LIB_STUFF = sshxmlrpc.py emulabserver.py emulabclient.py
LIBEXEC_STUFF = webxmlrpc
WWW_STUFF = xmlrpcapi.php3
DOWNLOAD_STUFF = sshxmlrpc.py sshxmlrpc_client.py emulabclient.py
DEBUG_STUFF = experiment
#
# These are the ones installed on plastic (users, control, etc).
#
USERBINS = sshxmlrpc_client.py
CLIENTBIN = sshxmlrpc_client.py
USERLIBS = sshxmlrpc.py emulabclient.py
#
# Force dependencies on the scripts so that they will be rerun through
# configure if the .in file is changed.
#
all: $(BIN_SCRIPTS) $(SBIN_SCRIPTS) $(LIB_STUFF) $(LIBEXEC_STUFF)
all: $(BIN_SCRIPTS) $(SBIN_SCRIPTS) $(LIB_STUFF) $(LIBEXEC_STUFF) \
$(DOWNLOAD_STUFF) $(DEBUG_STUFF)
include $(TESTBED_SRCDIR)/GNUmakerules
install: $(addprefix $(INSTALL_BINDIR)/, $(BIN_SCRIPTS)) \
$(addprefix $(INSTALL_SBINDIR)/, $(SBIN_SCRIPTS)) \
$(addprefix $(INSTALL_SBINDIR)/xmlrpc/, $(DEBUG_STUFF)) \
$(addprefix $(INSTALL_LIBEXECDIR)/, $(LIBEXEC_STUFF)) \
$(addprefix $(INSTALL_WWWDIR)/, $(WWW_STUFF)) \
$(addprefix $(INSTALL_WWWDIR)/downloads/xmlrpc/, $(DOWNLOAD_STUFF)) \
$(addprefix $(INSTALL_LIBDIR)/, $(LIB_STUFF))
$(INSTALL_WWWDIR)/%: %
......@@ -42,19 +48,20 @@ $(INSTALL_WWWDIR)/%: %
-mkdir -p $(patsubst %/,%,$(dir $@))
$(INSTALL_DATA) $(subst $$,\$$,$<) $(subst $$,\$$,$@)
$(INSTALL_WWWDIR)/downloads/xmlrpc/%: %
@echo "Installing $<"
-mkdir -p $(patsubst %/,%,$(dir $@))
$(INSTALL_DATA) $(subst $$,\$$,$<) $(subst $$,\$$,$@)
$(INSTALL_SBINDIR)/xmlrpc/%: %
@echo "Installing $<"
-mkdir -p $(INSTALL_SBINDIR)/xmlrpc
$(INSTALL_PROGRAM) $< $@
#
# Control node installation (okay, plastic)
#
ifneq ($(UNIFIED),1)
LINKS= cd $(INSTALL_BINDIR) && \
list='$(USERBINS)'; for file in $$list; do \
rm -f $$file; \
ln -s plasticwrap $$file; \
done;
endif
control-install:
$(LINKS)
control-install: $(addprefix $(INSTALL_LIBDIR)/, $(USERLIBS)) \
$(addprefix $(INSTALL_BINDIR)/, $(CLIENTBIN))
clean:
......@@ -15,11 +15,17 @@ MAXNSFILESIZE = (1024 * 512)
#
# This class defines a simple structure to return back to the caller.
# It includes the exit status of the command, and any output that it
# wants to send back. Why not return a tuple? Well, it appears that the
# python xmlrpc library requires that a singleton be returned from the
# server, and I do not want to depend on the "Fault" structure that XML
# defines for return values.
# It includes a basic response code (success, failure, badargs, etc),
# as well as a return "value" which can be any valid datatype that can
# be represented in XML (int, string, hash, float, etc). You can also
# send back some output (a string with embedded newlines) to print out
# to the user.
#
# Why not return a tuple? Well, it appears that the python xmlrpc
# library requires that a singleton be returned from the server.
#
# Note that XMLRPC does not actually return a "class" to the caller; It gets
# converted to a hashed array (Python Dictionary).
#
RESPONSE_SUCCESS = 0
RESPONSE_BADARGS = 1
......@@ -29,11 +35,11 @@ RESPONSE_BADVERSION = 4
RESPONSE_SERVERERROR = 5
RESPONSE_TOOBIG = 6
class ResponseBlock:
class EmulabResponse:
def __init__(self, code, value=0, output=None):
self.code = code
self.value = value
self.output = 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
#
......
......@@ -41,10 +41,10 @@ class emulabserver:
#
def echo(self, version, argdict):
if not argdict.has_key("str"):
return ResponseBlock(RESPONSE_BADARGS,
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply a string to echo!");
return ResponseBlock(RESPONSE_SUCCESS, 0,
return EmulabResponse(RESPONSE_SUCCESS, 0,
socket.gethostname() + ": " + str(version)
+ " " + argdict["str"])
......@@ -54,21 +54,21 @@ class emulabserver:
#
def batchexp(self, version, argdict):
if version != self.VERSION:
return ResponseBlock(RESPONSE_BADVERSION,
return EmulabResponse(RESPONSE_BADVERSION,
output="Client version mismatch!");
pass
if not (argdict.has_key("pid") and
argdict.has_key("eid")):
return ResponseBlock(RESPONSE_BADARGS,
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply pid and eid!");
nsfilename = None
argstr = ""
for opt, val in argdict.items():
if opt in ("batchmode"):
if val == 0:
if opt in ("batch"):
if val == "false":
argstr += " -i"
pass
pass
......@@ -88,8 +88,8 @@ class emulabserver:
argstr += " -p "
argstr += escapeshellarg(val)
pass
elif opt in ("idleswap"):
if opt == 0:
elif opt in ("swappable"):
if val == "false":
argstr += " -S "
argstr += argdict["noswap_reason"]
pass
......@@ -97,7 +97,7 @@ class emulabserver:
elif opt in ("noswap_reason"):
pass
elif opt in ("idleswap"):
if opt == 0:
if val == 0:
argstr += " -L "
argstr += escapeshellarg(argdict["noidleswap_reason"])
pass
......@@ -112,11 +112,15 @@ class emulabserver:
argstr += " -a "
argstr += escapeshellarg(val)
pass
elif opt in ("frontend"):
argstr += " -f "
elif opt in ("noswapin"):
if val == "true":
argstr += " -f "
pass
pass
elif opt in ("waitmode"):
argstr += " -w "
elif opt in ("wait"):
if val == "true":
argstr += " -w "
pass
pass
elif opt in ("nsfilepath"):
# Backend script will verify this local path.
......@@ -126,12 +130,12 @@ class emulabserver:
nsfilestr = val
if len(nsfilestr) > (1024 * 512):
return ResponseBlock(RESPONSE_TOOBIG,
return EmulabResponse(RESPONSE_TOOBIG,
output="NS File way too big!");
(nsfp, nsfilename) = writensfile(nsfilestr)
if not nsfilename:
return ResponseBlock(RESPONSE_SERVERERROR,
return EmulabResponse(RESPONSE_SERVERERROR,
output="Server Error")
pass
pass
......@@ -142,9 +146,9 @@ class emulabserver:
(exitval, output) = runcommand(TBDIR + "/bin/batchexp " + argstr)
if exitval:
return ResponseBlock(RESPONSE_ERROR, exitval >> 8, output=output)
return EmulabResponse(RESPONSE_ERROR, exitval >> 8, output=output)
return ResponseBlock(RESPONSE_SUCCESS, output=output)
return EmulabResponse(RESPONSE_SUCCESS, output=output)
#
# startexp is an alias for batchexp.
......@@ -152,13 +156,48 @@ class emulabserver:
def startexp(self, version, argdict):
return self.batchexp(version, argdict)
#
# swap an experiment using swapexp.
#
def swapexp(self, version, argdict):
if version != self.VERSION:
return EmulabResponse(RESPONSE_BADVERSION,
output="Client version mismatch!");
pass
argstr = ""
for opt, val in argdict.items():
if opt in ("wait"):
if val == "true":
argstr += " -w "
pass
pass
pass
if not (argdict.has_key("pid") and
argdict.has_key("eid") and
argdict.has_key("direction")):
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply pid, eid and direction!");
argstr += " -s " + escapeshellarg(argdict["direction"])
argstr += " " + escapeshellarg(argdict["pid"])
argstr += " " + escapeshellarg(argdict["eid"])
(exitval, output) = runcommand(TBDIR + "/bin/swapexp " + argstr)
if exitval:
return EmulabResponse(RESPONSE_ERROR, exitval >> 8, output=output)
return EmulabResponse(RESPONSE_SUCCESS, output=output)
#
# swap an experiment using swapexp. We get the NS file inline, which
# we have to write to a temp file first.
#
def swapexp(self, version, argdict):
def modify(self, version, argdict):
if version != self.VERSION:
return ResponseBlock(RESPONSE_BADVERSION,
return EmulabResponse(RESPONSE_BADVERSION,
output="Client version mismatch!");
pass
......@@ -166,18 +205,20 @@ class emulabserver:
argstr = ""
for opt, val in argdict.items():
if opt in ("waitmode"):
argstr += " -w "
if opt in ("wait"):
if val == "true":
argstr += " -w "
pass
pass
elif opt in ("reboot_nodes"):
argstr += " -r "
elif opt in ("reboot"):
if val == "true":
argstr += " -r "
pass
pass
elif opt in ("restart_eventsys"):
argstr += " -e "
pass
elif opt in ("swapop"):
argstr += " -s "
argstr += escapeshellarg(val)
if val == "true":
argstr += " -e "
pass
pass
elif opt in ("nsfilepath"):
# Backend script will verify this local path.
......@@ -187,21 +228,22 @@ class emulabserver:
nsfilestr = val
if len(nsfilestr) > (1024 * 512):
return ResponseBlock(RESPONSE_TOOBIG,
return EmulabResponse(RESPONSE_TOOBIG,
output="NS File way too big!");
(nsfp, nsfilename) = writensfile(nsfilestr)
if not nsfilename:
return ResponseBlock(RESPONSE_SERVERERROR,
return EmulabResponse(RESPONSE_SERVERERROR,
output="Server Error")
pass
pass
if not (argdict.has_key("pid") and
argdict.has_key("eid")):
return ResponseBlock(RESPONSE_BADARGS,
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply pid and eid!");
argstr += " -s modify"
argstr += " " + escapeshellarg(argdict["pid"])
argstr += " " + escapeshellarg(argdict["eid"])
......@@ -211,30 +253,32 @@ class emulabserver:
(exitval, output) = runcommand(TBDIR + "/bin/swapexp " + argstr)
if exitval:
return ResponseBlock(RESPONSE_ERROR, exitval >> 8, output=output)
return EmulabResponse(RESPONSE_ERROR, exitval >> 8, output=output)
return ResponseBlock(RESPONSE_SUCCESS, output=output)
return EmulabResponse(RESPONSE_SUCCESS, output=output)
#
# end an experiment using endexp.
#
def endexp(self, version, argdict):
if version != self.VERSION:
return ResponseBlock(RESPONSE_BADVERSION,
return EmulabResponse(RESPONSE_BADVERSION,
output="Client version mismatch!");
pass
argstr = ""
for opt, val in argdict.items():
if opt in ("waitmode"):
argstr += " -w "
if opt in ("wait"):
if val == "true":
argstr += " -w "
pass
pass
pass
if not (argdict.has_key("pid") and
argdict.has_key("eid")):
return ResponseBlock(RESPONSE_BADARGS,
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply pid and eid!");
argstr += " " + escapeshellarg(argdict["pid"])
......@@ -242,16 +286,16 @@ class emulabserver:
(exitval, output) = runcommand(TBDIR + "/bin/endexp " + argstr)
if exitval:
return ResponseBlock(RESPONSE_ERROR, exitval >> 8, output=output)
return EmulabResponse(RESPONSE_ERROR, exitval >> 8, output=output)
return ResponseBlock(RESPONSE_SUCCESS, output=output)
return EmulabResponse(RESPONSE_SUCCESS, output=output)
#
# nscheck an NS file.
#
def nscheck(self, version, argdict):
if version != self.VERSION:
return ResponseBlock(RESPONSE_BADVERSION,
return EmulabResponse(RESPONSE_BADVERSION,
output="Client version mismatch!");
pass
......@@ -261,12 +305,12 @@ class emulabserver:
nsfilestr = argdict["nsfilestr"]
if len(nsfilestr) > (1024 * 512):
return ResponseBlock(RESPONSE_TOOBIG,
return EmulabResponse(RESPONSE_TOOBIG,
output="NS File way too big!");
(nsfp, nsfilename) = writensfile(nsfilestr)
if not nsfilename:
return ResponseBlock(RESPONSE_SERVERERROR, output="Server Error")
return EmulabResponse(RESPONSE_SERVERERROR, output="Server Error")
argstr += nsfilename
pass
......@@ -275,21 +319,21 @@ class emulabserver:
argstr += escapeshellarg(argdict["nsfilepath"])
pass
else:
return ResponseBlock(RESPONSE_BADARGS,
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply an NS file to check!");
(exitval, output) = runcommand(TBDIR + "/bin/nscheck " + argstr)
if exitval:
return ResponseBlock(RESPONSE_ERROR, exitval >> 8, output=output)
return EmulabResponse(RESPONSE_ERROR, exitval >> 8, output=output)
return ResponseBlock(RESPONSE_SUCCESS, output=output)
return EmulabResponse(RESPONSE_SUCCESS, output=output)
#
# create_image.
#
def create_image(self, version, argdict):
if version != self.VERSION:
return ResponseBlock(RESPONSE_BADVERSION,
return EmulabResponse(RESPONSE_BADVERSION,
output="Client version mismatch!");
pass
......@@ -304,7 +348,7 @@ class emulabserver:
if not (argdict.has_key("imageid") and
argdict.has_key("nodeid")):
return ResponseBlock(RESPONSE_BADARGS,
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply imageid and nodeid!");
argstr += " " + escapeshellarg(argdict["imageid"])
......@@ -312,16 +356,16 @@ class emulabserver:
(exitval, output) = runcommand(TBDIR + "/bin/create_image " + argstr)
if exitval:
return ResponseBlock(RESPONSE_ERROR, exitval >> 8, output=output)
return EmulabResponse(RESPONSE_ERROR, exitval >> 8, output=output)
return ResponseBlock(RESPONSE_SUCCESS, output=output)
return EmulabResponse(RESPONSE_SUCCESS, output=output)
#
# create_image.
#
def delay_config(self, version, argdict):
if version != self.VERSION:
return ResponseBlock(RESPONSE_BADVERSION,
return EmulabResponse(RESPONSE_BADVERSION,
output="Client version mismatch!");
pass
......@@ -329,7 +373,9 @@ class emulabserver:
for opt, val in argdict.items():
if opt in ("modify_exp"):
argstr += " -m "
if val == "true":
argstr += " -m "
pass
pass
elif opt in ("srcvnode"):
argstr += " -s "
......@@ -341,7 +387,7 @@ class emulabserver:
argdict.has_key("eid") and
argdict.has_key("link") and
argdict.has_key("params")):
return ResponseBlock(RESPONSE_BADARGS,
return EmulabResponse(RESPONSE_BADARGS,
output="Must supply pid, eid, link, params!");
argstr += " " + escapeshellarg(argdict["pid"])
......@@ -354,9 +400,9 @@ class emulabserver:
(exitval, output) = runcommand(TBDIR + "/bin/delay_config " + argstr)
if exitval:
return ResponseBlock(RESPONSE_ERROR, exitval >> 8, output=output)
return EmulabResponse(RESPONSE_ERROR, exitval >> 8, output=output)
return ResponseBlock(RESPONSE_SUCCESS, output=output)
return EmulabResponse(RESPONSE_SUCCESS, output=output)
pass
......
#!/bin/sh
DIR=@prefix@/sbin/
exec $DIR/sshxmlrpc_server.py experiment
......@@ -172,9 +172,12 @@ class SSHTransport:
# @param verbose unused.
# @return The value returned
#
def request(self, host, handler, request_body, verbose=0):
def request(self, host, handler, request_body, verbose=0, path=None):
# Strip the leading slash in the handler, if there is one.
if handler.startswith('/'):
if path:
handler = path + handler
pass
elif handler.startswith('/'):
handler = handler[1:]
pass
......@@ -347,7 +350,7 @@ class SSHServerProxy:
# @param encoding Content encoding.
# @param verbose unused.
#
def __init__(self, uri, transport=None, encoding=None, verbose=0):
def __init__(self, uri, transport=None, encoding=None, verbose=0, path=None):
type, uri = urllib.splittype(uri)
if type not in ("ssh", ):
raise IOError, "unsupported XML-RPC protocol"
......@@ -362,6 +365,7 @@ class SSHServerProxy:
self.__encoding = encoding
self.__verbose = verbose
self.__path = path
return
##
......@@ -379,7 +383,8 @@ class SSHServerProxy:
self.__host,
self.__handler,
request,
verbose=self.__verbose
verbose=self.__verbose,
path=self.__path
)
# ... ensure there was a valid reply.
......
......@@ -32,6 +32,10 @@ module = "experiment"
# Debugging output.
debug = 0
# For admin people.
SERVER_PATH = "/usr/testbed/sbin/"
path = None
##
# Print the usage statement to stdout.
#
......@@ -74,7 +78,7 @@ def do_method(server, method_and_args):
#
params = {}
for param in method_and_args:
plist = string.split(param, "=")
plist = string.split(param, "=", 1)
if len(plist) != 2:
print "Parameters are of the form: param=value!"
return -1
......@@ -89,7 +93,7 @@ def do_method(server, method_and_args):
response = apply(meth, meth_args)
#
# Parse the Response, which is a Dictionary. See ResponseBlock in the
# Parse the Response, which is a Dictionary. See EmulabResponse in the
# emulabclient.py module. The XML standard converts classes to a plain
# Dictionary, hence the code below.
#
......@@ -116,7 +120,7 @@ def do_method(server, method_and_args):
try:
# Parse the options,
opts, req_args = getopt.getopt(sys.argv[1:],
"dhVs:l:",
"dhVs:l:a",
[ "help", "version", "server=", "login=", ])
# ... act on them appropriately, and
for opt, val in opts:
......@@ -137,9 +141,12 @@ try:
elif opt in ("-m", "--module"):
module = val
pass
elif opt in ("-d"):
elif opt in ("-d", "--debug"):
debug = 1
pass
elif opt in ("-a", "--admin"):
path = SERVER_PATH
pass
pass
pass
except getopt.error, e:
......@@ -150,7 +157,7 @@ except getopt.error, e:
# Get a handle on the server,
server = SSHServerProxy("ssh://" + login_id + "@" + xmlrpc_server +
"/xmlrpc/" + module)
"/xmlrpc/" + module, path=path)
if len(req_args):
# Method and args are on the command line.
......@@ -160,7 +167,7 @@ else:
try:
while True:
line = raw_input("$ ")
tokens = line.split(" ", 1)
tokens = line.split(" ")
if len(tokens) >= 1 and len(tokens[0]) > 0:
try:
print str(do_method(server, tokens))
......
......@@ -61,11 +61,11 @@ commands from the shell. For example:
which says to create an experiment called "myeid" in the "mypid" project,
swap it in immediately, wait for the exit status (instead of running
asynchronously), passing inline the contents of nsfile.ns in your home dir.
By default, the client will contact the RPC server at <tt><?php echo
$BOSSNODE ?></tt>, but you can override that by using the <tt>-s
hostname</tt> option to sshxmlrpc_client. If your login ID on the local
machine is different then your login ID at Emulab, you can use the <tt>-l
asynchronously), passing inline the contents of <tt>nsfile.ns</tt> in your
home dir on your desktop. By default, the client will contact the RPC
server at <tt><?php echo $BOSSNODE ?></tt>, but you can override that by
using the <tt>-s hostname</tt> option.. If your login ID on the local
machine is different then your login ID on Emulab, you can use the <tt>-l
login</tt> option. For example:
<code><pre>
......@@ -79,18 +79,19 @@ agent.
</p>
<p>
The <tt>sshxmlrpc_client</tt> python program is a simple demonstration of
how to use Emulab's RPC server. If you do not give provide a method
and arguments on the command line, it will enter a command loop where
you can type in commands (method and arguments) and wait for responses
from the server. It converts your command lines into RPCs to the
server, and prints out the results that the server sends back (exiting with
whatever status code the server returned). You can use this client program
as is, or you can write your own client program in whatever language you
like, as long as you speak to the server over an SSH connection. The API
for the server is broken into several different modules that export a
number of methods, each of which is described below. The python library we
use to speak XMLRPC over an SSH connection takes paths of the form:
The <a href="downloads/xmlrpc/"><tt>sshxmlrpc_client</tt></a>
python program is a simple demonstration of how to use Emulab's RPC
server. If you do not provide a method and arguments on the command
line, it will enter a command loop where you can type in commands (method
and arguments) and wait for responses from the server. It converts your
command lines into RPCs to the server, and prints out the results that the
server sends back (exiting with whatever status code the server
returned). You can use this client program as is, or you can write your own
client program in whatever language you like, as long as you speak to the
server over an SSH connection. The API for the server is broken into
several different modules that export a number of methods, each of which is
described below. The python library we use to speak XMLRPC over an SSH
connection takes paths of the form:
<code><pre>
ssh://user@hostname/XMLRPC/module</code></pre>
......@@ -100,8 +101,9 @@ form (in Python speak):
<code><pre>
def startexp(self, version, arguments):
return status</code></pre>
return EmulabResponse(RESPONSE_SUCCESS, value=0, output="Congratulations")</code></pre>
The arguments to each method:
<ul>
<li><tt>version</tt>: a numeric argument that the server uses to
determine if the client is really capable of speaking to the server.
......@@ -117,12 +119,25 @@ might:
args["pid"] = "mypid"
args["eid"] = "myeid"
args["swapop"] = "out"
return server->swapexp(CURRENTVERSION, args)</code></pre>
response = server->swapexp(CURRENTVERSION, args)</code></pre>
</ul>
The client specifies the <tt>pid</tt> and <tt>eid</tt> of the experiment
he/she wants to swap, as well as the actual swap operation, in this case
<tt>out</tt>.
<tt>out</tt>. The response from the server is another hashed array (Python
Dictionary) of the form:
<blockquote>
<ul>
<li><tt>code</tt>: An integer code as defined in
<a href="downloads/xmlrpc/"><tt>emulabclient.py</tt></a>.
<li><tt>value</tt>: A return value. May be any valid data type that
can be transfered in XML.
<li><tt>output</tt>: A string (with embedded newlines) to print out.
This is useful for debugging and for guiding users through the perils
of XMLRPC programming.
</ul>
</blockquote>
<ul>
<li><b>/XMLRPC/experiments</b>
......@@ -131,28 +146,275 @@ The <tt>experiments</tt> module lets you start, control, and terminate
experiments.
<ul>
<li><tt>startexp</tt>: Create an experiment. By default, the experiment
<li><tt><b>startexp</b></tt>: Create an experiment. By default, the experiment
is started as a <a href="tutorial/tutorial.php3#BatchMode"><em>batch</em></a>
experiment, but you can use the <tt>batchmode</tt> option described below to
alter that behaviour. You can pass an NS file inline, or you can give the
path of a file already on the Emulab fileserver.
<br>
<br>
The required arguments are:
<ul>
<li><tt>pid</tt>: The Emulab project ID in which to create the experiment.
<li><tt>eid</tt>: The unique ID to call the experiment.
<li><tt>nsfilestr</tt>: A string representing the NS file to use, with
embedded newlines, <b>or</b>,
<li><tt>nsfilepath</tt>: The pathname of a NS file on the Emulab file
server, within the project directory (example: /proj/mypid/foo.ns).
</ul>
The required arguments are:<br><br>
<table cellpadding=2>
<tr>
<td>name</td><td>type</td><td>description</td>
</tr>
<tr></tr>
<tr>
<td><tt>pid</tt></td>
<td>string</td>
<td>The Emulab project ID in which to create the experiment</td>
</tr>
<tr>
<td><tt>eid</tt></td>
<td>string</td>
<td>The unique ID to call the experiment</td>
</tr>
<tr>
<td><tt>nsfilestr</tt></td>
<td>string</td>
<td> A string representing the NS file to use, with embedded newlines,
<b>or</b>,</td>
</tr>
<tr>