--- /dev/null
+#
+# (Re)builds Python metafiles (__init__.py) and documentation
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2005 The Trustees of Princeton University
+#
+# $Id: plcsh,v 1.3 2006/01/09 19:57:24 mlhuang Exp $
+#
+
+# Metafiles
+INIT := PLC/__init__.py PLC/Methods/__init__.py
+
+# Other stuff
+SUBDIRS := doc
+
+all: $(INIT) $(SUBDIRS)
+
+$(SUBDIRS): %:
+ $(MAKE) -C $@
+
+clean:
+ find . -name '*.pyc' -execdir rm -f {} +
+ rm -f $(INIT)
+ for dir in $(SUBDIRS) ; do $(MAKE) -C $$dir clean ; done
+
+# All .py files in PLC/
+PLC := $(filter-out %/__init__.py, $(wildcard PLC/*.py))
+PLC_init := all = '$(notdir $(PLC:.py=))'.split()
+
+PLC/__init__.py:
+ echo "$(PLC_init)" >$@
+
+ifneq ($(sort $(PLC_init)), $(sort $(shell cat PLC/__init__.py 2>/dev/null)))
+PLC/__init__.py: force
+endif
+
+# All .py files in PLC/Methods/ and PLC/Methods/system/
+METHODS := $(filter-out %/__init__.py, $(wildcard PLC/Methods/*.py PLC/Methods/system/*.py))
+Methods_init := methods = '$(notdir $(subst system/, system., $(METHODS:.py=)))'.split()
+
+PLC/Methods/__init__.py:
+ echo "$(Methods_init)" >$@
+
+ifneq ($(sort $(Methods_init)), $(sort $(shell cat PLC/Methods/__init__.py 2>/dev/null)))
+PLC/Methods/__init__.py: force
+endif
+
+force:
+
+.PHONY: force clean $(SUBDIRS)
--- /dev/null
+#
+# Apache mod_python interface
+#
+# Aaron Klingaman <alk@absarokasoft.com>
+# Mark Huang <mlhuang@cs.princeton.edu>
+#
+# Copyright (C) 2004-2006 The Trustees of Princeton University
+# $Id$
+#
+
+import sys
+import traceback
+import xmlrpclib
+from mod_python import apache
+
+from PLC.Debug import log
+
+from PLC.API import PLCAPI
+api = PLCAPI()
+
+def handler(req):
+ try:
+ if req.method != "POST":
+ req.content_type = "text/html"
+ req.send_http_header()
+ req.write("""
+<html><head>
+<title>PLCAPI XML-RPC/SOAP Interface</title>
+</head><body>
+<h1>PLCAPI XML-RPC/SOAP Interface</h1>
+<p>Please use XML-RPC or SOAP to access the PLCAPI.</p>
+</body></html>
+""")
+ return apache.OK
+
+ # Read request
+ request = req.read(int(req.headers_in['content-length']))
+
+ # mod_python < 3.2: The IP address portion of remote_addr is
+ # incorrect (always 0.0.0.0) when IPv6 is enabled.
+ # http://issues.apache.org/jira/browse/MODPYTHON-64?page=all
+ (remote_ip, remote_port) = req.connection.remote_addr
+ remote_addr = (req.connection.remote_ip, remote_port)
+
+ # Handle request
+ response = api.handle(remote_addr, request)
+
+ # Write response
+ req.content_type = "text/xml"
+ req.headers_out.add("Content-length", str(len(response)))
+ req.send_http_header()
+ req.write(response)
+
+ return apache.OK
+
+ except:
+ # Log error in /var/log/httpd/(ssl_)?error_log
+ print >> log, traceback.format_exc()
+ return apache.HTTP_INTERNAL_SERVER_ERROR
--- /dev/null
+#
+# PLCAPI XML-RPC and SOAP interfaces
+#
+# Aaron Klingaman <alk@absarokasoft.com>
+# Mark Huang <mlhuang@cs.princeton.edu>
+#
+# Copyright (C) 2004-2006 The Trustees of Princeton University
+# $Id$
+#
+
+import sys
+import traceback
+
+import xmlrpclib
+
+import SOAPpy
+from SOAPpy.Parser import parseSOAPRPC
+from SOAPpy.Types import faultType
+from SOAPpy.NS import NS
+from SOAPpy.SOAPBuilder import buildSOAP
+
+from PLC.Config import Config
+from PLC.PostgreSQL import PostgreSQL
+from PLC.Faults import *
+import PLC.Methods
+
+class PLCAPI:
+ methods = PLC.Methods.methods
+
+ def __init__(self, config = "/etc/planetlab/plc_config"):
+ # Better just be documenting the API
+ if config is None:
+ return
+
+ # Load configuration
+ self.config = Config(config)
+
+ # Initialize database connection
+ if self.config.PLC_DB_TYPE == "postgresql":
+ self.db = PostgreSQL(self)
+ else:
+ raise PLCAPIError, "Unsupported database type " + config.PLC_DB_TYPE
+
+ def callable(self, method):
+ """
+ Return a new instance of the specified method.
+ """
+
+ # Look up method
+ if method not in self.methods:
+ raise PLCInvalidAPIMethod, method
+
+ # Get new instance of method
+ try:
+ classname = method.split(".")[-1]
+ module = __import__("PLC.Methods." + method, globals(), locals(), [classname])
+ return getattr(module, classname)(self)
+ except ImportError, AttributeError:
+ raise PLCInvalidAPIMethod, method
+
+ def call(self, source, method, *args):
+ """
+ Call the named method from the specified source with the
+ specified arguments.
+ """
+
+ function = self.callable(method)
+ function.source = source
+ return function(*args)
+
+ def handle(self, source, data):
+ """
+ Handle an XML-RPC or SOAP request from the specified source.
+ """
+
+ # Parse request into method name and arguments
+ try:
+ interface = xmlrpclib
+ (args, method) = xmlrpclib.loads(data)
+ methodresponse = True
+ except:
+ interface = SOAPpy
+ (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
+ method = r._name
+ args = r._aslist()
+ # XXX Support named arguments
+
+ try:
+ result = self.call(source, method, *args)
+ except PLCFault, fault:
+ # Handle expected faults
+ if interface == xmlrpclib:
+ result = fault
+ methodresponse = None
+ elif interface == SOAPpy:
+ result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
+ result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
+
+ # Return result
+ if interface == xmlrpclib:
+ if not isinstance(result, PLCFault):
+ result = (result,)
+ data = xmlrpclib.dumps(result, methodresponse = True)
+ elif interface == SOAPpy:
+ data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}})
+
+ return data
--- /dev/null
+#
+# Functions for interacting with the address_types table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from PLC.Parameter import Parameter
+
+class AddressTypes(dict):
+ """
+ Representation of the address_types table in the database.
+ """
+
+ fields = {
+ 'address_type_id': Parameter(int, "Address type identifier"),
+ 'name': Parameter(str, "Address type name"),
+ }
+
+ def __init__(self, api):
+ sql = "SELECT address_type_id, name FROM address_types"
+
+ for row in api.db.selectall(sql):
+ self[row['address_type_id']] = row['name']
+ self[row['name']] = row['address_type_id']
--- /dev/null
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+
+class Address(Row):
+ """
+ Representation of a row in the addresses table. To use, instantiate
+ with a dict of values.
+ """
+
+ fields = {
+ 'address_id': Parameter(int, "Address type"),
+ 'address_type_id': Parameter(int, "Address type identifier"),
+ 'address_type': Parameter(str, "Address type name"),
+ 'line1': Parameter(str, "Address line 1"),
+ 'line2': Parameter(str, "Address line 2"),
+ 'line3': Parameter(str, "Address line 3"),
+ 'city': Parameter(str, "City"),
+ 'state': Parameter(str, "State or province"),
+ 'postalcode': Parameter(str, "Postal code"),
+ 'country': Parameter(str, "Country"),
+ }
+
+ def __init__(self, api, fields):
+ self.api = api
+ dict.__init__(fields)
+
+ def flush(self, commit = True):
+ # XXX
+ pass
+
+ def delete(self, commit = True):
+ # XXX
+ pass
+
+class Addresses(Table):
+ """
+ Representation of row(s) from the addresses table in the
+ database.
+ """
+
+ def __init__(self, api, address_id_list = None):
+ # XXX
+ pass
--- /dev/null
+#
+# PLCAPI authentication parameters
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import crypt
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Persons
+
+class Auth(Parameter, dict):
+ """
+ Base class for all API authentication methods.
+ """
+
+ def __init__(self, auth):
+ Parameter.__init__(self, auth, "API authentication structure", False)
+ dict.__init__(auth)
+
+class NodeAuth(Auth):
+ """
+ PlanetLab version 3.x node authentication structure. Used by the
+ Boot Manager to make authenticated calls to the API based on a
+ unique node key or boot nonce value.
+ """
+
+ def __init__(self):
+ Auth.__init__(self, {
+ 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", False),
+ 'node_id': Parameter(str, "Node identifier", False),
+ 'node_ip': Parameter(str, "Node primary IP address", False),
+ 'value': Parameter(str, "HMAC of node key and method call", False)
+ })
+
+ def check(self, method, auth, *args):
+ # XXX Do HMAC checking
+ return True
+
+class AnonymousAuth(Auth):
+ """
+ PlanetLab version 3.x anonymous authentication structure.
+ """
+
+ def __init__(self):
+ Auth.__init__(self, {
+ 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
+ })
+
+ def check(self, method, auth, *args):
+ # Sure, dude, whatever
+ return True
+
+class PasswordAuth(Auth):
+ """
+ PlanetLab version 3.x password authentication structure.
+ """
+
+ def __init__(self):
+ Auth.__init__(self, {
+ 'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", False),
+ 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", False),
+ 'AuthString': Parameter(str, "Authentication string, typically a password", False),
+ 'Role': Parameter(str, "Role to use for this call", False)
+ })
+
+ def check(self, method, auth, *args):
+ # Method.type_check() should have checked that all of the
+ # mandatory fields were present.
+ assert auth.has_key('Username')
+
+ # Get record (must be enabled)
+ persons = Persons(method.api, [auth['Username']], enabled = True)
+ if len(persons) != 1:
+ raise PLCAuthenticationFailure, "No such account"
+
+ person = persons.values()[0]
+
+ if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
+ # "Capability" authentication, whatever the hell that was
+ # supposed to mean. It really means, login as the special
+ # "maintenance user" using password authentication. Can
+ # only be used on particular machines (those in a list).
+ sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
+ if method.source is not None and method.source[0] not in sources:
+ raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
+
+ # Not sure why this is not stored in the DB
+ password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
+
+ if auth['AuthString'] != password:
+ raise PLCAuthenticationFailure, "Maintenance account password verification failed"
+ else:
+ # Get encrypted password stored in the DB
+ password = person['password']
+
+ # Protect against blank passwords in the DB
+ if password is None or password[:12] == "" or \
+ crypt.crypt(auth['AuthString'], password[:12]) != password:
+ raise PLCAuthenticationFailure, "Password verification failed"
+
+ if auth['Role'] not in person['roles']:
+ raise PLCAuthenticationFailure, "Account does not have " + auth['Role'] + " role"
+
+ if method.roles and auth['Role'] not in method.roles:
+ raise PLCAuthenticationFailure, "Cannot call with " + auth['Role'] + "role"
+
+ method.caller = person
--- /dev/null
+#
+# Functions for interacting with the node_bootstates table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from PLC.Parameter import Parameter
+
+class BootStates(list):
+ """
+ Representation of the node_bootstates table in the database.
+ """
+
+ fields = {
+ 'boot_state': Parameter(int, "Node boot state"),
+ }
+
+ def __init__(self, api):
+ sql = "SELECT * FROM node_bootstates"
+
+ for row in api.db.selectall(sql):
+ self.append(row['boot_state'])
--- /dev/null
+#!/usr/bin/python
+#
+# PLCAPI configuration store. Supports XML-based configuration file
+# format exported by MyPLC.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2004-2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import os
+import sys
+
+from PLC.Faults import *
+from PLC.Debug import profile
+
+# If we have been checked out into a directory at the same
+# level as myplc, where plc_config.py lives. If we are in a
+# MyPLC environment, plc_config.py has already been installed
+# in site-packages.
+myplc = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + \
+ os.sep + "myplc"
+
+class Config:
+ """
+ Parse the bash/Python/PHP version of the configuration file. Very
+ fast but no type conversions.
+ """
+
+ def __init__(self, file = "/etc/planetlab/plc_config"):
+ # Load plc_config
+ try:
+ execfile(file, self.__dict__)
+ except:
+ # Try myplc directory
+ try:
+ execfile(file, self.__dict__)
+ except:
+ raise PLCAPIError("Could not find plc_config in " + \
+ file + ", " + \
+ myplc + "plc_config")
+
+class XMLConfig:
+ """
+ Parse the XML configuration file directly. Takes longer but is
+ presumably more accurate.
+ """
+
+ def __init__(self, file = "/etc/planetlab/plc_config.xml"):
+ try:
+ from plc_config import PLCConfiguration
+ except:
+ sys.path.append(myplc)
+ from plc_config import PLCConfiguration
+
+ # Load plc_config.xml
+ try:
+ cfg = PLCConfiguration(file)
+ except:
+ # Try myplc directory
+ try:
+ cfg = PLCConfiguration(myplc + os.sep + "plc_config.xml")
+ except:
+ raise PLCAPIError("Could not find plc_config.xml in " + \
+ file + ", " + \
+ myplc + "plc_config.xml")
+
+ for (category, variablelist) in cfg.variables().values():
+ for variable in variablelist.values():
+ # Try to cast each variable to an appropriate Python
+ # type.
+ if variable['type'] == "int":
+ value = int(variable['value'])
+ elif variable['type'] == "double":
+ value = float(variable['value'])
+ elif variable['type'] == "boolean":
+ if variable['value'] == "true":
+ value = True
+ else:
+ value = False
+ else:
+ value = variable['value']
+
+ # Variables are split into categories such as
+ # "plc_api", "plc_db", etc. Within each category are
+ # variables such as "host", "port", etc. For backward
+ # compatibility, refer to variables by their shell
+ # names.
+ shell_name = category['id'].upper() + "_" + variable['id'].upper()
+ setattr(self, shell_name, value)
+
+if __name__ == '__main__':
+ import pprint
+ pprint = pprint.PrettyPrinter()
+ pprint.pprint(Config().__dict__.items())
--- /dev/null
+import time
+import sys
+import syslog
+
+class unbuffered:
+ """
+ Write to /var/log/httpd/error_log. See
+
+ http://www.modpython.org/FAQ/faqw.py?req=edit&file=faq02.003.htp
+ """
+
+ def write(self, data):
+ sys.stderr.write(data)
+ sys.stderr.flush()
+
+log = unbuffered()
+
+def profile(callable):
+ """
+ Prints the runtime of the specified callable. Use as a decorator, e.g.,
+
+ @profile
+ def foo(...):
+ ...
+
+ Or, equivalently,
+
+ def foo(...):
+ ...
+ foo = profile(foo)
+
+ Or inline:
+
+ result = profile(foo)(...)
+ """
+
+ def wrapper(*args, **kwds):
+ start = time.time()
+ result = callable(*args, **kwds)
+ end = time.time()
+ args = map(str, args)
+ args += ["%s = %s" % (name, str(value)) for (name, value) in kwds.items()]
+ print >> log, "%s (%s): %f s" % (callable.__name__, ", ".join(args), end - start)
+ return result
+
+ return wrapper
+
+if __name__ == "__main__":
+ @profile
+ def sleep(seconds = 1):
+ time.sleep(seconds)
+
+ sleep(1)
--- /dev/null
+#
+# PLCAPI XML-RPC faults
+#
+# Aaron Klingaman <alk@absarokasoft.com>
+# Mark Huang <mlhuang@cs.princeton.edu>
+#
+# Copyright (C) 2004-2006 The Trustees of Princeton University
+# $Id$
+#
+
+import xmlrpclib
+
+class PLCFault(xmlrpclib.Fault):
+ def __init__(self, faultCode, faultString, extra = None):
+ if extra:
+ faultString += ": " + extra
+ xmlrpclib.Fault.__init__(self, faultCode, faultString)
+
+class PLCInvalidAPIMethod(PLCFault):
+ def __init__(self, method, role = None, extra = None):
+ faultString = "Invalid method " + method
+ if role:
+ faultString += " for role " + role
+ PLCFault.__init__(self, 100, faultString, extra)
+
+class PLCInvalidArgumentCount(PLCFault):
+ def __init__(self, got, min, max = min, extra = None):
+ if min != max:
+ expected = "%d-%d" % (min, max)
+ else:
+ expected = "%d" % min
+ faultString = "Expected %s arguments, got %d" % \
+ (expected, got)
+ PLCFault.__init__(self, 101, faultString, extra)
+
+class PLCInvalidArgument(PLCFault):
+ def __init__(self, extra = None, name = None):
+ if name is not None:
+ faultString = "Invalid %s value" % name
+ else:
+ faultString = "Invalid argument"
+ PLCFault.__init__(self, 102, faultString, extra)
+
+class PLCAuthenticationFailure(PLCFault):
+ def __init__(self, extra = None):
+ faultString = "Failed to authenticate call"
+ PLCFault.__init__(self, 103, faultString, extra)
+
+class PLCNotImplemented(PLCFault):
+ def __init__(self, extra = None):
+ faultString = "Not fully implemented"
+ PLCFault.__init__(self, 109, faultString, extra)
+
+class PLCDBError(PLCFault):
+ def __init__(self, extra = None):
+ faultString = "Database error"
+ PLCFault.__init__(self, 106, faultString, extra)
+
+class PLCPermissionDenied(PLCFault):
+ def __init__(self, extra = None):
+ faultString = "Permission denied"
+ PLCFault.__init__(self, 108, faultString, extra)
+
+class PLCAPIError(PLCFault):
+ def __init__(self, extra = None):
+ faultString = "Internal API error"
+ PLCFault.__init__(self, 111, faultString, extra)
--- /dev/null
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+
+class Key(Row):
+ """
+ Representation of a row in the keys table. To use, instantiate
+ with a dict of values.
+ """
+
+ fields = {
+ 'key_id': Parameter(int, "Key type"),
+ 'key_type': Parameter(str, "Key type"),
+ 'key': Parameter(str, "Key value"),
+ 'last_updated': Parameter(str, "Date and time of last update"),
+ 'is_blacklisted': Parameter(str, "Key has been blacklisted and is forever unusable"),
+ }
+
+ def __init__(self, api, fields):
+ self.api = api
+ dict.__init__(fields)
+
+ def commit(self):
+ # XXX
+ pass
+
+ def delete(self):
+ # XXX
+ pass
+
+class Keys(Table):
+ """
+ Representation of row(s) from the keys table in the
+ database.
+ """
+
+ def __init__(self, api, key_id_list = None):
+ # XXX
+ pass
--- /dev/null
+#
+# Base class for all PLCAPI functions
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import xmlrpclib
+from types import *
+import textwrap
+import os
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+class Method:
+ """
+ Base class for all PLCAPI functions. At a minimum, all PLCAPI
+ functions must define:
+
+ roles = [list of roles]
+ accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
+ returns = Parameter(return_type, return_doc)
+ call(arg1, arg2, ...): method body
+
+ Argument types may be Python types (e.g., int, bool, etc.), typed
+ values (e.g., 1, True, etc.), a Parameter, or lists or
+ dictionaries of possibly mixed types, values, and/or Parameters
+ (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
+
+ Once function decorators in Python 2.4 are fully supported,
+ consider wrapping calls with accepts() and returns() functions
+ instead of performing type checking manually.
+ """
+
+ # Defaults. Could implement authentication and type checking with
+ # decorators, but they are not supported in Python 2.3 and it
+ # would be hard to generate documentation without writing a code
+ # parser.
+
+ roles = []
+ accepts = []
+ returns = bool
+ status = "current"
+
+ def call(self):
+ """
+ Method body for all PLCAPI functions. Must override.
+ """
+
+ return True
+
+ def __init__(self, api):
+ self.name = self.__class__.__name__
+ self.api = api
+
+ # Auth may set this to a Person instance (if an anonymous
+ # method, will remain None).
+ self.caller = None
+
+ # API may set this to a (addr, port) tuple if known
+ self.source = None
+
+ def __call__(self, *args):
+ """
+ Main entry point for all PLCAPI functions. Type checks
+ arguments, authenticates, and executes call().
+ """
+
+ try:
+ (min_args, max_args, defaults) = self.args()
+
+ # Check that the right number of arguments were passed in
+ if len(args) < len(min_args) or len(args) > len(max_args):
+ raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
+
+ for name, value, expected in zip(max_args, args, self.accepts):
+ self.type_check(name, value, expected)
+
+ # The first argument to all methods that require
+ # authentication, should be an Auth structure. The rest of the
+ # arguments to the call may also be used in the authentication
+ # check. For example, calls made by the Boot Manager are
+ # verified by comparing a hash of the message parameters to
+ # the value in the authentication structure.
+
+ if len(self.accepts):
+ auth = None
+ if isinstance(self.accepts[0], Auth):
+ auth = self.accepts[0]
+ elif isinstance(self.accepts[0], Mixed):
+ for auth in self.accepts[0]:
+ if isinstance(auth, Auth):
+ break
+ if isinstance(auth, Auth):
+ auth.check(self, *args)
+
+ return self.call(*args)
+
+ except PLCFault, fault:
+ # Prepend method name to expected faults
+ fault.faultString = self.name + ": " + fault.faultString
+ raise fault
+
+ def help(self, indent = " "):
+ """
+ Text documentation for the method.
+ """
+
+ (min_args, max_args, defaults) = self.args()
+
+ text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
+
+ text += "Description:\n\n"
+ lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
+ text += "\n".join(lines) + "\n\n"
+
+ text += "Allowed Roles:\n\n"
+ if not self.roles:
+ roles = ["any"]
+ else:
+ roles = self.roles
+ text += indent + ", ".join(roles) + "\n\n"
+
+ def param_text(name, param, indent, step):
+ """
+ Format a method parameter.
+ """
+
+ text = indent
+
+ # Print parameter name
+ if name:
+ param_offset = 32
+ text += name.ljust(param_offset - len(indent))
+ else:
+ param_offset = len(indent)
+
+ # Print parameter type
+ param_type = python_type(param)
+ text += xmlrpc_type(param_type) + "\n"
+
+ # Print parameter documentation right below type
+ if isinstance(param, Parameter):
+ wrapper = textwrap.TextWrapper(width = 70,
+ initial_indent = " " * param_offset,
+ subsequent_indent = " " * param_offset)
+ text += "\n".join(wrapper.wrap(param.doc)) + "\n"
+ param = param.type
+
+ text += "\n"
+
+ # Indent struct fields and mixed types
+ if isinstance(param, dict):
+ for name, subparam in param.iteritems():
+ text += param_text(name, subparam, indent + step, step)
+ elif isinstance(param, Mixed):
+ for subparam in param:
+ text += param_text(name, subparam, indent + step, step)
+ elif isinstance(param, (list, tuple)):
+ for subparam in param:
+ text += param_text("", subparam, indent + step, step)
+
+ return text
+
+ text += "Parameters:\n\n"
+ for name, param in zip(max_args, self.accepts):
+ text += param_text(name, param, indent, indent)
+
+ text += "Returns:\n\n"
+ text += param_text("", self.returns, indent, indent)
+
+ return text
+
+ def args(self):
+ """
+ Returns a tuple:
+
+ ((arg1_name, arg2_name, ...),
+ (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
+ (None, None, ..., optional1_default, optional2_default, ...))
+
+ That represents the minimum and maximum sets of arguments that
+ this function accepts and the defaults for the optional arguments.
+ """
+
+ # Inspect call. Remove self from the argument list.
+ max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
+ defaults = self.call.func_defaults
+ if defaults is None:
+ defaults = ()
+
+ min_args = max_args[0:len(max_args) - len(defaults)]
+ defaults = tuple([None for arg in min_args]) + defaults
+
+ return (min_args, max_args, defaults)
+
+ def type_check(self, name, value, expected):
+ """
+ Checks the type of the named value against the expected type,
+ which may be a Python type, a typed value, a Parameter, a
+ Mixed type, or a list or dictionary of possibly mixed types,
+ values, Parameters, or Mixed types.
+
+ Extraneous members of lists must be of the same type as the
+ last specified type. For example, if the expected argument
+ type is [int, bool], then [1, False] and [14, True, False,
+ True] are valid, but [1], [False, 1] and [14, True, 1] are
+ not.
+
+ Extraneous members of dictionaries are ignored.
+ """
+
+ # If any of a number of types is acceptable
+ if isinstance(expected, Mixed):
+ for item in expected:
+ try:
+ self.type_check(name, value, item)
+ expected = item
+ break
+ except PLCInvalidArgument, fault:
+ pass
+ if expected != item:
+ xmlrpc_types = [xmlrpc_type(item) for item in expected]
+ raise PLCInvalidArgument("expected %s, got %s" % \
+ (" or ".join(xmlrpc_types),
+ xmlrpc_type(type(value))),
+ name)
+
+ # Get actual expected type from within the Parameter structure
+ elif isinstance(expected, Parameter):
+ expected = expected.type
+
+ expected_type = python_type(expected)
+
+ # Strings are a special case. Accept either unicode or str
+ # types if a string is expected.
+ if expected_type in StringTypes and isinstance(value, StringTypes):
+ pass
+
+ # Integers and long integers are also special types. Accept
+ # either int or long types if an int or long is expected.
+ elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
+ pass
+
+ elif not isinstance(value, expected_type):
+ raise PLCInvalidArgument("expected %s, got %s" % \
+ (xmlrpc_type(expected_type),
+ xmlrpc_type(type(value))),
+ name)
+
+ # If a list with particular types of items is expected
+ if isinstance(expected, (list, tuple)):
+ for i in range(len(value)):
+ if i >= len(expected):
+ i = len(expected) - 1
+ self.type_check(name + "[]", value[i], expected[i])
+
+ # If a struct with particular (or required) types of items is
+ # expected.
+ elif isinstance(expected, dict):
+ for key in value.keys():
+ if key in expected:
+ self.type_check(name + "['%s']" % key, value[key], expected[key])
+ for key, subparam in expected.iteritems():
+ if isinstance(subparam, Parameter) and \
+ not subparam.optional and key not in value.keys():
+ raise PLCInvalidArgument("'%s' not specified" % key, name)
+
+def python_type(arg):
+ """
+ Returns the Python type of the specified argument, which may be a
+ Python type, a typed value, or a Parameter.
+ """
+
+ if isinstance(arg, Parameter):
+ arg = arg.type
+
+ if isinstance(arg, type):
+ return arg
+ else:
+ return type(arg)
+
+def xmlrpc_type(arg):
+ """
+ Returns the XML-RPC type of the specified argument, which may be a
+ Python type, a typed value, or a Parameter.
+ """
+
+ arg_type = python_type(arg)
+
+ if arg_type == NoneType:
+ return "nil"
+ elif arg_type == IntType or arg_type == LongType:
+ return "int"
+ elif arg_type == bool:
+ return "boolean"
+ elif arg_type == FloatType:
+ return "double"
+ elif arg_type in StringTypes:
+ return "string"
+ elif arg_type == ListType or arg_type == TupleType:
+ return "array"
+ elif arg_type == DictType:
+ return "struct"
+ elif arg_type == Mixed:
+ # Not really an XML-RPC type but return "mixed" for
+ # documentation purposes.
+ return "mixed"
+ else:
+ raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Sites import Site, Sites
+from PLC.Auth import PasswordAuth
+
+class AdmAddNode(Method):
+ """
+ Adds a new node. Any values specified in optional_vals are used,
+ otherwise defaults are used.
+
+ PIs and techs may only add nodes to their own sites. Admins may
+ add nodes to any site.
+
+ Returns the new node_id (> 0) if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi', 'tech']
+
+ can_update = lambda (field, value): field in \
+ ['model', 'version']
+ update_fields = dict(filter(can_update, Node.fields.items()))
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Site.fields['site_id'],
+ Site.fields['login_base']),
+ Node.fields['hostname'],
+ Node.fields['boot_state'],
+ update_fields
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, site_id_or_login_base, hostname, boot_state, optional_vals = {}):
+ if filter(lambda field: field not in self.update_fields, optional_vals):
+ raise PLCInvalidArgument, "Invalid fields specified"
+
+ # Get site information
+ sites = Sites(self.api, [site_id_or_login_base])
+ if not sites:
+ raise PLCInvalidArgument, "No such site"
+
+ site = sites.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # If we are not an admin, make sure that the caller is a
+ # member of the site.
+ if 'admin' not in self.caller['roles']:
+ if site['site_id'] not in self.caller['site_ids']:
+ assert self.caller['person_id'] not in site['person_ids']
+ raise PLCPermissionDenied, "Not allowed to add nodes to specified site"
+ else:
+ assert self.caller['person_id'] in site['person_ids']
+
+ node = Node(self.api, optional_vals)
+ node['hostname'] = hostname
+ node['boot_state'] = boot_state
+ node.flush()
+
+ # Now associate the node with the site
+ node_id = node['node_id']
+ nodegroup_id = site['nodegroup_id']
+ self.api.db.do("INSERT INTO nodegroup_nodes (nodegroup_id, node_id)" \
+ " VALUES(%(nodegroup_id)d, %(node_id)d)",
+ locals())
+
+ return node['node_id']
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+
+class AdmAddPerson(Method):
+ """
+ Adds a new account. Any fields specified in optional_vals are
+ used, otherwise defaults are used.
+
+ Accounts are disabled by default. To enable an account, use
+ AdmSetPersonEnabled() or AdmUpdatePerson().
+
+ Returns the new person_id (> 0) if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi']
+
+ can_update = lambda (field, value): field in \
+ ['title', 'email', 'password', 'phone', 'url', 'bio']
+ update_fields = dict(filter(can_update, Person.fields.items()))
+
+ accepts = [
+ PasswordAuth(),
+ Person.fields['first_name'],
+ Person.fields['last_name'],
+ update_fields
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, first_name, last_name, optional_vals = {}):
+ if filter(lambda field: field not in self.update_fields, optional_vals):
+ raise PLCInvalidArgument, "Invalid fields specified"
+
+ person = Person(self.api, optional_vals)
+ person['first_name'] = first_name
+ person['last_name'] = last_name
+ person['enabled'] = False
+ person.flush()
+
+ return person['person_id']
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Auth import PasswordAuth
+
+class AdmAddPersonToSite(Method):
+ """
+ Adds the specified person to the specified site. If the person is
+ already a member of the site, no errors are returned. Does not
+ change the person's primary site.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ Mixed(Site.fields['site_id'],
+ Site.fields['login_base'])
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email, site_id_or_login_base):
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Get site information
+ sites = Sites(self.api, [site_id_or_login_base])
+ if not sites:
+ raise PLCInvalidArgument, "No such site"
+
+ site = sites.values()[0]
+
+ if site['site_id'] not in person['site_ids']:
+ person_id = person['person_id']
+ site_id = site['site_id']
+ self.api.db.do("INSERT INTO person_site (person_id, site_id)" \
+ " VALUES(%(person_id)d, %(site_id)d)",
+ locals())
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Sites import Site, Sites
+from PLC.Auth import PasswordAuth
+
+class AdmAddSite(Method):
+ """
+ Adds a new site, and creates a node group for that site. Any
+ fields specified in optional_vals are used, otherwise defaults are
+ used.
+
+ Returns the new site_id (> 0) if successful, faults otherwise.
+ """
+
+ roles = ['admin']
+
+ can_update = lambda (field, value): field in \
+ ['is_public', 'latitude', 'longitude', 'url',
+ 'organization_id', 'ext_consortium_id']
+ update_fields = dict(filter(can_update, Site.fields.items()))
+
+ accepts = [
+ PasswordAuth(),
+ Site.fields['name'],
+ Site.fields['abbreviated_name'],
+ Site.fields['login_base'],
+ update_fields
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, name, abbreviated_name, login_base, optional_vals = {}):
+ if filter(lambda field: field not in self.update_fields, optional_vals):
+ raise PLCInvalidArgument, "Invalid field specified"
+
+ site = Site(self.api, optional_vals)
+ site['name'] = name
+ site['abbreviated_name'] = abbreviated_name
+ site['login_base'] = login_base
+ site.flush()
+
+ return site['site_id']
--- /dev/null
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import PasswordAuth
+
+class AdmAuthCheck(Method):
+ """
+ Returns 1 if the user authenticated successfully, faults
+ otherwise.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+ accepts = [PasswordAuth()]
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth):
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import PasswordAuth
+from PLC.Nodes import Node, Nodes
+
+class AdmDeleteNode(Method):
+ """
+ Mark an existing node as deleted.
+
+ PIs and techs may only delete nodes at their own sites. Admins may
+ delete nodes at any site.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Node.fields['node_id'],
+ Node.fields['hostname'])
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, node_id_or_hostname):
+ # Get account information
+ nodes = Nodes(self.api, [node_id_or_hostname])
+ if not nodes:
+ raise PLCInvalidArgument, "No such node"
+
+ node = nodes.values()[0]
+
+ # If we are not an admin, make sure that the caller is a
+ # member of the site at which the node is located.
+ if 'admin' not in self.caller['roles']:
+ # Authenticated function
+ assert self.caller is not None
+
+ if node['site_id'] not in self.caller['site_ids']:
+ raise PLCPermissionDenied, "Not allowed to delete nodes from specified site"
+
+ node.delete()
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+
+class AdmDeletePerson(Method):
+ """
+ Mark an existing account as deleted.
+
+ Users and techs can only delete themselves. PIs can only delete
+ themselves and other non-PIs at their sites. Admins can delete
+ anyone.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email'])
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email):
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Check if we can update this account
+ if not self.caller.can_update(person):
+ raise PLCPermissionDenied, "Not allowed to delete specified account"
+
+ person.delete()
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Sites import Site, Sites
+from PLC.Persons import Person, Persons
+from PLC.Nodes import Node, Nodes
+from PLC.PCUs import PCU, PCUs
+from PLC.Auth import PasswordAuth
+
+class AdmDeleteSite(Method):
+ """
+ Mark an existing site as deleted. The accounts of people who are
+ not members of at least one other non-deleted site will also be
+ marked as deleted. Nodes, PCUs, and slices associated with the
+ site will be deleted.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Site.fields['site_id'],
+ Site.fields['login_base'])
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, site_id_or_login_base):
+ # Get account information
+ sites = Sites(self.api, [site_id_or_login_base])
+ if not sites:
+ raise PLCInvalidArgument, "No such site"
+
+ site = sites.values()[0]
+ site.delete()
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter
+from PLC.Roles import Roles
+from PLC.Auth import PasswordAuth
+
+class AdmGetAllRoles(Method):
+ """
+ Return all possible roles as a struct:
+
+ {'10': 'admin', '20': 'pi', '30': 'user', '40': 'tech'}
+
+ Note that because of XML-RPC marshalling limitations, the keys to
+ this struct are string representations of the integer role
+ identifiers.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+ accepts = [PasswordAuth()]
+ returns = dict
+
+ def call(self, auth, person_id_or_email_list = None, return_fields = None):
+ roles = Roles(self.api)
+
+ # Just the role_id: name mappings
+ roles = dict(filter(lambda (role_id, name): isinstance(role_id, (int, long)), \
+ roles.items()))
+
+ # Stringify the keys!
+ keys = map(str, roles.keys())
+
+ return dict(zip(keys, roles.values()))
--- /dev/null
+import os
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Auth import PasswordAuth
+
+class AdmGetNodes(Method):
+ """
+ Return an array of dictionaries containing details about the
+ specified accounts.
+
+ Admins may retrieve details about all accounts by not specifying
+ node_id_or_email_list or by specifying an empty list. Users and
+ techs may only retrieve details about themselves. PIs may retrieve
+ details about themselves and others at their sites.
+
+ If return_fields is specified, only the specified fields will be
+ returned, if set. Otherwise, the default set of fields returned is:
+
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ [Mixed(Node.fields['node_id'],
+ Node.fields['hostname'])],
+ Parameter([str], 'List of fields to return')
+ ]
+
+ # Filter out hidden fields
+ can_return = lambda (field, value): field not in ['deleted']
+ return_fields = dict(filter(can_return, Node.all_fields.items()))
+ returns = [return_fields]
+
+ def __init__(self, *args, **kwds):
+ Method.__init__(self, *args, **kwds)
+ # Update documentation with list of default fields returned
+ self.__doc__ += os.linesep.join(Node.default_fields.keys())
+
+ def call(self, auth, node_id_or_hostname_list = None, return_fields = None):
+ # Authenticated function
+ assert self.caller is not None
+
+ # Remove admin only fields
+ if 'admin' not in self.caller['roles']:
+ for key in ['boot_nonce', 'key', 'session', 'root_person_ids']:
+ del self.return_fields[key]
+
+ # Make sure that only valid fields are specified
+ if return_fields is None:
+ return_fields = self.return_fields
+ elif filter(lambda field: field not in self.return_fields, return_fields):
+ raise PLCInvalidArgument, "Invalid return field specified"
+
+ # Get node information
+ nodes = Nodes(self.api, node_id_or_hostname_list, return_fields).values()
+
+ # Filter out undesired or None fields (XML-RPC cannot marshal
+ # None) and turn each node into a real dict.
+ valid_return_fields_only = lambda (key, value): \
+ key in return_fields and value is not None
+ nodes = [dict(filter(valid_return_fields_only, node.items())) \
+ for node in nodes]
+
+ return nodes
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+
+class AdmGetPersonRoles(Method):
+ """
+ Return the roles that the specified person has as a struct:
+
+ {'10': 'admin', '30': 'user', '20': 'pi', '40': 'tech'}
+
+ Admins can get the roles for any user. PIs can only get the roles
+ for members of their sites. All others may only get their own
+ roles.
+
+ Note that because of XML-RPC marshalling limitations, the keys to
+ this struct are string representations of the integer role
+ identifiers.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email'])
+ ]
+
+ returns = dict
+
+ # Stupid return type, and can get now roles through
+ # AdmGetPersons().
+ status = "useless"
+
+ def call(self, auth, person_id_or_email):
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Check if we can view this account
+ if not self.caller.can_view(person):
+ raise PLCPermissionDenied, "Not allowed to view specified account"
+
+ # Stringify the keys!
+ role_ids = map(str, person['role_ids'])
+ roles = person['roles']
+
+ return dict(zip(role_ids, roles))
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Auth import PasswordAuth
+
+from PLC.Methods.AdmGetPersons import AdmGetPersons
+
+class AdmGetPersonSites(AdmGetPersons):
+ """
+ Returns the sites that the specified person is associated with as
+ an array of site identifiers.
+
+ Admins may retrieve details about anyone. Users and techs may only
+ retrieve details about themselves. PIs may retrieve details about
+ themselves and others at their sites.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email'])
+ ]
+
+ returns = [Site.fields['site_id']]
+
+ def call(self, auth, person_id_or_email):
+ persons = AdmGetPersons.call(self, auth, [person_id_or_email])
+
+ # AdmGetPersons() validates person_id_or_email
+ assert persons
+ person = persons[0]
+
+ # Filter out deleted sites
+ sites = Sites(self.api, persons['site_ids'])
+
+ return [site['site_id'] for site in sites]
--- /dev/null
+import os
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+
+class AdmGetPersons(Method):
+ """
+ Return an array of dictionaries containing details about the
+ specified accounts.
+
+ Admins may retrieve details about all accounts by not specifying
+ person_id_or_email_list or by specifying an empty list. Users and
+ techs may only retrieve details about themselves. PIs may retrieve
+ details about themselves and others at their sites.
+
+ If return_fields is specified, only the specified fields will be
+ returned, if set. Otherwise, the default set of fields returned is:
+
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ [Mixed(Person.fields['person_id'],
+ Person.fields['email'])],
+ Parameter([str], 'List of fields to return')
+ ]
+
+ # Filter out password and deleted fields
+ can_return = lambda (field, value): field not in ['password', 'deleted']
+ default_fields = dict(filter(can_return, Person.default_fields.items()))
+ return_fields = dict(filter(can_return, Person.all_fields.items()))
+ returns = [return_fields]
+
+ def __init__(self, *args, **kwds):
+ Method.__init__(self, *args, **kwds)
+ # Update documentation with list of default fields returned
+ self.__doc__ += os.linesep.join(self.default_fields.keys())
+
+ def call(self, auth, person_id_or_email_list = None, return_fields = None):
+ # Make sure that only valid fields are specified
+ if return_fields is None:
+ return_fields = self.return_fields
+ elif filter(lambda field: field not in self.return_fields, return_fields):
+ raise PLCInvalidArgument, "Invalid return field specified"
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Only admins can not specify person_id_or_email_list or
+ # specify an empty list.
+ if not person_id_or_email_list and 'admin' not in self.caller['roles']:
+ raise PLCInvalidArgument, "List of accounts to retrieve not specified"
+
+ # Get account information
+ persons = Persons(self.api, person_id_or_email_list)
+
+ # Filter out accounts that are not viewable and turn into list
+ persons = filter(self.caller.can_view, persons.values())
+
+ # Filter out undesired or None fields (XML-RPC cannot marshal
+ # None) and turn each person into a real dict.
+ valid_return_fields_only = lambda (key, value): \
+ key in return_fields and value is not None
+ persons = [dict(filter(valid_return_fields_only, person.items())) \
+ for person in persons]
+
+ return persons
--- /dev/null
+import os
+
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import PasswordAuth
+from PLC.Sites import Site, Sites
+
+class AdmGetSites(Method):
+ """
+ Return an array of structs containing details about all sites. If
+ site_id_list is specified, only the specified sites will be
+ queried.
+
+ If return_fields is specified, only the specified fields will be
+ returned, if set. Otherwise, the default set of fields returned is:
+
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ [Mixed(Site.fields['site_id'],
+ Site.fields['login_base'])],
+ Parameter([str], 'List of fields to return')
+ ]
+
+ # Filter out deleted fields
+ can_return = lambda (field, value): field not in ['deleted']
+ return_fields = dict(filter(can_return, Site.all_fields.items()))
+ returns = [return_fields]
+
+ def __init__(self, *args, **kwds):
+ Method.__init__(self, *args, **kwds)
+ # Update documentation with list of default fields returned
+ self.__doc__ += os.linesep.join(Site.default_fields.keys())
+
+ def call(self, auth, site_id_or_login_base_list = None, return_fields = None):
+ # Make sure that only valid fields are specified
+ if return_fields is None:
+ return_fields = self.return_fields
+ elif filter(lambda field: field not in self.return_fields, return_fields):
+ raise PLCInvalidArgument, "Invalid return field specified"
+
+ # Get site information
+ sites = Sites(self.api, site_id_or_login_base_list, return_fields)
+
+ # Filter out undesired or None fields (XML-RPC cannot marshal
+ # None) and turn each site into a real dict.
+ valid_return_fields_only = lambda (key, value): \
+ key in return_fields and value is not None
+ sites = [dict(filter(valid_return_fields_only, site.items())) \
+ for site in sites.values()]
+
+ return sites
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+from PLC.Roles import Roles
+
+class AdmGrantRoleToPerson(Method):
+ """
+ Grants the specified role to the person.
+
+ PIs can only grant the tech and user roles to users and techs at
+ their sites. Admins can grant any role to any user.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ Roles.fields['role_id']
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email, role_id):
+ # Get all roles
+ roles = Roles(self.api)
+ if role_id not in roles:
+ raise PLCInvalidArgument, "Invalid role ID"
+
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Check if we can update this account
+ if not self.caller.can_update(person):
+ raise PLCPermissionDenied, "Not allowed to update specified account"
+
+ # Can only grant lesser (higher) roles to others
+ if 'admin' not in self.caller['roles'] and \
+ role_id <= min(self.caller['role_ids']):
+ raise PLCInvalidArgument, "Not allowed to grant that role"
+
+ if role_id not in person['role_ids']:
+ person_id = person['person_id']
+ self.api.db.do("INSERT INTO person_roles (person_id, role_id)" \
+ " VALUES(%(person_id)d, %(role_id)d)",
+ locals())
+
+ return 1
--- /dev/null
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+from PLC.Roles import Roles
+
+class AdmIsPersonInRole(Method):
+ """
+ Returns 1 if the specified account has the specified role, 0
+ otherwise. This function differs from AdmGetPersonRoles() in that
+ any authorized user can call it. It is currently restricted to
+ verifying PI roles.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ Roles.fields['role_id']
+ ]
+
+ returns = Parameter(int, "1 if account has role, 0 otherwise")
+
+ status = "useless"
+
+ def call(self, auth, person_id_or_email, role_id):
+ # This is a totally fucked up function. I have no idea why it
+ # exists or who calls it, but here is how it is supposed to
+ # work.
+
+ # Only allow PI roles to be checked
+ roles = Roles(self.api)
+ if not roles.has_key(role_id) or roles[role_id] != "pi":
+ raise PLCInvalidArgument, "Only the PI role may be checked"
+
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+
+ # Rather than raise an error, and indicate whether or not
+ # the person is real, return 0.
+ if not persons:
+ return 0
+
+ person = persons.values()[0]
+
+ if role_id in person['role_ids']:
+ return 1
+
+ return 0
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Auth import PasswordAuth
+
+class AdmRemovePersonFromSite(Method):
+ """
+ Removes the specified person from the specified site. If the
+ person is not a member of the specified site, no error is
+ returned.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ Mixed(Site.fields['site_id'],
+ Site.fields['login_base'])
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email, site_id_or_login_base):
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Get site information
+ sites = Sites(self.api, [site_id_or_login_base])
+ if not sites:
+ raise PLCInvalidArgument, "No such site"
+
+ site = sites.values()[0]
+
+ if site['site_id'] in person['site_ids']:
+ person_id = person['person_id']
+ site_id = site['site_id']
+ self.api.db.do("DELETE FROM person_site" \
+ " WHERE person_id = %(person_id)d" \
+ " AND site_id = %(site_id)d",
+ locals())
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+from PLC.Roles import Roles
+
+class AdmRevokeRoleFromPerson(Method):
+ """
+ Revokes the specified role from the person.
+
+ PIs can only revoke the tech and user roles from users and techs
+ at their sites. Admins can revoke any role from any user.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ Parameter(int, 'Role ID')
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email, role_id):
+ # Get all roles
+ roles = Roles(self.api)
+ if role_id not in roles:
+ raise PLCInvalidArgument, "Invalid role ID"
+
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Check if we can update this account
+ if not self.caller.can_update(person):
+ raise PLCPermissionDenied, "Not allowed to update specified account"
+
+ # Can only revoke lesser (higher) roles from others
+ if 'admin' not in self.caller['roles'] and \
+ role_id <= min(self.caller['role_ids']):
+ raise PLCPermissionDenied, "Not allowed to revoke that role"
+
+ if role_id in person['role_ids']:
+ person_id = person['person_id']
+ self.api.db.do("DELETE FROM person_roles" \
+ " WHERE person_id = %(person_id)d" \
+ " AND role_id = %(role_id)d",
+ locals())
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+
+class AdmSetPersonEnabled(Method):
+ """
+ Enables or disables a person.
+
+ Users and techs can only update themselves. PIs can only update
+ themselves and other non-PIs at their sites. Admins can update
+ anyone.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ Person.fields['enabled']
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email, enabled):
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Check if we can update this account
+ if not self.caller.can_update(person):
+ raise PLCPermissionDenied, "Not allowed to enable specified account"
+
+ person['enabled'] = enabled
+ person.flush()
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Auth import PasswordAuth
+
+class AdmSetPersonPrimarySite(Method):
+ """
+ Makes the specified site the person's primary site. The person
+ must already be a member of the site.
+
+ Admins may update anyone. All others may only update themselves.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ Mixed(Site.fields['site_id'],
+ Site.fields['login_base'])
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email, site_id_or_login_base):
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Non-admins can only update their own primary site
+ if 'admin' not in self.caller['roles'] and \
+ self.caller['person_id'] != person['person_id']:
+ raise PLCPermissionDenied, "Not allowed to update specified account"
+
+ # Get site information
+ sites = Sites(self.api, [site_id_or_login_base])
+ if not sites:
+ raise PLCInvalidArgument, "No such site"
+
+ site = sites.values()[0]
+
+ if site['site_id'] not in person['site_ids']:
+ raise PLCInvalidArgument, "Not a member of the specified site"
+
+ person_id = person['person_id']
+ site_id = site['site_id']
+ self.api.db.do("UPDATE person_site SET is_primary = False" \
+ " WHERE person_id = %(person_id)d",
+ locals())
+ self.api.db.do("UPDATE person_site SET is_primary = True" \
+ " WHERE person_id = %(person_id)d" \
+ " AND site_id = %(site_id)d",
+ locals())
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Auth import PasswordAuth
+
+class AdmUpdateNode(Method):
+ """
+ Updates a node. Only the fields specified in update_fields are
+ updated, all other fields are left untouched.
+
+ To remove a value without setting a new one in its place (for
+ example, to remove an address from the node), specify -1 for int
+ and double fields and 'null' for string fields. hostname and
+ boot_state cannot be unset.
+
+ PIs and techs can only update the nodes at their sites.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi', 'tech']
+
+ can_update = lambda (field, value): field in \
+ ['hostname', 'boot_state', 'model', 'version']
+ update_fields = dict(filter(can_update, Node.fields.items()))
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Node.fields['node_id'],
+ Node.fields['hostname']),
+ update_fields
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, node_id_or_hostname, update_fields):
+ if filter(lambda field: field not in self.update_fields, update_fields):
+ raise PLCInvalidArgument, "Invalid field specified"
+
+ # XML-RPC cannot marshal None, so we need special values to
+ # represent "unset".
+ for key, value in update_fields.iteritems():
+ if value == -1 or value == "null":
+ if key in ['hostname', 'boot_state']:
+ raise PLCInvalidArgument, "hostname and boot_state cannot be unset"
+ update_fields[key] = None
+
+ # Get account information
+ nodes = Nodes(self.api, [node_id_or_hostname])
+ if not nodes:
+ raise PLCInvalidArgument, "No such node"
+
+ node = nodes.values()[0]
+
+ # If we are not an admin, make sure that the caller is a
+ # member of the site at which the node is located.
+ if 'admin' not in self.caller['roles']:
+ # Authenticated function
+ assert self.caller is not None
+
+ if node['site_id'] not in self.caller['site_ids']:
+ raise PLCPermissionDenied, "Not allowed to delete nodes from specified site"
+
+ # Check if we can update this account
+ node = nodes.values()[0]
+ if not self.caller.can_update(node):
+ raise PLCPermissionDenied, "Not allowed to update specified account"
+
+ node.update(update_fields)
+ node.flush()
+
+ return 1
--- /dev/null
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Auth import PasswordAuth
+
+class AdmUpdatePerson(Method):
+ """
+ Updates a person. Only the fields specified in update_fields are
+ updated, all other fields are left untouched.
+
+ To remove a value without setting a new one in its place (for
+ example, to remove an address from the person), specify -1 for int
+ and double fields and 'null' for string fields. first_name and
+ last_name cannot be unset.
+
+ Users and techs can only update themselves. PIs can only update
+ themselves and other non-PIs at their sites.
+
+ Returns 1 if successful, faults otherwise.
+ """
+
+ roles = ['admin', 'pi', 'user', 'tech']
+
+ can_update = lambda (field, value): field in \
+ ['first_name', 'last_name', 'title', 'email',
+ 'password', 'phone', 'url', 'bio', 'accepted_aup']
+ update_fields = dict(filter(can_update, Person.fields.items()))
+
+ accepts = [
+ PasswordAuth(),
+ Mixed(Person.fields['person_id'],
+ Person.fields['email']),
+ update_fields
+ ]
+
+ returns = Parameter(int, '1 if successful')
+
+ def call(self, auth, person_id_or_email, update_fields):
+ if filter(lambda field: field not in self.update_fields, update_fields):
+ raise PLCInvalidArgument, "Invalid field specified"
+
+ # XML-RPC cannot marshal None, so we need special values to
+ # represent "unset".
+ for key, value in update_fields.iteritems():
+ if value == -1 or value == "null":
+ if key in ['first_name', 'last_name']:
+ raise PLCInvalidArgument, "first_name and last_name cannot be unset"
+ update_fields[key] = None
+
+ # Get account information
+ persons = Persons(self.api, [person_id_or_email])
+ if not persons:
+ raise PLCInvalidArgument, "No such account"
+
+ person = persons.values()[0]
+
+ # Authenticated function
+ assert self.caller is not None
+
+ # Check if we can update this account
+ if not self.caller.can_update(person):
+ raise PLCPermissionDenied, "Not allowed to update specified account"
+
+ person.update(update_fields)
+ person.flush()
+
+ return 1
--- /dev/null
+methods = 'AdmAddNode AdmAddPerson AdmAddPersonToSite AdmAddSite AdmAuthCheck AdmDeleteNode AdmDeletePerson AdmDeleteSite AdmGetAllRoles AdmGetNodes AdmGetPersonRoles AdmGetPersonSites AdmGetPersons AdmGetSites AdmGrantRoleToPerson AdmIsPersonInRole AdmRemovePersonFromSite AdmRevokeRoleFromPerson AdmSetPersonEnabled AdmSetPersonPrimarySite AdmUpdateNode AdmUpdatePerson AuthenticatePrincipal system.listMethods system.methodHelp system.methodSignature system.multicall'.split()
--- /dev/null
+from PLC.Method import Method
+from PLC.Parameter import Parameter
+import PLC.Methods
+
+class listMethods(Method):
+ """
+ This method lists all the methods that the XML-RPC server knows
+ how to dispatch.
+ """
+
+ roles = []
+ accepts = []
+ returns = Parameter(list, 'List of methods')
+
+ def __init__(self, api):
+ Method.__init__(self, api)
+ self.name = "system.listMethods"
+
+ def call(self):
+ return self.api.methods
--- /dev/null
+from PLC.Method import Method
+from PLC.Parameter import Parameter
+
+class methodHelp(Method):
+ """
+ Returns help text if defined for the method passed, otherwise
+ returns an empty string.
+ """
+
+ roles = []
+ accepts = [Parameter(str, 'Method name')]
+ returns = Parameter(str, 'Method help')
+
+ def __init__(self, api):
+ Method.__init__(self, api)
+ self.name = "system.methodHelp"
+
+ def call(self, method):
+ function = self.api.callable(method)
+ return function.help()
--- /dev/null
+from PLC.Parameter import Parameter, Mixed
+from PLC.Method import Method, xmlrpc_type
+
+class methodSignature(Method):
+ """
+ Returns an array of known signatures (an array of arrays) for the
+ method name passed. If no signatures are known, returns a
+ none-array (test for type != array to detect missing signature).
+ """
+
+ roles = []
+ accepts = [Parameter(str, "Method name")]
+ returns = [Parameter([str], "Method signature")]
+
+ def __init__(self, api):
+ Method.__init__(self, api)
+ self.name = "system.methodSignature"
+
+ def possible_signatures(self, signature, arg):
+ """
+ Return a list of the possible new signatures given a current
+ signature and the next argument.
+ """
+
+ if isinstance(arg, Mixed):
+ arg_types = [xmlrpc_type(mixed_arg) for mixed_arg in arg]
+ else:
+ arg_types = [xmlrpc_type(arg)]
+
+ return [signature + [arg_type] for arg_type in arg_types]
+
+ def signatures(self, returns, args):
+ """
+ Returns a list of possible signatures given a return value and
+ a set of arguments.
+ """
+
+ signatures = [[xmlrpc_type(returns)]]
+
+ for arg in args:
+ # Create lists of possible new signatures for each current
+ # signature. Reduce the list of lists back down to a
+ # single list.
+ signatures = reduce(lambda a, b: a + b,
+ [self.possible_signatures(signature, arg) \
+ for signature in signatures])
+
+ return signatures
+
+ def call(self, method):
+ function = self.api.callable(method)
+ (min_args, max_args, defaults) = function.args()
+
+ signatures = []
+
+ assert len(max_args) >= len(min_args)
+ for num_args in range(len(min_args), len(max_args) + 1):
+ signatures += self.signatures(function.returns, function.accepts[:num_args])
+
+ return signatures
--- /dev/null
+import sys
+import xmlrpclib
+
+from PLC.Parameter import Parameter, Mixed
+from PLC.Method import Method
+
+class multicall(Method):
+ """
+ Process an array of calls, and return an array of results. Calls
+ should be structs of the form
+
+ {'methodName': string, 'params': array}
+
+ Each result will either be a single-item array containg the result
+ value, or a struct of the form
+
+ {'faultCode': int, 'faultString': string}
+
+ This is useful when you need to make lots of small calls without
+ lots of round trips.
+ """
+
+ roles = []
+ accepts = [[{'methodName': Parameter(str, "Method name"),
+ 'params': Parameter(list, "Method arguments")}]]
+ returns = Mixed([Mixed()],
+ {'faultCode': Parameter(int, "XML-RPC fault code"),
+ 'faultString': Parameter(int, "XML-RPC fault detail")})
+
+ def __init__(self, api):
+ Method.__init__(self, api)
+ self.name = "system.multicall"
+
+ def call(self, calls):
+ # Some error codes, borrowed from xmlrpc-c.
+ REQUEST_REFUSED_ERROR = -507
+
+ results = []
+ for call in calls:
+ try:
+ name = call['methodName']
+ params = call['params']
+ if name == 'system.multicall':
+ errmsg = "Recursive system.multicall forbidden"
+ raise xmlrpclib.Fault(REQUEST_REFUSED_ERROR, errmsg)
+ result = [self.api.call(self.source, name, *params)]
+ except xmlrpclib.Fault, fault:
+ result = {'faultCode': fault.faultCode,
+ 'faultString': fault.faultString}
+ except:
+ errmsg = "%s:%s" % (sys.exc_type, sys.exc_value)
+ result = {'faultCode': 1, 'faultString': errmsg}
+ results.append(result)
+ return results
--- /dev/null
+#
+# Functions for interacting with the nodegroups table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+
+class NodeGroup(Row):
+ """
+ Representation of a row in the nodegroups table. To use, optionally
+ instantiate with a dict of values. Update as you would a
+ dict. Commit to the database with flush().
+ """
+
+ fields = {
+ 'nodegroup_id': Parameter(int, "Node group identifier"),
+ 'name': Parameter(str, "Node group name"),
+ 'description': Parameter(str, "Node group description"),
+ 'is_custom': Parameter(bool, "Is a custom node group (i.e., is not a site node group)")
+ }
+
+ # These fields are derived from join tables and are not
+ # actually in the nodegroups table.
+ join_fields = {
+ 'node_ids': Parameter([int], "List of nodes in this node group"),
+ }
+
+ def __init__(self, api, fields):
+ Row.__init__(self, fields)
+ self.api = api
+
+ def validate_name(self, name):
+ conflicts = NodeGroups(self.api, [name])
+ for nodegroup_id in conflicts:
+ if 'nodegroup_id' not in self or self['nodegroup_id'] != nodegroup_id:
+ raise PLCInvalidArgument, "Node group name already in use"
+
+ def flush(self, commit = True):
+ """
+ Flush changes back to the database.
+ """
+
+ self.validate()
+
+ # Fetch a new nodegroup_id if necessary
+ if 'nodegroup_id' not in self:
+ rows = self.api.db.selectall("SELECT NEXTVAL('nodegroups_nodegroup_id_seq') AS nodegroup_id")
+ if not rows:
+ raise PLCDBError, "Unable to fetch new nodegroup_id"
+ self['nodegroup_id'] = rows[0]['nodegroup_id']
+ insert = True
+ else:
+ insert = False
+
+ # Filter out fields that cannot be set or updated directly
+ fields = dict(filter(lambda (key, value): key in self.fields,
+ self.items()))
+
+ # Parameterize for safety
+ keys = fields.keys()
+ values = [self.api.db.param(key, value) for (key, value) in fields.items()]
+
+ if insert:
+ # Insert new row in nodegroups table
+ sql = "INSERT INTO nodegroups (%s) VALUES (%s)" % \
+ (", ".join(keys), ", ".join(values))
+ else:
+ # Update existing row in sites table
+ columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
+ sql = "UPDATE nodegroups SET " + \
+ ", ".join(columns) + \
+ " WHERE nodegroup_id = %(nodegroup_id)d"
+
+ self.api.db.do(sql, fields)
+
+ if commit:
+ self.api.db.commit()
+
+ def delete(self, commit = True):
+ """
+ Delete existing nodegroup from the database.
+ """
+
+ assert 'nodegroup_id' in self
+
+ # Delete ourself
+ tables = ['nodegroup_nodes', 'override_bootscripts',
+ 'conf_assoc', 'node_root_access']
+
+ if self['is_custom']:
+ tables.append('nodegroups')
+ else:
+ # XXX Cannot delete site node groups yet
+ pass
+
+ for table in tables:
+ self.api.db.do("DELETE FROM %s" \
+ " WHERE nodegroup_id = %(nodegroup_id)" % \
+ table, self)
+
+ if commit:
+ self.api.db.commit()
+
+class NodeGroups(Table):
+ """
+ Representation of row(s) from the nodegroups table in the
+ database.
+ """
+
+ def __init__(self, api, nodegroup_id_or_name_list = None):
+ self.api = api
+
+ # N.B.: Node IDs returned may be deleted.
+ sql = "SELECT nodegroups.*, nodegroup_nodes.node_id" \
+ " FROM nodegroups" \
+ " LEFT JOIN nodegroup_nodes USING (nodegroup_id)"
+
+ if nodegroup_id_or_name_list:
+ # Separate the list into integers and strings
+ nodegroup_ids = filter(lambda nodegroup_id: isinstance(nodegroup_id, (int, long)),
+ nodegroup_id_or_name_list)
+ names = filter(lambda name: isinstance(name, StringTypes),
+ nodegroup_id_or_name_list)
+ sql += " AND (False"
+ if nodegroup_ids:
+ sql += " OR nodegroup_id IN (%s)" % ", ".join(map(str, nodegroup_ids))
+ if names:
+ sql += " OR name IN (%s)" % ", ".join(api.db.quote(names)).lower()
+ sql += ")"
+
+ rows = self.api.db.selectall(sql)
+ for row in rows:
+ if self.has_key(row['nodegroup_id']):
+ nodegroup = self[row['nodegroup_id']]
+ nodegroup.update(row)
+ else:
+ self[row['nodegroup_id']] = NodeGroup(api, row)
--- /dev/null
+#
+# Functions for interacting with the nodenetworks table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from types import StringTypes
+import socket
+import struct
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+import PLC.Nodes
+
+def in_same_network(address1, address2, netmask):
+ """
+ Returns True if two IPv4 addresses are in the same network. Faults
+ if an address is invalid.
+ """
+
+ address1 = struct.unpack('>L', socket.inet_aton(address1))[0]
+ address2 = struct.unpack('>L', socket.inet_aton(address2))[0]
+ netmask = struct.unpack('>L', socket.inet_aton(netmask))[0]
+
+ return (address1 & netmask) == (address2 & netmask)
+
+class NodeNetwork(Row):
+ """
+ Representation of a row in the nodenetworks table. To use, optionally
+ instantiate with a dict of values. Update as you would a
+ dict. Commit to the database with flush().
+ """
+
+ fields = {
+ 'nodenetwork_id': Parameter(int, "Node interface identifier"),
+ 'method': Parameter(str, "Addressing method (e.g., 'static' or 'dhcp')"),
+ 'type': Parameter(str, "Address type (e.g., 'ipv4')"),
+ 'ip': Parameter(str, "IP address"),
+ 'mac': Parameter(str, "MAC address"),
+ 'gateway': Parameter(str, "IP address of primary gateway"),
+ 'network': Parameter(str, "Subnet address"),
+ 'broadcast': Parameter(str, "Network broadcast address"),
+ 'netmask': Parameter(str, "Subnet mask"),
+ 'dns1': Parameter(str, "IP address of primary DNS server"),
+ 'dns2': Parameter(str, "IP address of secondary DNS server"),
+ # XXX Should be an int (bps)
+ 'bwlimit': Parameter(str, "Bandwidth limit"),
+ 'hostname': Parameter(str, "(Optional) Hostname"),
+ }
+
+ # These fields are derived from join tables and are not
+ # actually in the nodenetworks table.
+ join_fields = {
+ 'node_id': Parameter(int, "Node associated with this interface (if any)"),
+ 'is_primary': Parameter(bool, "Is the primary interface for this node"),
+ }
+
+ methods = ['static', 'dhcp', 'proxy', 'tap', 'ipmi', 'unknown']
+
+ types = ['ipv4']
+
+ bwlimits = ['-1',
+ '100kbit', '250kbit', '500kbit',
+ '1mbit', '2mbit', '5mbit',
+ '10mbit', '20mbit', '50mbit',
+ '100mbit']
+
+ def __init__(self, api, fields):
+ Row.__init__(self, fields)
+ self.api = api
+
+ def validate_method(self, method):
+ if method not in self.methods:
+ raise PLCInvalidArgument, "Invalid addressing method"
+
+ def validate_type(self, type):
+ if type not in self.types:
+ raise PLCInvalidArgument, "Invalid address type"
+
+ def validate_ip(self, ip):
+ try:
+ ip = socket.inet_ntoa(socket.inet_aton(ip))
+ except socket.error:
+ raise PLCInvalidArgument, "Invalid IP address " + ip
+
+ return ip
+
+ def validate_mac(self, mac):
+ try:
+ bytes = mac.split(":")
+ if len(bytes) < 6:
+ raise Exception
+ for i, byte in enumerate(bytes):
+ byte = int(byte, 16)
+ if byte < 0 or byte > 255:
+ raise Exception
+ bytes[i] = "%02x" % byte
+ mac = ":".join(bytes)
+ except:
+ raise PLCInvalidArgument, "Invalid MAC address"
+
+ return mac
+
+ validate_gateway = validate_ip
+ validate_network = validate_ip
+ validate_broadcast = validate_ip
+ validate_netmask = validate_ip
+ validate_dns1 = validate_ip
+ validate_dns2 = validate_ip
+
+ def validate_bwlimit(self, bwlimit):
+ if bwlimit not in self.bwlimits:
+ raise PLCInvalidArgument, "Invalid bandwidth limit"
+
+ def validate_hostname(self, hostname):
+ # Optional
+ if not hostname:
+ return hostname
+
+ # Validate hostname, and check for conflicts with a node hostname
+ return PLC.Nodes.Node.validate_hostname(self, hostname)
+
+ def flush(self, commit = True):
+ """
+ Flush changes back to the database.
+ """
+
+ # Validate all specified fields
+ self.validate()
+
+ try:
+ method = self['method']
+ self['type']
+ except KeyError:
+ raise PLCInvalidArgument, "method and type must both be specified"
+
+ if method == "proxy" or method == "tap":
+ if 'mac' in self:
+ raise PLCInvalidArgument, "For %s method, mac should not be specified" % method
+ if 'ip' not in self:
+ raise PLCInvalidArgument, "For %s method, ip is required" % method
+ if method == "tap" and 'gateway' not in self:
+ raise PLCInvalidArgument, "For tap method, gateway is required and should be " \
+ "the IP address of the node that proxies for this address"
+ # Should check that the proxy address is reachable, but
+ # there's no way to tell if the only primary interface is
+ # DHCP!
+
+ elif method == "static":
+ for key in ['ip', 'gateway', 'network', 'broadcast', 'netmask', 'dns1']:
+ if key not in self:
+ raise PLCInvalidArgument, "For static method, %s is required" % key
+ locals()[key] = self[key]
+ if not in_same_network(ip, network, netmask):
+ raise PLCInvalidArgument, "IP address %s is inconsistent with network %s/%s" % \
+ (ip, network, netmask)
+ if not in_same_network(broadcast, network, netmask):
+ raise PLCInvalidArgument, "Broadcast address %s is inconsistent with network %s/%s" % \
+ (broadcast, network, netmask)
+ if not in_same_network(ip, gateway, netmask):
+ raise PLCInvalidArgument, "Gateway %s is not reachable from %s/%s" % \
+ (gateway, ip, netmask)
+
+ elif method == "ipmi":
+ if 'ip' not in self:
+ raise PLCInvalidArgument, "For ipmi method, ip is required"
+
+ # Fetch a new nodenetwork_id if necessary
+ if 'nodenetwork_id' not in self:
+ rows = self.api.db.selectall("SELECT NEXTVAL('nodenetworks_nodenetwork_id_seq') AS nodenetwork_id")
+ if not rows:
+ raise PLCDBError("Unable to fetch new nodenetwork_id")
+ self['nodenetwork_id'] = rows[0]['nodenetwork_id']
+ insert = True
+ else:
+ insert = False
+
+ # Filter out fields that cannot be set or updated directly
+ fields = dict(filter(lambda (key, value): key in self.fields,
+ self.items()))
+
+ # Parameterize for safety
+ keys = fields.keys()
+ values = [self.api.db.param(key, value) for (key, value) in fields.items()]
+
+ if insert:
+ # Insert new row in nodenetworks table
+ sql = "INSERT INTO nodenetworks (%s) VALUES (%s)" % \
+ (", ".join(keys), ", ".join(values))
+ else:
+ # Update existing row in sites table
+ columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
+ sql = "UPDATE nodenetworks SET " + \
+ ", ".join(columns) + \
+ " WHERE nodenetwork_id = %(nodenetwork_id)d"
+
+ self.api.db.do(sql, fields)
+
+ if commit:
+ self.api.db.commit()
+
+ def delete(self, commit = True):
+ """
+ Delete existing nodenetwork.
+ """
+
+ assert 'nodenetwork_id' in self
+
+ # Delete ourself
+ for table in ['node_nodenetworks', 'nodenetworks']:
+ self.api.db.do("DELETE FROM %s" \
+ " WHERE nodenetwork_id = %d" % \
+ (table, self['nodenetwork_id']))
+
+ if commit:
+ self.api.db.commit()
+
+class NodeNetworks(Table):
+ """
+ Representation of row(s) from the nodenetworks table in the
+ database.
+ """
+
+ def __init__(self, api, nodenetwork_id_or_hostname_list = None):
+ self.api = api
+
+ # N.B.: Node IDs returned may be deleted.
+ sql = "SELECT nodenetworks.*" \
+ ", node_nodenetworks.node_id" \
+ ", node_nodenetworks.is_primary" \
+ " FROM nodenetworks" \
+ " LEFT JOIN node_nodenetworks USING (nodenetwork_id)"
+
+ if nodenetwork_id_or_hostname_list:
+ # Separate the list into integers and strings
+ nodenetwork_ids = filter(lambda nodenetwork_id: isinstance(nodenetwork_id, (int, long)),
+ nodenetwork_id_or_hostname_list)
+ hostnames = filter(lambda hostname: isinstance(hostname, StringTypes),
+ nodenetwork_id_or_hostname_list)
+ sql += " WHERE (False"
+ if nodenetwork_ids:
+ sql += " OR nodenetwork_id IN (%s)" % ", ".join(map(str, nodenetwork_ids))
+ if hostnames:
+ sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
+ sql += ")"
+
+ rows = self.api.db.selectall(sql)
+ for row in rows:
+ if self.has_key(row['nodenetwork_id']):
+ nodenetwork = self[row['nodenetwork_id']]
+ nodenetwork.update(row)
+ else:
+ self[row['nodenetwork_id']] = NodeNetwork(api, row)
--- /dev/null
+#
+# Functions for interacting with the nodes table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from types import StringTypes
+import re
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.BootStates import BootStates
+
+class Node(Row):
+ """
+ Representation of a row in the nodes table. To use, optionally
+ instantiate with a dict of values. Update as you would a
+ dict. Commit to the database with flush().
+ """
+
+ fields = {
+ 'node_id': Parameter(int, "Node identifier"),
+ 'hostname': Parameter(str, "Fully qualified hostname"),
+ 'boot_state': Parameter(str, "Boot state"),
+ 'model': Parameter(str, "Make and model of the actual machine"),
+ 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot"),
+ 'version': Parameter(str, "Apparent Boot CD version"),
+ 'ssh_rsa_key': Parameter(str, "Last known SSH host key"),
+ 'date_created': Parameter(str, "Date and time when node entry was created"),
+ 'deleted': Parameter(bool, "Has been deleted"),
+ 'key': Parameter(str, "(Admin only) Node key"),
+ 'session': Parameter(str, "(Admin only) Node session value"),
+ }
+
+ # These fields are derived from join tables and are not actually
+ # in the nodes table.
+ join_fields = {
+ 'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
+ }
+
+ # These fields are derived from join tables and are not returned
+ # by default unless specified.
+ extra_fields = {
+ 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
+ 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
+ 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
+ # XXX Too inefficient
+ # 'slice_ids': Parameter([int], "List of slices on this node"),
+ 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
+ 'site_id': Parameter([int], "Site at which this node is located"),
+ }
+
+ # Primary interface values
+ primary_nodenetwork_fields = dict(filter(lambda (key, value): \
+ key not in ['node_id', 'is_primary', 'hostname'],
+ NodeNetwork.fields.items()))
+
+ extra_fields.update(primary_nodenetwork_fields)
+
+ default_fields = dict(fields.items() + join_fields.items())
+ all_fields = dict(default_fields.items() + extra_fields.items())
+
+ def __init__(self, api, fields):
+ Row.__init__(self, fields)
+ self.api = api
+
+ def validate_hostname(self, hostname):
+ # 1. Each part begins and ends with a letter or number.
+ # 2. Each part except the last can contain letters, numbers, or hyphens.
+ # 3. Each part is between 1 and 64 characters, including the trailing dot.
+ # 4. At least two parts.
+ # 5. Last part can only contain between 2 and 6 letters.
+ good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
+ r'[a-z]{2,6}$'
+ if not hostname or \
+ not re.match(good_hostname, hostname, re.IGNORECASE):
+ raise PLCInvalidArgument, "Invalid hostname"
+
+ conflicts = Nodes(self.api, [hostname])
+ for node_id, node in conflicts.iteritems():
+ if not node['deleted'] and ('node_id' not in self or self['node_id'] != node_id):
+ raise PLCInvalidArgument, "Hostname already in use"
+
+ # Check for conflicts with a nodenetwork hostname
+ conflicts = NodeNetworks(self.api, [hostname])
+ for nodenetwork_id in conflicts:
+ if 'nodenetwork_ids' not in self or nodenetwork_id not in self['nodenetwork_ids']:
+ raise PLCInvalidArgument, "Hostname already in use"
+
+ return hostname
+
+ def validate_boot_state(self, boot_state):
+ if boot_state not in BootStates(self.api):
+ raise PLCInvalidArgument, "Invalid boot state"
+
+ return boot_state
+
+ def flush(self, commit = True):
+ """
+ Flush changes back to the database.
+ """
+
+ self.validate()
+
+ # Fetch a new node_id if necessary
+ if 'node_id' not in self:
+ rows = self.api.db.selectall("SELECT NEXTVAL('nodes_node_id_seq') AS node_id")
+ if not rows:
+ raise PLCDBError, "Unable to fetch new node_id"
+ self['node_id'] = rows[0]['node_id']
+ insert = True
+ else:
+ insert = False
+
+ # Filter out fields that cannot be set or updated directly
+ fields = dict(filter(lambda (key, value): key in self.fields,
+ self.items()))
+
+ # Parameterize for safety
+ keys = fields.keys()
+ values = [self.api.db.param(key, value) for (key, value) in fields.items()]
+
+ if insert:
+ # Insert new row in nodes table
+ sql = "INSERT INTO nodes (%s) VALUES (%s)" % \
+ (", ".join(keys), ", ".join(values))
+ else:
+ # Update existing row in nodes table
+ columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
+ sql = "UPDATE nodes SET " + \
+ ", ".join(columns) + \
+ " WHERE node_id = %(node_id)d"
+
+ self.api.db.do(sql, fields)
+
+ if commit:
+ self.api.db.commit()
+
+ def delete(self, commit = True):
+ """
+ Delete existing node.
+ """
+
+ assert 'node_id' in self
+
+ # Delete all nodenetworks
+ nodenetworks = NodeNetworks(self.api, self['nodenetwork_ids'])
+ for nodenetwork in nodenetworks.values():
+ nodenetwork.delete(commit = False)
+
+ # Clean up miscellaneous join tables
+ for table in ['nodegroup_nodes', 'pod_hash', 'conf_assoc',
+ 'node_root_access', 'dslice03_slicenode',
+ 'pcu_ports']:
+ self.api.db.do("DELETE FROM %s" \
+ " WHERE node_id = %d" % \
+ (table, self['node_id']))
+
+ # Mark as deleted
+ self['deleted'] = True
+ self.flush(commit)
+
+class Nodes(Table):
+ """
+ Representation of row(s) from the nodes table in the
+ database.
+ """
+
+ def __init__(self, api, node_id_or_hostname_list = None, extra_fields = []):
+ self.api = api
+
+ sql = "SELECT nodes.*, node_nodenetworks.nodenetwork_id"
+
+ # For compatibility and convenience, support returning primary
+ # interface values directly in the Node structure.
+ extra_nodenetwork_fields = set(extra_fields).intersection(Node.primary_nodenetwork_fields)
+
+ # N.B.: Joined IDs may be marked as deleted in their primary tables
+ join_tables = {
+ # extra_field: (extra_table, extra_column, join_using)
+ 'nodegroup_ids': ('nodegroup_nodes', 'nodegroup_id', 'node_id'),
+ 'conf_file_ids': ('conf_assoc', 'conf_file_id', 'node_id'),
+ 'root_person_ids': ('node_root_access', 'person_id AS root_person_id', 'node_id'),
+ 'slice_ids': ('dslice03_slicenode', 'slice_id', 'node_id'),
+ 'pcu_ids': ('pcu_ports', 'pcu_id', 'node_id'),
+ }
+
+ extra_fields = filter(join_tables.has_key, extra_fields)
+ extra_tables = ["%s USING (%s)" % \
+ (join_tables[field][0], join_tables[field][2]) \
+ for field in extra_fields]
+ extra_columns = ["%s.%s" % \
+ (join_tables[field][0], join_tables[field][1]) \
+ for field in extra_fields]
+
+ if extra_columns:
+ sql += ", " + ", ".join(extra_columns)
+
+ sql += " FROM nodes" \
+ " LEFT JOIN node_nodenetworks USING (node_id)"
+
+ if extra_tables:
+ sql += " LEFT JOIN " + " LEFT JOIN ".join(extra_tables)
+
+ sql += " WHERE deleted IS False"
+
+ if node_id_or_hostname_list:
+ # Separate the list into integers and strings
+ node_ids = filter(lambda node_id: isinstance(node_id, (int, long)),
+ node_id_or_hostname_list)
+ hostnames = filter(lambda hostname: isinstance(hostname, StringTypes),
+ node_id_or_hostname_list)
+ sql += " AND (False"
+ if node_ids:
+ sql += " OR node_id IN (%s)" % ", ".join(map(str, node_ids))
+ if hostnames:
+ sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
+ sql += ")"
+
+ # So that if the node has a primary interface, it is listed
+ # first.
+ if 'nodenetwork_ids' in extra_fields:
+ sql += " ORDER BY node_nodenetworks.is_primary DESC"
+
+ rows = self.api.db.selectall(sql)
+ for row in rows:
+ if self.has_key(row['node_id']):
+ node = self[row['node_id']]
+ node.update(row)
+ else:
+ self[row['node_id']] = Node(api, row)
+
+ # XXX Should instead have a site_node join table that is
+ # magically taken care of above.
+ if rows:
+ sql = "SELECT node_id, sites.site_id FROM nodegroup_nodes" \
+ " INNER JOIN sites USING (nodegroup_id)" \
+ " WHERE node_id IN (%s)" % ", ".join(map(str, self.keys()))
+
+ rows = self.api.db.selectall(sql, self)
+ for row in rows:
+ assert self.has_key(row['node_id'])
+ node = self[row['node_id']]
+ node.update(row)
+
+ # Fill in optional primary interface fields for each node
+ if extra_nodenetwork_fields:
+ # More efficient to get all the nodenetworks at once
+ nodenetwork_ids = []
+ for node in self.values():
+ nodenetwork_ids += node['nodenetwork_ids']
+
+ # Remove duplicates
+ nodenetwork_ids = set(nodenetwork_ids)
+
+ # Get all nodenetwork information
+ nodenetworks = NodeNetworks(self.api, nodenetwork_ids)
+
+ for node in self.values():
+ for nodenetwork_id in node['nodenetwork_ids']:
+ nodenetwork = nodenetworks[nodenetwork_id]
+ if nodenetwork['is_primary']:
+ for field in extra_nodenetwork_fields:
+ node[field] = nodenetwork[field]
+ break
--- /dev/null
+#
+# Functions for interacting with the pcus table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+
+class PCU(Row):
+ """
+ Representation of a row in the pcu table. To use,
+ instantiate with a dict of values.
+ """
+
+ fields = {
+ 'pcu_id': Parameter(int, "Node group identifier"),
+ 'hostname': Parameter(str, "Fully qualified hostname"),
+ }
+
+ # These fields are derived from join tables and are not
+ # actually in the pcu table.
+ join_fields = {
+ 'node_ids': Parameter([int], "List of nodes that this PCU controls"),
+ }
+
+ def __init__(self, api, fields):
+ Row.__init__(self, fields)
+ self.api = api
+
+ def flush(self, commit = True):
+ """
+ Commit changes back to the database.
+ """
+
+ self.validate()
+
+ # Fetch a new pcu_id if necessary
+ if 'pcu_id' not in self:
+ rows = self.api.db.selectall("SELECT NEXTVAL('pcu_pcu_id_seq') AS pcu_id")
+ if not rows:
+ raise PLCDBError, "Unable to fetch new pcu_id"
+ self['pcu_id'] = rows[0]['pcu_id']
+ insert = True
+ else:
+ insert = False
+
+ # Filter out unknown fields
+ fields = dict(filter(lambda (key, value): key in self.fields,
+ self.items()))
+
+ # Parameterize for safety
+ keys = fields.keys()
+ values = [self.api.db.param(key, value) for (key, value) in fields.items()]
+
+ if insert:
+ # Insert new row in pcu table
+ sql = "INSERT INTO pcu (%s) VALUES (%s)" % \
+ (", ".join(keys), ", ".join(values))
+ else:
+ # Update existing row in sites table
+ columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
+ sql = "UPDATE pcu SET " + \
+ ", ".join(columns) + \
+ " WHERE pcu_id = %(pcu_id)d"
+
+ self.api.db.do(sql, fields)
+
+ if commit:
+ self.api.db.commit()
+
+ def delete(self, commit = True):
+ """
+ Delete existing PCU.
+ """
+
+ assert 'pcu_id' in self
+
+ # Delete ourself
+ for table in ['pcu_ports', 'pcu']:
+ self.api.db.do("DELETE FROM %s" \
+ " WHERE nodenetwork_id = %(pcu_id)" % \
+ table, self)
+
+ if commit:
+ self.api.db.commit()
+
+class PCUs(Table):
+ """
+ Representation of row(s) from the pcu table in the
+ database.
+ """
+
+ def __init__(self, api, pcu_id_or_hostname_list = None):
+ self.api = api
+
+ # N.B.: Node IDs returned may be deleted.
+ sql = "SELECT pcu.*, pcu_ports.node_id" \
+ " FROM pcu" \
+ " LEFT JOIN pcu_ports USING (pcu_id)"
+
+ if pcu_id_or_hostname_list:
+ # Separate the list into integers and strings
+ pcu_ids = filter(lambda pcu_id: isinstance(pcu_id, (int, long)),
+ pcu_id_or_hostname_list)
+ hostnames = filter(lambda hostname: isinstance(hostname, StringTypes),
+ pcu_id_or_hostname_list)
+ sql += " AND (False"
+ if pcu_ids:
+ sql += " OR pcu_id IN (%s)" % ", ".join(map(str, pcu_ids))
+ if hostnames:
+ sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
+ sql += ")"
+
+ rows = self.api.db.selectall(sql, locals())
+ for row in rows:
+ if self.has_key(row['pcu_id']):
+ pcu = self[row['pcu_id']]
+ pcu.update(row)
+ else:
+ self[row['pcu_id']] = PCU(api, row)
--- /dev/null
+#
+# Shared type definitions
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+class Parameter:
+ """
+ Typed value wrapper. Use in accepts and returns to document method
+ parameters. Set the optional and default attributes for
+ sub-parameters (i.e., dict fields).
+ """
+
+ def __init__(self, type, doc = "", optional = True, default = None):
+ (self.type, self.doc, self.optional, self.default) = \
+ (type, doc, optional, default)
+
+ def __repr__(self):
+ return repr(self.type)
+
+class Mixed(tuple):
+ """
+ A list (technically, a tuple) of types. Use in accepts and returns
+ to document method parameters that may return mixed types.
+ """
+
+ def __new__(cls, *types):
+ return tuple.__new__(cls, types)
--- /dev/null
+#
+# Functions for interacting with the persons table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from types import StringTypes
+from datetime import datetime
+import md5
+import time
+from random import Random
+import re
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.Roles import Roles
+from PLC.Addresses import Address, Addresses
+from PLC.Keys import Key, Keys
+from PLC import md5crypt
+
+class Person(Row):
+ """
+ Representation of a row in the persons table. To use, optionally
+ instantiate with a dict of values. Update as you would a
+ dict. Commit to the database with flush().
+ """
+
+ fields = {
+ 'person_id': Parameter(int, "Account identifier"),
+ 'first_name': Parameter(str, "Given name"),
+ 'last_name': Parameter(str, "Surname"),
+ 'title': Parameter(str, "Title"),
+ 'email': Parameter(str, "Primary e-mail address"),
+ 'phone': Parameter(str, "Telephone number"),
+ 'url': Parameter(str, "Home page"),
+ 'bio': Parameter(str, "Biography"),
+ 'accepted_aup': Parameter(bool, "Has accepted the AUP"),
+ 'enabled': Parameter(bool, "Has been enabled"),
+ 'deleted': Parameter(bool, "Has been deleted"),
+ 'password': Parameter(str, "Account password in crypt() form"),
+ 'last_updated': Parameter(str, "Date and time of last update"),
+ 'date_created': Parameter(str, "Date and time when account was created"),
+ }
+
+ # These fields are derived from join tables and are not actually
+ # in the persons table.
+ join_fields = {
+ 'role_ids': Parameter([int], "List of role identifiers"),
+ 'roles': Parameter([str], "List of roles"),
+ 'site_ids': Parameter([int], "List of site identifiers"),
+ }
+
+ # These fields are derived from join tables and are not returned
+ # by default unless specified.
+ extra_fields = {
+ 'address_ids': Parameter([int], "List of address identifiers"),
+ 'key_ids': Parameter([int], "List of key identifiers"),
+ 'slice_ids': Parameter([int], "List of slice identifiers"),
+ }
+
+ default_fields = dict(fields.items() + join_fields.items())
+ all_fields = dict(default_fields.items() + extra_fields.items())
+
+ def __init__(self, api, fields):
+ Row.__init__(self, fields)
+ self.api = api
+
+ def validate_email(self, email):
+ """
+ Validate email address. Stolen from Mailman.
+ """
+
+ invalid_email = PLCInvalidArgument("Invalid e-mail address")
+ email_badchars = r'[][()<>|;^,\200-\377]'
+
+ # Pretty minimal, cheesy check. We could do better...
+ if not email or email.count(' ') > 0:
+ raise invalid_email
+ if re.search(email_badchars, email) or email[0] == '-':
+ raise invalid_email
+
+ email = email.lower()
+ at_sign = email.find('@')
+ if at_sign < 1:
+ raise invalid_email
+ user = email[:at_sign]
+ rest = email[at_sign+1:]
+ domain = rest.split('.')
+
+ # This means local, unqualified addresses, are no allowed
+ if not domain:
+ raise invalid_email
+ if len(domain) < 2:
+ raise invalid_email
+
+ conflicts = Persons(self.api, [email])
+ for person_id, person in conflicts.iteritems():
+ if not person['deleted'] and ('person_id' not in self or self['person_id'] != person_id):
+ raise PLCInvalidArgument, "E-mail address already in use"
+
+ return email
+
+ def validate_password(self, password):
+ """
+ Encrypt password if necessary before committing to the
+ database.
+ """
+
+ if len(password) > len(md5crypt.MAGIC) and \
+ password[0:len(md5crypt.MAGIC)] == md5crypt.MAGIC:
+ return password
+ else:
+ # Generate a somewhat unique 2 character salt string
+ salt = str(time.time()) + str(Random().random())
+ salt = md5.md5(salt).hexdigest()[:8]
+ return md5crypt.md5crypt(password, salt)
+
+ def validate_role_ids(self, role_ids):
+ """
+ Ensure that the specified role_ids are all valid.
+ """
+
+ roles = Roles(self.api)
+ for role_id in role_ids:
+ if role_id not in roles:
+ raise PLCInvalidArgument, "No such role"
+
+ return role_ids
+
+ def validate_site_ids(self, site_ids):
+ """
+ Ensure that the specified site_ids are all valid.
+ """
+
+ sites = Sites(self.api, site_ids)
+ for site_id in site_ids:
+ if site_id not in sites:
+ raise PLCInvalidArgument, "No such site"
+
+ return site_ids
+
+ def can_update(self, person):
+ """
+ Returns true if we can update the specified person. We can
+ update a person if:
+
+ 1. We are the person.
+ 2. We are an admin.
+ 3. We are a PI and the person is a user or tech or at
+ one of our sites.
+ """
+
+ if self['person_id'] == person['person_id']:
+ return True
+
+ if 'admin' in self['roles']:
+ return True
+
+ if 'pi' in self['roles']:
+ if set(self['site_ids']).intersection(person['site_ids']):
+ # Can update people with higher role IDs
+ return min(self['role_ids']) < min(person['role_ids'])
+
+ return False
+
+ def can_view(self, person):
+ """
+ Returns true if we can view the specified person. We can
+ view a person if:
+
+ 1. We are the person.
+ 2. We are an admin.
+ 3. We are a PI and the person is at one of our sites.
+ """
+
+ if self.can_update(person):
+ return True
+
+ if 'pi' in self['roles']:
+ if set(self['site_ids']).intersection(person['site_ids']):
+ # Can view people with equal or higher role IDs
+ return min(self['role_ids']) <= min(person['role_ids'])
+
+ return False
+
+ def flush(self, commit = True):
+ """
+ Commit changes back to the database.
+ """
+
+ self.validate()
+
+ # Fetch a new person_id if necessary
+ if 'person_id' not in self:
+ rows = self.api.db.selectall("SELECT NEXTVAL('persons_person_id_seq') AS person_id")
+ if not rows:
+ raise PLCDBError, "Unable to fetch new person_id"
+ self['person_id'] = rows[0]['person_id']
+ insert = True
+ else:
+ insert = False
+
+ # Filter out fields that cannot be set or updated directly
+ fields = dict(filter(lambda (key, value): key in self.fields,
+ self.items()))
+
+ # Parameterize for safety
+ keys = fields.keys()
+ values = [self.api.db.param(key, value) for (key, value) in fields.items()]
+
+ if insert:
+ # Insert new row in persons table
+ sql = "INSERT INTO persons (%s) VALUES (%s)" % \
+ (", ".join(keys), ", ".join(values))
+ else:
+ # Update existing row in persons table
+ columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
+ sql = "UPDATE persons SET " + \
+ ", ".join(columns) + \
+ " WHERE person_id = %(person_id)d"
+
+ self.api.db.do(sql, fields)
+
+ if commit:
+ self.api.db.commit()
+
+ def delete(self, commit = True):
+ """
+ Delete existing account.
+ """
+
+ assert 'person_id' in self
+
+ # Make sure extra fields are present
+ persons = Persons(self.api, [self['person_id']],
+ ['address_ids', 'key_ids'])
+ assert persons
+ self.update(persons.values()[0])
+
+ # Delete all addresses
+ addresses = Addresses(self.api, self['address_ids'])
+ for address in addresses.values():
+ address.delete(commit = False)
+
+ # Delete all keys
+ keys = Keys(self.api, self['key_ids'])
+ for key in keys.values():
+ key.delete(commit = False)
+
+ # Clean up miscellaneous join tables
+ for table in ['person_roles', 'person_capabilities', 'person_site',
+ 'node_root_access', 'dslice03_sliceuser']:
+ self.api.db.do("DELETE FROM %s" \
+ " WHERE person_id = %d" % \
+ (table, self['person_id']))
+
+ # Mark as deleted
+ self['deleted'] = True
+ self.flush(commit)
+
+class Persons(Table):
+ """
+ Representation of row(s) from the persons table in the
+ database. Specify deleted and/or enabled to force a match on
+ whether a person is deleted and/or enabled. Default is to match on
+ non-deleted accounts.
+ """
+
+ def __init__(self, api, person_id_or_email_list = None, extra_fields = [], deleted = False, enabled = None):
+ self.api = api
+
+ role_max = Roles.role_max
+
+ # N.B.: Site IDs returned may be deleted. Persons returned are
+ # never deleted, but may not be enabled.
+ sql = "SELECT persons.*" \
+ ", roles.role_id, roles.name AS role" \
+ ", person_site.site_id" \
+
+ # N.B.: Joined IDs may be marked as deleted in their primary tables
+ join_tables = {
+ # extra_field: (extra_table, extra_column, join_using)
+ 'address_ids': ('person_address', 'address_id', 'person_id'),
+ 'key_ids': ('person_keys', 'key_id', 'person_id'),
+ 'slice_ids': ('dslice03_sliceuser', 'slice_id', 'person_id'),
+ }
+
+ extra_fields = filter(join_tables.has_key, extra_fields)
+ extra_tables = ["%s USING (%s)" % \
+ (join_tables[field][0], join_tables[field][2]) \
+ for field in extra_fields]
+ extra_columns = ["%s.%s" % \
+ (join_tables[field][0], join_tables[field][1]) \
+ for field in extra_fields]
+
+ if extra_columns:
+ sql += ", " + ", ".join(extra_columns)
+
+ sql += " FROM persons" \
+ " LEFT JOIN person_roles USING (person_id)" \
+ " LEFT JOIN roles USING (role_id)" \
+ " LEFT JOIN person_site USING (person_id)"
+
+ if extra_tables:
+ sql += " LEFT JOIN " + " LEFT JOIN ".join(extra_tables)
+
+ # So that people with no roles have empty role_ids and roles values
+ sql += " WHERE (role_id IS NULL or role_id <= %(role_max)d)"
+
+ if deleted is not None:
+ sql += " AND deleted IS %(deleted)s"
+
+ if enabled is not None:
+ sql += " AND enabled IS %(enabled)s"
+
+ if person_id_or_email_list:
+ # Separate the list into integers and strings
+ person_ids = filter(lambda person_id: isinstance(person_id, (int, long)),
+ person_id_or_email_list)
+ emails = filter(lambda email: isinstance(email, StringTypes),
+ person_id_or_email_list)
+ sql += " AND (False"
+ if person_ids:
+ sql += " OR person_id IN (%s)" % ", ".join(map(str, person_ids))
+ if emails:
+ # Case insensitive e-mail address comparison
+ sql += " OR lower(email) IN (%s)" % ", ".join(api.db.quote(emails)).lower()
+ sql += ")"
+
+ # The first site_id in the site_ids list is the primary site
+ # of the user. See AdmGetPersonSites().
+ sql += " ORDER BY person_site.is_primary DESC"
+
+ rows = self.api.db.selectall(sql, locals())
+ for row in rows:
+ if self.has_key(row['person_id']):
+ person = self[row['person_id']]
+ person.update(row)
+ else:
+ self[row['person_id']] = Person(api, row)
--- /dev/null
+#
+# PostgreSQL database interface. Sort of like DBI(3) (Database
+# independent interface for Perl).
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import pgdb
+from types import StringTypes, NoneType
+import traceback
+import commands
+from pprint import pformat
+
+from PLC.Debug import profile, log
+from PLC.Faults import *
+
+class PostgreSQL:
+ def __init__(self, api):
+ self.api = api
+
+ # Initialize database connection
+ self.db = pgdb.connect(user = api.config.PLC_DB_USER,
+ password = api.config.PLC_DB_PASSWORD,
+ host = "%s:%d" % (api.config.PLC_DB_HOST, api.config.PLC_DB_PORT),
+ database = api.config.PLC_DB_NAME)
+ self.cursor = self.db.cursor()
+
+ (self.rowcount, self.description, self.lastrowid) = \
+ (None, None, None)
+
+ def quote(self, params):
+ """
+ Returns quoted version(s) of the specified parameter(s).
+ """
+
+ # pgdb._quote functions are good enough for general SQL quoting
+ if hasattr(params, 'has_key'):
+ params = pgdb._quoteitem(params)
+ elif isinstance(params, list) or isinstance(params, tuple):
+ params = map(pgdb._quote, params)
+ else:
+ params = pgdb._quote(params)
+
+ return params
+
+ quote = classmethod(quote)
+
+ def param(self, name, value):
+ # None is converted to the unquoted string NULL
+ if isinstance(value, NoneType):
+ conversion = "s"
+ # True and False are also converted to unquoted strings
+ elif isinstance(value, bool):
+ conversion = "s"
+ elif isinstance(value, float):
+ conversion = "f"
+ elif not isinstance(value, StringTypes):
+ conversion = "d"
+ else:
+ conversion = "s"
+
+ return '%(' + name + ')' + conversion
+
+ param = classmethod(param)
+
+ def begin_work(self):
+ # Implicit in pgdb.connect()
+ pass
+
+ def commit(self):
+ self.db.commit()
+
+ def rollback(self):
+ self.db.rollback()
+
+ def do(self, query, params = None):
+ self.execute(query, params)
+ return self.rowcount
+
+ def last_insert_id(self):
+ return self.lastrowid
+
+ def execute(self, query, params = None):
+ self.execute_array(query, (params,))
+
+ def execute_array(self, query, param_seq):
+ cursor = self.cursor
+ try:
+ cursor.executemany(query, param_seq)
+ (self.rowcount, self.description, self.lastrowid) = \
+ (cursor.rowcount, cursor.description, cursor.lastrowid)
+ except pgdb.DatabaseError, e:
+ self.rollback()
+ uuid = commands.getoutput("uuidgen")
+ print >> log, "Database error %s:" % uuid
+ print >> log, e
+ print >> log, "Query:"
+ print >> log, query
+ print >> log, "Params:"
+ print >> log, pformat(param_seq[0])
+ raise PLCDBError("Please contact " + \
+ self.api.config.PLC_NAME + " Support " + \
+ "<" + self.api.config.PLC_MAIL_SUPPORT_ADDRESS + ">" + \
+ " and reference " + uuid)
+
+ def selectall(self, query, params = None, hashref = True, key_field = None):
+ """
+ Return each row as a dictionary keyed on field name (like DBI
+ selectrow_hashref()). If key_field is specified, return rows
+ as a dictionary keyed on the specified field (like DBI
+ selectall_hashref()).
+
+ If params is specified, the specified parameters will be bound
+ to the query (see PLC.DB.parameterize() and
+ pgdb.cursor.execute()).
+ """
+
+ self.execute(query, params)
+ rows = self.cursor.fetchall()
+
+ if hashref:
+ # Return each row as a dictionary keyed on field name
+ # (like DBI selectrow_hashref()).
+ labels = [column[0] for column in self.description]
+ rows = [dict(zip(labels, row)) for row in rows]
+
+ if key_field is not None and key_field in labels:
+ # Return rows as a dictionary keyed on the specified field
+ # (like DBI selectall_hashref()).
+ return dict([(row[key_field], row) for row in rows])
+ else:
+ return rows
--- /dev/null
+#
+# Functions for interacting with the roles table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+from PLC.Parameter import Parameter
+
+class Roles(dict):
+ """
+ Representation of the roles table in the database.
+ """
+
+ fields = {
+ 'role_id': Parameter(int, "Role identifier"),
+ 'name': Parameter(str, "Role name"),
+ }
+
+ # Role IDs equal to or lower than this number are for use by real
+ # accounts. Other role IDs are used internally.
+ role_max = 500
+
+ def __init__(self, api):
+ sql = "SELECT * FROM roles" \
+ " WHERE role_id <= %d" % self.role_max
+
+ for row in api.db.selectall(sql):
+ self[row['role_id']] = row['name']
+ self[row['name']] = row['role_id']
--- /dev/null
+from types import StringTypes
+import string
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.Persons import Person, Persons
+from PLC.Slices import Slice, Slices
+from PLC.PCUs import PCU, PCUs
+from PLC.Nodes import Node, Nodes
+from PLC.NodeGroups import NodeGroup, NodeGroups
+
+class Site(Row):
+ """
+ Representation of a row in the sites table. To use, optionally
+ instantiate with a dict of values. Update as you would a
+ dict. Commit to the database with flush().
+ """
+
+ fields = {
+ 'site_id': Parameter(int, "Site identifier"),
+ 'name': Parameter(str, "Full site name"),
+ 'abbreviated_name': Parameter(str, "Abbreviated site name"),
+ 'login_base': Parameter(str, "Site slice prefix"),
+ 'is_public': Parameter(bool, "Publicly viewable site"),
+ 'latitude': Parameter(float, "Decimal latitude of the site"),
+ 'longitude': Parameter(float, "Decimal longitude of the site"),
+ 'url': Parameter(str, "URL of a page that describes the site"),
+ 'nodegroup_id': Parameter(int, "Identifier of the nodegroup containing the site's nodes"),
+ 'organization_id': Parameter(int, "Organizational identifier if the site is part of a larger organization"),
+ 'ext_consortium_id': Parameter(int, "Consortium identifier if the site is part of an external consortium"),
+ 'date_created': Parameter(str, "Date and time when node entry was created"),
+ 'deleted': Parameter(bool, "Has been deleted"),
+ }
+
+ # These fields are derived from join tables and are not actually
+ # in the sites table.
+ join_fields = {
+ 'max_slices': Parameter(int, "Maximum number of slices that the site is able to create"),
+ 'site_share': Parameter(float, "Relative resource share for this site's slices"),
+ }
+
+ # These fields are derived from join tables and are not returned
+ # by default unless specified.
+ extra_fields = {
+ 'person_ids': Parameter([int], "List of account identifiers"),
+ 'slice_ids': Parameter([int], "List of slice identifiers"),
+ 'defaultattribute_ids': Parameter([int], "List of default slice attribute identifiers"),
+ 'pcu_ids': Parameter([int], "List of PCU identifiers"),
+ 'node_ids': Parameter([int], "List of site node identifiers"),
+ }
+
+ default_fields = dict(fields.items() + join_fields.items())
+ all_fields = dict(default_fields.items() + extra_fields.items())
+
+ # Number of slices assigned to each site at the time that the site is created
+ default_max_slices = 0
+
+ # XXX Useless, unclear what this value means
+ default_site_share = 1.0
+
+ def __init__(self, api, fields):
+ Row.__init__(self, fields)
+ self.api = api
+
+ def validate_login_base(self, login_base):
+ if len(login_base) > 20:
+ raise PLCInvalidArgument, "Login base must be <= 20 characters"
+
+ if not set(login_base).issubset(string.ascii_letters):
+ raise PLCInvalidArgument, "Login base must consist only of ASCII letters"
+
+ login_base = login_base.lower()
+ conflicts = Sites(self.api, [login_base])
+ for site_id, site in conflicts.iteritems():
+ if not site['deleted'] and ('site_id' not in self or self['site_id'] != site_id):
+ raise PLCInvalidArgument, "login_base already in use"
+
+ return login_base
+
+ def validate_latitude(self, latitude):
+ if latitude < -90.0 or latitude > 90.0:
+ raise PLCInvalidArgument, "Invalid latitude value"
+
+ if not self.has_key('longitude') or \
+ self['longitude'] is None:
+ raise PLCInvalidArgument, "Longitude must also be specified"
+
+ return latitude
+
+ def validate_longitude(self, longitude):
+ if longitude < -180.0 or longitude > 180.0:
+ raise PLCInvalidArgument, "Invalid longitude value"
+
+ if not self.has_key('latitude') or \
+ self['latitude'] is None:
+ raise PLCInvalidArgument, "Latitude must also be specified"
+
+ return longitude
+
+ def validate_nodegroup_id(self, nodegroup_id):
+ nodegroups = NodeGroups(self.api)
+ if nodegroup_id not in nodegroups:
+ raise PLCInvalidArgument, "No such nodegroup"
+
+ return nodegroup_id
+
+ def validate_organization_id(self, organization_id):
+ organizations = Organizations(self.api)
+ if role_id not in organizations:
+ raise PLCInvalidArgument, "No such organization"
+
+ return organization_id
+
+ def validate_ext_consortium_id(self, organization_id):
+ consortiums = Consortiums(self.api)
+ if consortium_id not in consortiums:
+ raise PLCInvalidArgument, "No such consortium"
+
+ return nodegroup_id
+
+ def flush(self, commit = True):
+ """
+ Flush changes back to the database.
+ """
+
+ self.validate()
+
+ try:
+ if not self['name'] or \
+ not self['abbreviated_name'] or \
+ not self['login_base']:
+ raise KeyError
+ except KeyError:
+ raise PLCInvalidArgument, "name, abbreviated_name, and login_base must all be specified"
+
+ # Fetch a new site_id if necessary
+ if 'site_id' not in self:
+ rows = self.api.db.selectall("SELECT NEXTVAL('sites_site_id_seq') AS site_id")
+ if not rows:
+ raise PLCDBError, "Unable to fetch new site_id"
+ self['site_id'] = rows[0]['site_id']
+ insert = True
+ else:
+ insert = False
+
+ # Create site node group if necessary
+ if 'nodegroup_id' not in self:
+ rows = self.api.db.selectall("SELECT NEXTVAL('nodegroups_nodegroup_id_seq') as nodegroup_id")
+ if not rows:
+ raise PLCDBError, "Unable to fetch new nodegroup_id"
+ self['nodegroup_id'] = rows[0]['nodegroup_id']
+
+ nodegroup_id = self['nodegroup_id']
+ # XXX Needs a unique name because we cannot delete site node groups yet
+ name = self['login_base'] + str(self['site_id'])
+ description = "Nodes at " + self['name']
+ is_custom = False
+ self.api.db.do("INSERT INTO nodegroups (nodegroup_id, name, description, is_custom)" \
+ " VALUES (%(nodegroup_id)d, %(name)s, %(description)s, %(is_custom)s)",
+ locals())
+
+ # Filter out fields that cannot be set or updated directly
+ fields = dict(filter(lambda (key, value): key in self.fields,
+ self.items()))
+
+ # Parameterize for safety
+ keys = fields.keys()
+ values = [self.api.db.param(key, value) for (key, value) in fields.items()]
+
+ if insert:
+ # Insert new row in sites table
+ self.api.db.do("INSERT INTO sites (%s) VALUES (%s)" % \
+ (", ".join(keys), ", ".join(values)),
+ fields)
+
+ # Setup default slice site info
+ # XXX Will go away soon
+ self['max_slices'] = self.default_max_slices
+ self['site_share'] = self.default_site_share
+ self.api.db.do("INSERT INTO dslice03_siteinfo (site_id, max_slices, site_share)" \
+ " VALUES (%(site_id)d, %(max_slices)d, %(site_share)f)",
+ self)
+ else:
+ # Update default slice site info
+ # XXX Will go away soon
+ if 'max_slices' in self and 'site_share' in self:
+ self.api.db.do("UPDATE dslice03_siteinfo SET " \
+ " max_slices = %(max_slices)d, site_share = %(site_share)f" \
+ " WHERE site_id = %(site_id)d",
+ self)
+
+ # Update existing row in sites table
+ columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
+ self.api.db.do("UPDATE sites SET " + \
+ ", ".join(columns) + \
+ " WHERE site_id = %(site_id)d",
+ fields)
+
+ if commit:
+ self.api.db.commit()
+
+ def delete(self, commit = True):
+ """
+ Delete existing site.
+ """
+
+ assert 'site_id' in self
+
+ # Make sure extra fields are present
+ sites = Sites(self.api, [self['site_id']],
+ ['person_ids', 'slice_ids', 'pcu_ids', 'node_ids'])
+ assert sites
+ self.update(sites.values()[0])
+
+ # Delete accounts of all people at the site who are not
+ # members of at least one other non-deleted site.
+ persons = Persons(self.api, self['person_ids'])
+ for person_id, person in persons.iteritems():
+ delete = True
+
+ person_sites = Sites(self.api, person['site_ids'])
+ for person_site_id, person_site in person_sites.iteritems():
+ if person_site_id != self['site_id'] and \
+ not person_site['deleted']:
+ delete = False
+ break
+
+ if delete:
+ person.delete(commit = False)
+
+ # Delete all site slices
+ slices = Slices(self.api, self['slice_ids'])
+ for slice in slices.values():
+ slice.delete(commit = False)
+
+ # Delete all site PCUs
+ pcus = PCUs(self.api, self['pcu_ids'])
+ for pcu in pcus.values():
+ pcu.delete(commit = False)
+
+ # Delete all site nodes
+ nodes = Nodes(self.api, self['node_ids'])
+ for node in nodes.values():
+ node.delete(commit = False)
+
+ # Clean up miscellaneous join tables
+ for table in ['site_authorized_subnets',
+ 'dslice03_defaultattribute',
+ 'dslice03_siteinfo']:
+ self.api.db.do("DELETE FROM %s" \
+ " WHERE site_id = %d" % \
+ (table, self['site_id']))
+
+ # XXX Cannot delete site node groups yet
+
+ # Mark as deleted
+ self['deleted'] = True
+ self.flush(commit)
+
+class Sites(Table):
+ """
+ Representation of row(s) from the sites table in the
+ database. Specify extra_fields to be able to view and modify extra
+ fields.
+ """
+
+ def __init__(self, api, site_id_or_login_base_list = None, extra_fields = []):
+ self.api = api
+
+ sql = "SELECT sites.*" \
+ ", dslice03_siteinfo.max_slices"
+
+ # N.B.: Joined IDs may be marked as deleted in their primary tables
+ join_tables = {
+ # extra_field: (extra_table, extra_column, join_using)
+ 'person_ids': ('person_site', 'person_id', 'site_id'),
+ 'slice_ids': ('dslice03_slices', 'slice_id', 'site_id'),
+ 'defaultattribute_ids': ('dslice03_defaultattribute', 'defaultattribute_id', 'site_id'),
+ 'pcu_ids': ('pcu', 'pcu_id', 'site_id'),
+ 'node_ids': ('nodegroup_nodes', 'node_id', 'nodegroup_id'),
+ }
+
+ extra_fields = filter(join_tables.has_key, extra_fields)
+ extra_tables = ["%s USING (%s)" % \
+ (join_tables[field][0], join_tables[field][2]) \
+ for field in extra_fields]
+ extra_columns = ["%s.%s" % \
+ (join_tables[field][0], join_tables[field][1]) \
+ for field in extra_fields]
+
+ if extra_columns:
+ sql += ", " + ", ".join(extra_columns)
+
+ sql += " FROM sites" \
+ " LEFT JOIN dslice03_siteinfo USING (site_id)"
+
+ if extra_tables:
+ sql += " LEFT JOIN " + " LEFT JOIN ".join(extra_tables)
+
+ sql += " WHERE deleted IS False"
+
+ if site_id_or_login_base_list:
+ # Separate the list into integers and strings
+ site_ids = filter(lambda site_id: isinstance(site_id, (int, long)),
+ site_id_or_login_base_list)
+ login_bases = filter(lambda login_base: isinstance(login_base, StringTypes),
+ site_id_or_login_base_list)
+ sql += " AND (False"
+ if site_ids:
+ sql += " OR site_id IN (%s)" % ", ".join(map(str, site_ids))
+ if login_bases:
+ sql += " OR login_base IN (%s)" % ", ".join(api.db.quote(login_bases))
+ sql += ")"
+
+ rows = self.api.db.selectall(sql)
+ for row in rows:
+ if self.has_key(row['site_id']):
+ site = self[row['site_id']]
+ site.update(row)
+ else:
+ self[row['site_id']] = Site(api, row)
--- /dev/null
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+
+class Slice(Row):
+ """
+ Representation of a row in the slices table. To use, instantiate
+ with a dict of values.
+ """
+
+ fields = {
+ 'slice_id': Parameter(int, "Slice type"),
+ }
+
+ def __init__(self, api, fields):
+ self.api = api
+ dict.__init__(fields)
+
+ def commit(self):
+ # XXX
+ pass
+
+ def delete(self):
+ # XXX
+ pass
+
+class Slices(Table):
+ """
+ Representation of row(s) from the slices table in the
+ database.
+ """
+
+ def __init__(self, api, slice_id_list = None):
+ # XXX
+ pass
--- /dev/null
+class Row(dict):
+ """
+ Representation of a row in a database table. To use, optionally
+ instantiate with a dict of values. Update as you would a
+ dict. Commit to the database with flush().
+ """
+
+ # Set this to a dict of the valid columns in this table. If a column
+ # name ends in 's' and the column value is set by referring to the
+ # column without the 's', it is assumed that the column values
+ # should be aggregated into lists. For example, if fields contains
+ # the column 'role_ids' and row['role_id'] is set repeatedly to
+ # different values, row['role_ids'] will contain a list of the set
+ # values.
+ fields = {}
+
+ # These fields are derived from join tables and are not actually
+ # in the sites table.
+ join_fields = {}
+
+ # These fields are derived from join tables and are not returned
+ # by default unless specified.
+ extra_fields = {}
+
+ def __init__(self, fields):
+ self.update(fields)
+
+ def update(self, fields):
+ for key, value in fields.iteritems():
+ self.__setitem__(key, value)
+
+ def __setitem__(self, key, value):
+ """
+ Magically takes care of aggregating certain variables into
+ lists.
+ """
+
+ # All known keys
+ all_fields = self.fields.keys() + \
+ self.join_fields.keys() + \
+ self.extra_fields.keys()
+
+ # Aggregate into lists
+ if (key + 's') in all_fields:
+ key += 's'
+ try:
+ if value not in self[key] and value is not None:
+ self[key].append(value)
+ except KeyError:
+ if value is None:
+ self[key] = []
+ else:
+ self[key] = [value]
+ return
+
+ elif key in all_fields:
+ dict.__setitem__(self, key, value)
+
+ def validate(self):
+ """
+ Validates values. Will validate a value with a custom function
+ if a function named 'validate_[key]' exists.
+ """
+
+ # Validate values before committing
+ # XXX Also truncate strings that are too long
+ for key, value in self.iteritems():
+ if value is not None and hasattr(self, 'validate_' + key):
+ validate = getattr(self, 'validate_' + key)
+ self[key] = validate(value)
+
+ def flush(self, commit = True):
+ """
+ Flush changes back to the database.
+ """
+
+ pass
+
+class Table(dict):
+ """
+ Representation of row(s) in a database table.
+ """
+
+ def flush(self, commit = True):
+ """
+ Flush changes back to the database.
+ """
+
+ for row in self.values():
+ row.flush(commit)
--- /dev/null
+all = 'Addresses AddressTypes API Auth BootStates Config Debug Faults Keys md5crypt Method NodeGroups NodeNetworks Nodes Parameter PCUs Persons PostgreSQL Roles Sites Slices Table'.split()
--- /dev/null
+#########################################################
+# md5crypt.py
+#
+# 0423.2000 by michal wallace http://www.sabren.com/
+# based on perl's Crypt::PasswdMD5 by Luis Munoz (lem@cantv.net)
+# based on /usr/src/libcrypt/crypt.c from FreeBSD 2.2.5-RELEASE
+#
+# MANY THANKS TO
+#
+# Carey Evans - http://home.clear.net.nz/pages/c.evans/
+# Dennis Marti - http://users.starpower.net/marti1/
+#
+# For the patches that got this thing working!
+#
+#########################################################
+"""md5crypt.py - Provides interoperable MD5-based crypt() function
+
+SYNOPSIS
+
+ import md5crypt.py
+
+ cryptedpassword = md5crypt.md5crypt(password, salt);
+
+DESCRIPTION
+
+unix_md5_crypt() provides a crypt()-compatible interface to the
+rather new MD5-based crypt() function found in modern operating systems.
+It's based on the implementation found on FreeBSD 2.2.[56]-RELEASE and
+contains the following license in it:
+
+ "THE BEER-WARE LICENSE" (Revision 42):
+ <phk@login.dknet.dk> wrote this file. As long as you retain this notice you
+ can do whatever you want with this stuff. If we meet some day, and you think
+ this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
+
+apache_md5_crypt() provides a function compatible with Apache's
+.htpasswd files. This was contributed by Bryan Hart <bryan@eai.com>.
+
+"""
+
+MAGIC = '$1$' # Magic string
+ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+import md5
+
+def to64 (v, n):
+ ret = ''
+ while (n - 1 >= 0):
+ n = n - 1
+ ret = ret + ITOA64[v & 0x3f]
+ v = v >> 6
+ return ret
+
+
+def apache_md5_crypt (pw, salt):
+ # change the Magic string to match the one used by Apache
+ return unix_md5_crypt(pw, salt, '$apr1$')
+
+
+def unix_md5_crypt(pw, salt, magic=None):
+
+ if magic==None:
+ magic = MAGIC
+
+ # Take care of the magic string if present
+ if salt[:len(magic)] == magic:
+ salt = salt[len(magic):]
+
+
+ # salt can have up to 8 characters:
+ import string
+ salt = string.split(salt, '$', 1)[0]
+ salt = salt[:8]
+
+ ctx = pw + magic + salt
+
+ final = md5.md5(pw + salt + pw).digest()
+
+ for pl in range(len(pw),0,-16):
+ if pl > 16:
+ ctx = ctx + final[:16]
+ else:
+ ctx = ctx + final[:pl]
+
+
+ # Now the 'weird' xform (??)
+
+ i = len(pw)
+ while i:
+ if i & 1:
+ ctx = ctx + chr(0) #if ($i & 1) { $ctx->add(pack("C", 0)); }
+ else:
+ ctx = ctx + pw[0]
+ i = i >> 1
+
+ final = md5.md5(ctx).digest()
+
+ # The following is supposed to make
+ # things run slower.
+
+ # my question: WTF???
+
+ for i in range(1000):
+ ctx1 = ''
+ if i & 1:
+ ctx1 = ctx1 + pw
+ else:
+ ctx1 = ctx1 + final[:16]
+
+ if i % 3:
+ ctx1 = ctx1 + salt
+
+ if i % 7:
+ ctx1 = ctx1 + pw
+
+ if i & 1:
+ ctx1 = ctx1 + final[:16]
+ else:
+ ctx1 = ctx1 + pw
+
+
+ final = md5.md5(ctx1).digest()
+
+
+ # Final xform
+
+ passwd = ''
+
+ passwd = passwd + to64((int(ord(final[0])) << 16)
+ |(int(ord(final[6])) << 8)
+ |(int(ord(final[12]))),4)
+
+ passwd = passwd + to64((int(ord(final[1])) << 16)
+ |(int(ord(final[7])) << 8)
+ |(int(ord(final[13]))), 4)
+
+ passwd = passwd + to64((int(ord(final[2])) << 16)
+ |(int(ord(final[8])) << 8)
+ |(int(ord(final[14]))), 4)
+
+ passwd = passwd + to64((int(ord(final[3])) << 16)
+ |(int(ord(final[9])) << 8)
+ |(int(ord(final[15]))), 4)
+
+ passwd = passwd + to64((int(ord(final[4])) << 16)
+ |(int(ord(final[10])) << 8)
+ |(int(ord(final[5]))), 4)
+
+ passwd = passwd + to64((int(ord(final[11]))), 2)
+
+
+ return magic + salt + '$' + passwd
+
+
+## assign a wrapper function:
+md5crypt = unix_md5_crypt
+
+if __name__ == "__main__":
+ print unix_md5_crypt("cat", "hat")
--- /dev/null
+#!/usr/bin/python
+#
+# Simple standalone HTTP server for testing PLCAPI
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import os
+import sys
+import getopt
+import traceback
+import BaseHTTPServer
+
+from PLC.API import PLCAPI
+
+class PLCAPIRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ """
+ Simple standalone HTTP request handler for testing PLCAPI.
+ """
+
+ def do_POST(self):
+ try:
+ # Read request
+ request = self.rfile.read(int(self.headers["Content-length"]))
+
+ # Handle request
+ response = self.server.api.handle(self.client_address, request)
+
+ # Write response
+ self.send_response(200)
+ self.wfile.write(response)
+ self.wfile.flush()
+
+ self.connection.shutdown(1)
+
+ except Exception, e:
+ # Log error
+ sys.stderr.write(traceback.format_exc())
+ sys.stderr.flush()
+
+ def do_GET(self):
+ self.send_response(200)
+ self.send_header("Content-type", 'text/html')
+ self.end_headers()
+ self.wfile.write("""
+<html><head>
+<title>PLCAPI XML-RPC/SOAP Interface</title>
+</head><body>
+<h1>PLCAPI XML-RPC/SOAP Interface</h1>
+<p>Please use XML-RPC or SOAP to access the PLCAPI.</p>
+</body></html>
+""")
+
+class PLCAPIServer(BaseHTTPServer.HTTPServer):
+ """
+ Simple standalone HTTP server for testing PLCAPI.
+ """
+
+ def __init__(self, addr, config):
+ self.api = PLCAPI(config)
+ self.allow_reuse_address = 1
+ BaseHTTPServer.HTTPServer.__init__(self, addr, PLCAPIRequestHandler)
+
+# Defaults
+addr = "0.0.0.0"
+port = 8000
+config = "/etc/planetlab/plc_config.xml"
+
+def usage():
+ print "Usage: %s [OPTION]..." % sys.argv[0]
+ print "Options:"
+ print " -p PORT, --port=PORT TCP port number to listen on (default: %d)" % port
+ print " -f FILE, --config=FILE PLC configuration file (default: %s)" % config
+ print " -h, --help This message"
+ sys.exit(1)
+
+# Get options
+try:
+ (opts, argv) = getopt.getopt(sys.argv[1:], "p:f:h", ["port=", "config=", "help"])
+except getopt.GetoptError, err:
+ print "Error: " + err.msg
+ usage()
+
+for (opt, optval) in opts:
+ if opt == "-p" or opt == "--port":
+ try:
+ port = int(optval)
+ except ValueError:
+ usage()
+ elif opt == "-f" or opt == "--config":
+ config = optval
+ elif opt == "-h" or opt == "--help":
+ usage()
+
+# Start server
+PLCAPIServer((addr, port), config).serve_forever()
--- /dev/null
+#!/usr/bin/python
+#
+# Interactive shell for testing PLCAPI
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2005 The Trustees of Princeton University
+#
+# $Id: plcsh,v 1.3 2006/01/09 19:57:24 mlhuang Exp $
+#
+
+import os, sys
+import traceback
+import getopt
+import pydoc
+
+from PLC.API import PLCAPI
+from PLC.Method import Method
+
+# Defaults
+config = "/etc/planetlab/plc_config"
+
+def usage():
+ print "Usage: %s [OPTION]..." % sys.argv[0]
+ print "Options:"
+ print " -f, --config=FILE PLC configuration file (default: %s)" % config
+ print " -h, --help This message"
+ sys.exit(1)
+
+# Get options
+try:
+ (opts, argv) = getopt.getopt(sys.argv[1:], "f:h", ["config=", "help"])
+except getopt.GetoptError, err:
+ print "Error: " + err.msg
+ usage()
+
+for (opt, optval) in opts:
+ if opt == "-f" or opt == "--config":
+ config = optval
+ elif opt == "-h" or opt == "--help":
+ usage()
+
+api = PLCAPI(config)
+
+class Dummy:
+ """
+ Dummy class to support tab completion of API methods with dots in
+ their names (e.g., system.listMethods).
+ """
+ pass
+
+# Define all methods in the global namespace to support tab completion
+for method in api.methods:
+ paths = method.split(".")
+ if len(paths) > 1:
+ first = paths.pop(0)
+ if first not in globals():
+ globals()[first] = Dummy()
+ obj = globals()[first]
+ for path in paths:
+ if not hasattr(obj, path):
+ if path == paths[-1]:
+ setattr(obj, path, api.callable(method))
+ else:
+ setattr(obj, path, Dummy())
+ obj = getattr(obj, path)
+ else:
+ globals()[method] = api.callable(method)
+
+pyhelp = help
+def help(thing):
+ """
+ Override builtin help() function to support calling help(method).
+ """
+
+ if isinstance(thing, Method):
+ return pydoc.pager(thing.help())
+ else:
+ return pyhelp(thing)
+
+# If a file is specified
+if argv:
+ execfile(argv[0])
+ sys.exit(0)
+
+# Otherwise, create an interactive shell environment
+
+print "PlanetLab Central Direct API Access"
+prompt = ""
+print 'Type "system.listMethods()" or "help(method)" for more information.'
+
+# Readline and tab completion support
+import atexit
+import readline
+import rlcompleter
+
+# Load command history
+history_path = os.path.join(os.environ["HOME"], ".plcapi_history")
+try:
+ file(history_path, 'a').close()
+ readline.read_history_file(history_path)
+ atexit.register(readline.write_history_file, history_path)
+except IOError:
+ pass
+
+# Enable tab completion
+readline.parse_and_bind("tab: complete")
+
+try:
+ while True:
+ command = ""
+ while True:
+ # Get line
+ try:
+ if command == "":
+ sep = ">>> "
+ else:
+ sep = "... "
+ line = raw_input(prompt + sep)
+ # Ctrl-C
+ except KeyboardInterrupt:
+ command = ""
+ print
+ break
+
+ # Build up multi-line command
+ command += line
+
+ # Blank line or first line does not end in :
+ if line == "" or (command == line and line[-1] != ':'):
+ break
+
+ command += os.linesep
+
+ # Blank line
+ if command == "":
+ continue
+ # Quit
+ elif command in ["q", "quit", "exit"]:
+ break
+
+ try:
+ try:
+ # Try evaluating as an expression and printing the result
+ result = eval(command)
+ if result is not None:
+ print result
+ except:
+ # Fall back to executing as a statement
+ exec command
+ except Exception, err:
+ traceback.print_exc()
+
+except EOFError:
+ print
+ pass
--- /dev/null
+* Event logging
+ * In the current API, every call is logged and certain interesting
+ events are logged in the events table. I haven't implemented event
+ logging yet in the new API.
+
+* Tests
+ * With Shell.py, it should be easy to write a large set of tests. I've
+ thought about writing a SQLite DB backend so that MyPLC/PostgreSQL
+ doesn't have to be setup in order for the tests to be run. But there
+ are some technical limitations to SQLite. It would probably be best
+ to run the testsuite against MyPLC for now.
+
+* Authentication
+ * Need to implement node and certificate/federation authentication.
+ * Need to (re)implement "capability" (i.e. trusted host)
+ authentication. Maybe implement it in the same way as node
+ authentication.
+
+* Anonymous functions
+ * Implement anonymous functions for now for backward compatibility,
+ but get rid of them as soon as possible
+
+* Hierarchical layout
+ * Probably need to organize the functions inside PLC/Methods/
+
+* Deletion
+ * Need to come up with a sane, consistent principal deletion policy.
--- /dev/null
+*.dvi
+*.html
+*.man
+*.ps
+*.pdf
+*.rtf
+*.tex
+*.texi
+*.txt
+*.xml.valid
+Methods.xml
--- /dev/null
+#!/usr/bin/python
+#
+# Generates a DocBook section documenting all PLCAPI methods on
+# stdout.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import xml.dom.minidom
+from xml.dom.minidom import Element, Text
+import codecs
+
+from PLC.API import PLCAPI
+from PLC.Method import *
+
+api = PLCAPI(None)
+
+# xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
+# data when pretty-printing. Override this behavior.
+class TrimText(Text):
+ def writexml(self, writer, indent="", addindent="", newl=""):
+ Text.writexml(self, writer, "", "", "")
+
+class TrimTextElement(Element):
+ def writexml(self, writer, indent="", addindent="", newl=""):
+ writer.write(indent)
+ Element.writexml(self, writer, "", "", "")
+ writer.write(newl)
+
+class simpleElement(TrimTextElement):
+ """<tagName>text</tagName>"""
+ def __init__(self, tagName, text = None):
+ TrimTextElement.__init__(self, tagName)
+ if text is not None:
+ t = TrimText()
+ t.data = unicode(text)
+ self.appendChild(t)
+
+class paraElement(simpleElement):
+ """<para>text</para>"""
+ def __init__(self, text = None):
+ simpleElement.__init__(self, 'para', text)
+
+class blockquoteElement(Element):
+ """<blockquote><para>text...</para><para>...text</para></blockquote>"""
+ def __init__(self, text = None):
+ Element.__init__(self, 'blockquote')
+ if text is not None:
+ # Split on blank lines
+ lines = [line.strip() for line in text.strip().split("\n")]
+ lines = "\n".join(lines)
+ paragraphs = lines.split("\n\n")
+
+ for paragraph in paragraphs:
+ self.appendChild(paraElement(paragraph))
+
+class entryElement(simpleElement):
+ """<entry>text</entry>"""
+ def __init__(self, text = None):
+ simpleElement.__init__(self, 'entry', text)
+
+class rowElement(Element):
+ """
+ <row>
+ <entry>entries[0]</entry>
+ <entry>entries[1]</entry>
+ ...
+ </row>
+ """
+
+ def __init__(self, entries = None):
+ Element.__init__(self, 'row')
+ if entries:
+ for entry in entries:
+ if isinstance(entry, entryElement):
+ self.appendChild(entry)
+ else:
+ self.appendChild(entryElement(entry))
+
+class informaltableElement(Element):
+ """
+ <informaltable>
+ <tgroup>
+ <thead>
+ <row>
+ <entry>label1</entry>
+ <entry>label2</entry>
+ ...
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry>row1value1</entry>
+ <entry>row1value2</entry>
+ ...
+ </row>
+ ...
+ </tbody>
+ </tgroup>
+ </informaltable>
+ """
+
+ def __init__(self, head = None, rows = None):
+ Element.__init__(self, 'informaltable')
+ tgroup = Element('tgroup')
+ self.appendChild(tgroup)
+ if head:
+ thead = Element('thead')
+ tgroup.appendChild(thead)
+ if isinstance(head, rowElement):
+ thead.appendChild(head)
+ else:
+ thead.appendChild(rowElement(head))
+ if rows:
+ tbody = Element('tbody')
+ tgroup.appendChild(tbody)
+ for row in rows:
+ if isinstance(row, rowElement):
+ tbody.appendChild(row)
+ else:
+ tobdy.appendChild(rowElement(row))
+ cols = len(thead.childNodes[0].childNodes)
+ tgroup.setAttribute('cols', str(cols))
+
+def parameters(param, name, optional, default, doc, indent, step):
+ """Format a parameter into parameter row(s)."""
+
+ rows = []
+
+ row = rowElement()
+ rows.append(row)
+
+ # Parameter name
+ entry = entryElement()
+ entry.appendChild(simpleElement('literallayout', indent + name))
+ row.appendChild(entry)
+
+ # Parameter type
+ param_type = python_type(param)
+ row.appendChild(entryElement(xmlrpc_type(param_type)))
+
+ # Whether parameter is optional
+ row.appendChild(entryElement(str(bool(optional))))
+
+ # Parameter default
+ if optional and default is not None:
+ row.appendChild(entryElement(unicode(default)))
+ else:
+ row.appendChild(entryElement())
+
+ # Parameter documentation
+ row.appendChild(entryElement(doc))
+
+ # Indent the name of each sub-parameter
+ subparams = []
+ if isinstance(param, dict):
+ subparams = param.iteritems()
+ elif isinstance(param, Mixed):
+ subparams = [(name, subparam) for subparam in param]
+ elif isinstance(param, (list, tuple)):
+ subparams = [("", subparam) for subparam in param]
+
+ for name, subparam in subparams:
+ if isinstance(subparam, Parameter):
+ (optional, default, doc) = (subparam.optional, subparam.default, subparam.doc)
+ else:
+ # All named sub-parameters are optional if not otherwise specified
+ (optional, default, doc) = (True, None, "")
+ rows += parameters(subparam, name, optional, default, doc, indent + step, step)
+
+ return rows
+
+for method in api.methods:
+ func = api.callable(method)
+ (min_args, max_args, defaults) = func.args()
+
+ section = Element('section')
+ section.setAttribute('id', func.name)
+ section.appendChild(simpleElement('title', func.name))
+
+ para = paraElement('Status:')
+ para.appendChild(blockquoteElement(func.status))
+ section.appendChild(para)
+
+ prototype = "%s (%s)" % (method, ", ".join(max_args))
+ para = paraElement('Prototype:')
+ para.appendChild(blockquoteElement(prototype))
+ section.appendChild(para)
+
+ para = paraElement('Description:')
+ para.appendChild(blockquoteElement(func.__doc__))
+ section.appendChild(para)
+
+ para = paraElement('Parameters:')
+ blockquote = blockquoteElement()
+ para.appendChild(blockquote)
+ section.appendChild(para)
+
+ head = rowElement(['Name', 'Type', 'Optional', 'Default', 'Description'])
+ rows = []
+
+ indent = " "
+ for name, param, default in zip(max_args, func.accepts, defaults):
+ optional = name not in min_args
+ if isinstance(param, Parameter):
+ doc = param.doc
+ else:
+ doc = ""
+ rows += parameters(param, name, optional, default, doc, "", indent)
+
+ if rows:
+ informaltable = informaltableElement(head, rows)
+ informaltable.setAttribute('frame', "none")
+ informaltable.setAttribute('rules', "rows")
+ blockquote.appendChild(informaltable)
+ else:
+ blockquote.appendChild(paraElement("None"))
+
+ print section.toprettyxml(encoding = "UTF-8")
--- /dev/null
+#
+# (Re)builds API documentation
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: plcsh,v 1.3 2006/01/09 19:57:24 mlhuang Exp $
+#
+
+all: PLCAPI.pdf
+
+.PLCAPI.xml.valid: Methods.xml
+
+Methods.xml: DocBook.py ../PLC/__init__.py ../PLC/Methods/__init__.py
+ PYTHONPATH=.. python $< > $@
+
+#
+# Documentation
+#
+
+# Validate the XML
+.%.xml.valid: %.xml
+ xmllint --valid --output $@ $<
+
+# Remove the temporary output file after compilation
+.SECONDARY: .%.xml.valid
+
+# Compile it into other formats
+FORMATS := dvi html man ps pdf rtf tex texi txt
+
+DOCBOOK2FLAGS := -V biblio-number=1
+
+define docbook2
+%.$(1): %.xml .%.xml.valid
+ docbook2$(1) --nochunks $$(DOCBOOK2FLAGS) $$<
+endef
+
+$(foreach format,$(FORMATS),$(eval $(call docbook2,$(format))))
+
+clean:
+ rm -f $(patsubst %,*.%,$(FORMATS)) .*.xml.valid
+
+force:
+
+.PHONY: force clean docclean
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
+<!ENTITY Methods SYSTEM "Methods.xml">
+]>
+
+<book>
+ <bookinfo>
+ <title>PlanetLab Central API Documentation</title>
+ <authorgroup>
+ <author><firstname>Aaron</firstname><surname>Klingaman</surname></author>
+ <author><firstname>Mark</firstname><surname>Huang</surname></author>
+ </authorgroup>
+ </bookinfo>
+
+ <chapter id="Methods">
+ <title>PlanetLab API Methods</title>
+ <para></para>
+
+ &Methods;
+ </chapter>
+
+</book>