starting new development to support slice conf files
authorTony Mack <tmack@cs.princeton.edu>
Fri, 10 Oct 2008 19:10:15 +0000 (19:10 +0000)
committerTony Mack <tmack@cs.princeton.edu>
Fri, 10 Oct 2008 19:10:15 +0000 (19:10 +0000)
281 files changed:
PLC/.cvsignore [new file with mode: 0644]
PLC/API.py [new file with mode: 0644]
PLC/AddressTypes.py [new file with mode: 0644]
PLC/Addresses.py [new file with mode: 0644]
PLC/Auth.py [new file with mode: 0644]
PLC/Boot.py [new file with mode: 0644]
PLC/BootStates.py [new file with mode: 0644]
PLC/ConfFiles.py [new file with mode: 0644]
PLC/Config.py [new file with mode: 0644]
PLC/Debug.py [new file with mode: 0644]
PLC/EventObjects.py [new file with mode: 0644]
PLC/Events.py [new file with mode: 0644]
PLC/Faults.py [new file with mode: 0644]
PLC/Filter.py [new file with mode: 0644]
PLC/GPG.py [new file with mode: 0644]
PLC/InitScripts.py [new file with mode: 0644]
PLC/KeyTypes.py [new file with mode: 0644]
PLC/Keys.py [new file with mode: 0644]
PLC/Messages.py [new file with mode: 0644]
PLC/Method.py [new file with mode: 0644]
PLC/Methods/.cvsignore [new file with mode: 0644]
PLC/Methods/AddAddressType.py [new file with mode: 0644]
PLC/Methods/AddAddressTypeToAddress.py [new file with mode: 0644]
PLC/Methods/AddBootState.py [new file with mode: 0644]
PLC/Methods/AddConfFile.py [new file with mode: 0644]
PLC/Methods/AddConfFileToNode.py [new file with mode: 0644]
PLC/Methods/AddConfFileToNodeGroup.py [new file with mode: 0644]
PLC/Methods/AddInitScript.py [new file with mode: 0644]
PLC/Methods/AddKeyType.py [new file with mode: 0644]
PLC/Methods/AddMessage.py [new file with mode: 0644]
PLC/Methods/AddNetworkMethod.py [new file with mode: 0644]
PLC/Methods/AddNetworkType.py [new file with mode: 0644]
PLC/Methods/AddNode.py [new file with mode: 0644]
PLC/Methods/AddNodeGroup.py [new file with mode: 0644]
PLC/Methods/AddNodeNetwork.py [new file with mode: 0644]
PLC/Methods/AddNodeNetworkSetting.py [new file with mode: 0644]
PLC/Methods/AddNodeNetworkSettingType.py [new file with mode: 0644]
PLC/Methods/AddNodeToNodeGroup.py [new file with mode: 0644]
PLC/Methods/AddNodeToPCU.py [new file with mode: 0644]
PLC/Methods/AddPCU.py [new file with mode: 0644]
PLC/Methods/AddPCUProtocolType.py [new file with mode: 0644]
PLC/Methods/AddPCUType.py [new file with mode: 0644]
PLC/Methods/AddPeer.py [new file with mode: 0644]
PLC/Methods/AddPerson.py [new file with mode: 0644]
PLC/Methods/AddPersonKey.py [new file with mode: 0644]
PLC/Methods/AddPersonToSite.py [new file with mode: 0644]
PLC/Methods/AddPersonToSlice.py [new file with mode: 0644]
PLC/Methods/AddRole.py [new file with mode: 0644]
PLC/Methods/AddRoleToPerson.py [new file with mode: 0644]
PLC/Methods/AddSession.py [new file with mode: 0644]
PLC/Methods/AddSite.py [new file with mode: 0644]
PLC/Methods/AddSiteAddress.py [new file with mode: 0644]
PLC/Methods/AddSlice.py [new file with mode: 0644]
PLC/Methods/AddSliceAttribute.py [new file with mode: 0644]
PLC/Methods/AddSliceAttributeType.py [new file with mode: 0644]
PLC/Methods/AddSliceInstantiation.py [new file with mode: 0644]
PLC/Methods/AddSliceToNodes.py [new file with mode: 0644]
PLC/Methods/AddSliceToNodesWhitelist.py [new file with mode: 0644]
PLC/Methods/AdmAddAddressType.py [new file with mode: 0644]
PLC/Methods/AdmAddNode.py [new file with mode: 0644]
PLC/Methods/AdmAddNodeGroup.py [new file with mode: 0644]
PLC/Methods/AdmAddNodeNetwork.py [new file with mode: 0644]
PLC/Methods/AdmAddNodeToNodeGroup.py [new file with mode: 0644]
PLC/Methods/AdmAddPerson.py [new file with mode: 0644]
PLC/Methods/AdmAddPersonKey.py [new file with mode: 0644]
PLC/Methods/AdmAddPersonToSite.py [new file with mode: 0644]
PLC/Methods/AdmAddSite.py [new file with mode: 0644]
PLC/Methods/AdmAddSitePowerControlUnit.py [new file with mode: 0644]
PLC/Methods/AdmAssociateNodeToPowerControlUnitPort.py [new file with mode: 0644]
PLC/Methods/AdmAuthCheck.py [new file with mode: 0644]
PLC/Methods/AdmDeleteAddressType.py [new file with mode: 0644]
PLC/Methods/AdmDeleteAllPersonKeys.py [new file with mode: 0644]
PLC/Methods/AdmDeleteNode.py [new file with mode: 0644]
PLC/Methods/AdmDeleteNodeGroup.py [new file with mode: 0644]
PLC/Methods/AdmDeleteNodeNetwork.py [new file with mode: 0644]
PLC/Methods/AdmDeletePerson.py [new file with mode: 0644]
PLC/Methods/AdmDeletePersonKeys.py [new file with mode: 0644]
PLC/Methods/AdmDeleteSite.py [new file with mode: 0644]
PLC/Methods/AdmDeleteSitePowerControlUnit.py [new file with mode: 0644]
PLC/Methods/AdmDisassociatePowerControlUnitPort.py [new file with mode: 0644]
PLC/Methods/AdmGenerateNodeConfFile.py [new file with mode: 0644]
PLC/Methods/AdmGetAllAddressTypes.py [new file with mode: 0644]
PLC/Methods/AdmGetAllKeyTypes.py [new file with mode: 0644]
PLC/Methods/AdmGetAllNodeNetworks.py [new file with mode: 0644]
PLC/Methods/AdmGetAllRoles.py [new file with mode: 0644]
PLC/Methods/AdmGetNodeGroupNodes.py [new file with mode: 0644]
PLC/Methods/AdmGetNodeGroups.py [new file with mode: 0644]
PLC/Methods/AdmGetNodes.py [new file with mode: 0644]
PLC/Methods/AdmGetPersonKeys.py [new file with mode: 0644]
PLC/Methods/AdmGetPersonRoles.py [new file with mode: 0644]
PLC/Methods/AdmGetPersonSites.py [new file with mode: 0644]
PLC/Methods/AdmGetPersons.py [new file with mode: 0644]
PLC/Methods/AdmGetPowerControlUnitNodes.py [new file with mode: 0644]
PLC/Methods/AdmGetPowerControlUnits.py [new file with mode: 0644]
PLC/Methods/AdmGetSiteNodes.py [new file with mode: 0644]
PLC/Methods/AdmGetSitePIs.py [new file with mode: 0644]
PLC/Methods/AdmGetSitePersons.py [new file with mode: 0644]
PLC/Methods/AdmGetSitePowerControlUnits.py [new file with mode: 0644]
PLC/Methods/AdmGetSiteTechContacts.py [new file with mode: 0644]
PLC/Methods/AdmGetSites.py [new file with mode: 0644]
PLC/Methods/AdmGrantRoleToPerson.py [new file with mode: 0644]
PLC/Methods/AdmIsPersonInRole.py [new file with mode: 0644]
PLC/Methods/AdmQueryConfFile.py [new file with mode: 0644]
PLC/Methods/AdmQueryNode.py [new file with mode: 0644]
PLC/Methods/AdmQueryPerson.py [new file with mode: 0644]
PLC/Methods/AdmQueryPowerControlUnit.py [new file with mode: 0644]
PLC/Methods/AdmQuerySite.py [new file with mode: 0644]
PLC/Methods/AdmRebootNode.py [new file with mode: 0644]
PLC/Methods/AdmRemoveNodeFromNodeGroup.py [new file with mode: 0644]
PLC/Methods/AdmRemovePersonFromSite.py [new file with mode: 0644]
PLC/Methods/AdmRevokeRoleFromPerson.py [new file with mode: 0644]
PLC/Methods/AdmSetPersonEnabled.py [new file with mode: 0644]
PLC/Methods/AdmSetPersonPrimarySite.py [new file with mode: 0644]
PLC/Methods/AdmUpdateNode.py [new file with mode: 0644]
PLC/Methods/AdmUpdateNodeGroup.py [new file with mode: 0644]
PLC/Methods/AdmUpdateNodeNetwork.py [new file with mode: 0644]
PLC/Methods/AdmUpdatePerson.py [new file with mode: 0644]
PLC/Methods/AdmUpdateSite.py [new file with mode: 0644]
PLC/Methods/AdmUpdateSitePowerControlUnit.py [new file with mode: 0644]
PLC/Methods/AnonAdmGetNodeGroups.py [new file with mode: 0644]
PLC/Methods/AuthCheck.py [new file with mode: 0644]
PLC/Methods/BlacklistKey.py [new file with mode: 0644]
PLC/Methods/BootCheckAuthentication.py [new file with mode: 0644]
PLC/Methods/BootGetNodeDetails.py [new file with mode: 0644]
PLC/Methods/BootNotifyOwners.py [new file with mode: 0644]
PLC/Methods/BootUpdateNode.py [new file with mode: 0644]
PLC/Methods/DeleteAddress.py [new file with mode: 0644]
PLC/Methods/DeleteAddressType.py [new file with mode: 0644]
PLC/Methods/DeleteAddressTypeFromAddress.py [new file with mode: 0644]
PLC/Methods/DeleteBootState.py [new file with mode: 0644]
PLC/Methods/DeleteConfFile.py [new file with mode: 0644]
PLC/Methods/DeleteConfFileFromNode.py [new file with mode: 0644]
PLC/Methods/DeleteConfFileFromNodeGroup.py [new file with mode: 0644]
PLC/Methods/DeleteInitScript.py [new file with mode: 0644]
PLC/Methods/DeleteKey.py [new file with mode: 0644]
PLC/Methods/DeleteKeyType.py [new file with mode: 0644]
PLC/Methods/DeleteMessage.py [new file with mode: 0644]
PLC/Methods/DeleteNetworkMethod.py [new file with mode: 0644]
PLC/Methods/DeleteNetworkType.py [new file with mode: 0644]
PLC/Methods/DeleteNode.py [new file with mode: 0644]
PLC/Methods/DeleteNodeFromNodeGroup.py [new file with mode: 0644]
PLC/Methods/DeleteNodeFromPCU.py [new file with mode: 0644]
PLC/Methods/DeleteNodeGroup.py [new file with mode: 0644]
PLC/Methods/DeleteNodeNetwork.py [new file with mode: 0644]
PLC/Methods/DeleteNodeNetworkSetting.py [new file with mode: 0644]
PLC/Methods/DeleteNodeNetworkSettingType.py [new file with mode: 0644]
PLC/Methods/DeletePCU.py [new file with mode: 0644]
PLC/Methods/DeletePCUProtocolType.py [new file with mode: 0644]
PLC/Methods/DeletePCUType.py [new file with mode: 0644]
PLC/Methods/DeletePeer.py [new file with mode: 0644]
PLC/Methods/DeletePerson.py [new file with mode: 0644]
PLC/Methods/DeletePersonFromSite.py [new file with mode: 0644]
PLC/Methods/DeletePersonFromSlice.py [new file with mode: 0644]
PLC/Methods/DeleteRole.py [new file with mode: 0644]
PLC/Methods/DeleteRoleFromPerson.py [new file with mode: 0644]
PLC/Methods/DeleteSession.py [new file with mode: 0644]
PLC/Methods/DeleteSite.py [new file with mode: 0644]
PLC/Methods/DeleteSlice.py [new file with mode: 0644]
PLC/Methods/DeleteSliceAttribute.py [new file with mode: 0644]
PLC/Methods/DeleteSliceAttributeType.py [new file with mode: 0644]
PLC/Methods/DeleteSliceFromNodes.py [new file with mode: 0644]
PLC/Methods/DeleteSliceFromNodesWhitelist.py [new file with mode: 0644]
PLC/Methods/DeleteSliceInstantiation.py [new file with mode: 0644]
PLC/Methods/GenerateNodeConfFile.py [new file with mode: 0644]
PLC/Methods/GetAddressTypes.py [new file with mode: 0644]
PLC/Methods/GetAddresses.py [new file with mode: 0644]
PLC/Methods/GetBootMedium.py [new file with mode: 0644]
PLC/Methods/GetBootStates.py [new file with mode: 0644]
PLC/Methods/GetConfFiles.py [new file with mode: 0644]
PLC/Methods/GetEventObjects.py [new file with mode: 0644]
PLC/Methods/GetEvents.py [new file with mode: 0644]
PLC/Methods/GetInitScripts.py [new file with mode: 0644]
PLC/Methods/GetKeyTypes.py [new file with mode: 0644]
PLC/Methods/GetKeys.py [new file with mode: 0644]
PLC/Methods/GetMessages.py [new file with mode: 0644]
PLC/Methods/GetNetworkMethods.py [new file with mode: 0644]
PLC/Methods/GetNetworkTypes.py [new file with mode: 0644]
PLC/Methods/GetNodeGroups.py [new file with mode: 0644]
PLC/Methods/GetNodeNetworkSettingTypes.py [new file with mode: 0644]
PLC/Methods/GetNodeNetworkSettings.py [new file with mode: 0644]
PLC/Methods/GetNodeNetworks.py [new file with mode: 0644]
PLC/Methods/GetNodes.py [new file with mode: 0644]
PLC/Methods/GetPCUProtocolTypes.py [new file with mode: 0644]
PLC/Methods/GetPCUTypes.py [new file with mode: 0644]
PLC/Methods/GetPCUs.py [new file with mode: 0644]
PLC/Methods/GetPeerData.py [new file with mode: 0644]
PLC/Methods/GetPeerName.py [new file with mode: 0644]
PLC/Methods/GetPeers.py [new file with mode: 0644]
PLC/Methods/GetPersons.py [new file with mode: 0644]
PLC/Methods/GetPlcRelease.py [new file with mode: 0644]
PLC/Methods/GetRoles.py [new file with mode: 0644]
PLC/Methods/GetSession.py [new file with mode: 0644]
PLC/Methods/GetSessions.py [new file with mode: 0644]
PLC/Methods/GetSites.py [new file with mode: 0644]
PLC/Methods/GetSliceAttributeTypes.py [new file with mode: 0644]
PLC/Methods/GetSliceAttributes.py [new file with mode: 0644]
PLC/Methods/GetSliceInstantiations.py [new file with mode: 0644]
PLC/Methods/GetSliceKeys.py [new file with mode: 0644]
PLC/Methods/GetSliceTicket.py [new file with mode: 0644]
PLC/Methods/GetSlices.py [new file with mode: 0644]
PLC/Methods/GetSlicesMD5.py [new file with mode: 0644]
PLC/Methods/GetSlivers.py [new file with mode: 0644]
PLC/Methods/GetWhitelist.py [new file with mode: 0644]
PLC/Methods/NotifyPersons.py [new file with mode: 0644]
PLC/Methods/NotifySupport.py [new file with mode: 0644]
PLC/Methods/RebootNode.py [new file with mode: 0644]
PLC/Methods/RefreshPeer.py [new file with mode: 0644]
PLC/Methods/ResetPassword.py [new file with mode: 0644]
PLC/Methods/SetPersonPrimarySite.py [new file with mode: 0644]
PLC/Methods/SliceCreate.py [new file with mode: 0644]
PLC/Methods/SliceDelete.py [new file with mode: 0644]
PLC/Methods/SliceExtendedInfo.py [new file with mode: 0644]
PLC/Methods/SliceGetTicket.py [new file with mode: 0644]
PLC/Methods/SliceInfo.py [new file with mode: 0644]
PLC/Methods/SliceListNames.py [new file with mode: 0644]
PLC/Methods/SliceListUserSlices.py [new file with mode: 0644]
PLC/Methods/SliceNodesAdd.py [new file with mode: 0644]
PLC/Methods/SliceNodesDel.py [new file with mode: 0644]
PLC/Methods/SliceNodesList.py [new file with mode: 0644]
PLC/Methods/SliceRenew.py [new file with mode: 0644]
PLC/Methods/SliceTicketGet.py [new file with mode: 0644]
PLC/Methods/SliceUpdate.py [new file with mode: 0644]
PLC/Methods/SliceUserAdd.py [new file with mode: 0644]
PLC/Methods/SliceUserDel.py [new file with mode: 0644]
PLC/Methods/SliceUsersList.py [new file with mode: 0644]
PLC/Methods/UpdateAddress.py [new file with mode: 0644]
PLC/Methods/UpdateAddressType.py [new file with mode: 0644]
PLC/Methods/UpdateConfFile.py [new file with mode: 0644]
PLC/Methods/UpdateInitScript.py [new file with mode: 0644]
PLC/Methods/UpdateKey.py [new file with mode: 0644]
PLC/Methods/UpdateMessage.py [new file with mode: 0644]
PLC/Methods/UpdateNode.py [new file with mode: 0644]
PLC/Methods/UpdateNodeGroup.py [new file with mode: 0644]
PLC/Methods/UpdateNodeNetwork.py [new file with mode: 0644]
PLC/Methods/UpdateNodeNetworkSetting.py [new file with mode: 0644]
PLC/Methods/UpdateNodeNetworkSettingType.py [new file with mode: 0644]
PLC/Methods/UpdatePCU.py [new file with mode: 0644]
PLC/Methods/UpdatePCUProtocolType.py [new file with mode: 0644]
PLC/Methods/UpdatePCUType.py [new file with mode: 0644]
PLC/Methods/UpdatePeer.py [new file with mode: 0644]
PLC/Methods/UpdatePerson.py [new file with mode: 0644]
PLC/Methods/UpdateSite.py [new file with mode: 0644]
PLC/Methods/UpdateSlice.py [new file with mode: 0644]
PLC/Methods/UpdateSliceAttribute.py [new file with mode: 0644]
PLC/Methods/UpdateSliceAttributeType.py [new file with mode: 0644]
PLC/Methods/VerifyPerson.py [new file with mode: 0644]
PLC/Methods/__init__.py [new file with mode: 0644]
PLC/Methods/system/.cvsignore [new file with mode: 0644]
PLC/Methods/system/__init__.py [new file with mode: 0644]
PLC/Methods/system/listMethods.py [new file with mode: 0644]
PLC/Methods/system/methodHelp.py [new file with mode: 0644]
PLC/Methods/system/methodSignature.py [new file with mode: 0644]
PLC/Methods/system/multicall.py [new file with mode: 0644]
PLC/NetworkMethods.py [new file with mode: 0644]
PLC/NetworkTypes.py [new file with mode: 0644]
PLC/NodeGroups.py [new file with mode: 0644]
PLC/NodeNetworkSettingTypes.py [new file with mode: 0644]
PLC/NodeNetworkSettings.py [new file with mode: 0644]
PLC/NodeNetworks.py [new file with mode: 0644]
PLC/Nodes.py [new file with mode: 0644]
PLC/PCUProtocolTypes.py [new file with mode: 0644]
PLC/PCUTypes.py [new file with mode: 0644]
PLC/PCUs.py [new file with mode: 0644]
PLC/POD.py [new file with mode: 0644]
PLC/Parameter.py [new file with mode: 0644]
PLC/Peers.py [new file with mode: 0644]
PLC/Persons.py [new file with mode: 0644]
PLC/PostgreSQL.py [new file with mode: 0644]
PLC/PyCurl.py [new file with mode: 0644]
PLC/Roles.py [new file with mode: 0644]
PLC/Sessions.py [new file with mode: 0644]
PLC/Shell.py [new file with mode: 0644]
PLC/Sites.py [new file with mode: 0644]
PLC/SliceAttributeTypes.py [new file with mode: 0644]
PLC/SliceAttributes.py [new file with mode: 0644]
PLC/SliceInstantiations.py [new file with mode: 0644]
PLC/Slices.py [new file with mode: 0644]
PLC/Table.py [new file with mode: 0644]
PLC/Test.py [new file with mode: 0644]
PLC/__init__.py [new file with mode: 0644]
PLC/sendmail.py [new file with mode: 0644]

diff --git a/PLC/.cvsignore b/PLC/.cvsignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
diff --git a/PLC/API.py b/PLC/API.py
new file mode 100644 (file)
index 0000000..5c4cb9d
--- /dev/null
@@ -0,0 +1,174 @@
+#
+# 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: API.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+import sys
+import traceback
+import string
+
+import xmlrpclib
+
+# See "2.2 Characters" in the XML specification:
+#
+# #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
+# avoiding
+# [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
+
+invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
+xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
+
+def xmlrpclib_escape(s, replace = string.replace):
+    """
+    xmlrpclib does not handle invalid 7-bit control characters. This
+    function augments xmlrpclib.escape, which by default only replaces
+    '&', '<', and '>' with entities.
+    """
+
+    # This is the standard xmlrpclib.escape function
+    s = replace(s, "&", "&amp;")
+    s = replace(s, "<", "&lt;")
+    s = replace(s, ">", "&gt;",)
+
+    # Replace invalid 7-bit control characters with '?'
+    return s.translate(xml_escape_table)
+
+def xmlrpclib_dump(self, value, write):
+    """
+    xmlrpclib cannot marshal instances of subclasses of built-in
+    types. This function overrides xmlrpclib.Marshaller.__dump so that
+    any value that is an instance of one of its acceptable types is
+    marshalled as that type.
+
+    xmlrpclib also cannot handle invalid 7-bit control characters. See
+    above.
+    """
+
+    # Use our escape function
+    args = [self, value, write]
+    if isinstance(value, (str, unicode)):
+        args.append(xmlrpclib_escape)
+
+    try:
+        # Try for an exact match first
+        f = self.dispatch[type(value)]
+    except KeyError:
+        # Try for an isinstance() match
+        for Type, f in self.dispatch.iteritems():
+            if isinstance(value, Type):
+                f(*args)
+                return
+        raise TypeError, "cannot marshal %s objects" % type(value)
+    else:
+        f(*args)
+
+# You can't hide from me!
+xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
+
+# SOAP support is optional
+try:
+    import SOAPpy
+    from SOAPpy.Parser import parseSOAPRPC
+    from SOAPpy.Types import faultType
+    from SOAPpy.NS import NS
+    from SOAPpy.SOAPBuilder import buildSOAP
+except ImportError:
+    SOAPpy = None
+
+from PLC.Config import Config
+from PLC.Faults import *
+import PLC.Methods
+
+class PLCAPI:
+    methods = PLC.Methods.methods
+
+    def __init__(self, config = "/etc/planetlab/plc_config", encoding = "utf-8"):
+        self.encoding = encoding
+
+        # 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":
+            from PLC.PostgreSQL import PostgreSQL
+            self.db = PostgreSQL(self)
+
+        else:
+            raise PLCAPIError, "Unsupported database type " + self.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 Exception, e:
+            if SOAPpy is not None:
+                interface = SOAPpy
+                (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
+                method = r._name
+                args = r._aslist()
+                # XXX Support named arguments
+            else:
+                raise e
+
+        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, encoding = self.encoding, allow_none = 1)
+        elif interface == SOAPpy:
+            data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
+
+        return data
diff --git a/PLC/AddressTypes.py b/PLC/AddressTypes.py
new file mode 100644 (file)
index 0000000..7156c00
--- /dev/null
@@ -0,0 +1,66 @@
+#
+# 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: AddressTypes.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from types import StringTypes
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+
+class AddressType(Row):
+    """
+    Representation of a row in the address_types table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'address_types'
+    primary_key = 'address_type_id'
+    join_tables = ['address_address_type']
+    fields = {
+        'address_type_id': Parameter(int, "Address type identifier"),
+        'name': Parameter(str, "Address type", max = 20),
+        'description': Parameter(str, "Address type description", max = 254),
+        }
+
+    def validate_name(self, name):
+       # Make sure name is not blank
+        if not len(name):
+            raise PLCInvalidArgument, "Address type must be specified"
+       
+       # Make sure address type does not already exist
+       conflicts = AddressTypes(self.api, [name])
+       for address_type_id in conflicts:
+            if 'address_type_id' not in self or self['address_type_id'] != address_type_id:
+               raise PLCInvalidArgument, "Address type name already in use"
+
+       return name
+
+class AddressTypes(Table):
+    """
+    Representation of the address_types table in the database.
+    """
+
+    def __init__(self, api, address_type_filter = None, columns = None):
+       Table.__init__(self, api, AddressType, columns)
+
+        sql = "SELECT %s FROM address_types WHERE True" % \
+              ", ".join(self.columns)
+
+        if address_type_filter is not None:
+            if isinstance(address_type_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), address_type_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), address_type_filter)
+                address_type_filter = Filter(AddressType.fields, {'address_type_id': ints, 'name': strs})
+                sql += " AND (%s) %s" % address_type_filter.sql(api, "OR")
+            elif isinstance(address_type_filter, dict):
+                address_type_filter = Filter(AddressType.fields, address_type_filter)
+                sql += " AND (%s) %s" % address_type_filter.sql(api, "AND")
+
+        self.selectall(sql)
diff --git a/PLC/Addresses.py b/PLC/Addresses.py
new file mode 100644 (file)
index 0000000..36c0d1d
--- /dev/null
@@ -0,0 +1,99 @@
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+from PLC.Filter import Filter
+from PLC.AddressTypes import AddressType, AddressTypes
+
+class Address(Row):
+    """
+    Representation of a row in the addresses table. To use, instantiate
+    with a dict of values.
+    """
+
+    table_name = 'addresses'
+    primary_key = 'address_id'
+    join_tables = ['address_address_type', 'site_address']
+    fields = {
+        'address_id': Parameter(int, "Address identifier"),
+        'line1': Parameter(str, "Address line 1", max = 254),
+        'line2': Parameter(str, "Address line 2", max = 254, nullok = True),
+        'line3': Parameter(str, "Address line 3", max = 254, nullok = True),
+        'city': Parameter(str, "City", max = 254),
+        'state': Parameter(str, "State or province", max = 254),
+        'postalcode': Parameter(str, "Postal code", max = 64),
+        'country': Parameter(str, "Country", max = 128),
+        'address_type_ids': Parameter([int], "Address type identifiers"),
+        'address_types': Parameter([str], "Address types"),
+        }
+
+    def add_address_type(self, address_type, commit = True):
+        """
+        Add address type to existing address.
+        """
+
+        assert 'address_id' in self
+        assert isinstance(address_type, AddressType)
+        assert 'address_type_id' in address_type
+
+        address_id = self['address_id']
+        address_type_id = address_type['address_type_id']
+
+        if address_type_id not in self['address_type_ids']:
+            assert address_type['name'] not in self['address_types']
+
+            self.api.db.do("INSERT INTO address_address_type (address_id, address_type_id)" \
+                           " VALUES(%(address_id)d, %(address_type_id)d)",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['address_type_ids'].append(address_type_id)
+            self['address_types'].append(address_type['name'])
+
+    def remove_address_type(self, address_type, commit = True):
+        """
+        Add address type to existing address.
+        """
+
+        assert 'address_id' in self
+        assert isinstance(address_type, AddressType)
+        assert 'address_type_id' in address_type
+
+        address_id = self['address_id']
+        address_type_id = address_type['address_type_id']
+
+        if address_type_id in self['address_type_ids']:
+            assert address_type['name'] in self['address_types']
+
+            self.api.db.do("DELETE FROM address_address_type" \
+                           " WHERE address_id = %(address_id)d" \
+                           " AND address_type_id = %(address_type_id)d",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['address_type_ids'].remove(address_type_id)
+            self['address_types'].remove(address_type['name'])
+
+class Addresses(Table):
+    """
+    Representation of row(s) from the addresses table in the
+    database.
+    """
+
+    def __init__(self, api, address_filter = None, columns = None):
+       Table.__init__(self, api, Address, columns)
+
+        sql = "SELECT %s FROM view_addresses WHERE True" % \
+              ", ".join(self.columns)
+
+        if address_filter is not None:
+            if isinstance(address_filter, (list, tuple, set)):
+                address_filter = Filter(Address.fields, {'address_id': address_filter})
+            elif isinstance(address_filter, dict):
+                address_filter = Filter(Address.fields, address_filter)
+            sql += " AND (%s) %s" % address_filter.sql(api)
+
+        self.selectall(sql)
diff --git a/PLC/Auth.py b/PLC/Auth.py
new file mode 100644 (file)
index 0000000..498acc5
--- /dev/null
@@ -0,0 +1,332 @@
+#
+# PLCAPI authentication parameters
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Auth.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+import crypt
+import sha
+import hmac
+import time
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Persons
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Sessions import Session, Sessions
+from PLC.Peers import Peer, Peers
+from PLC.Boot import notify_owners
+
+class Auth(Parameter):
+    """
+    Base class for all API authentication methods, as well as a class
+    that can be used to represent all supported API authentication
+    methods.
+    """
+
+    def __init__(self, auth = None):
+        if auth is None:
+            auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
+        Parameter.__init__(self, auth, "API authentication structure")
+
+    def check(self, method, auth, *args):
+        # Method.type_check() should have checked that all of the
+        # mandatory fields were present.
+        assert 'AuthMethod' in auth
+
+        if auth['AuthMethod'] == "session":
+            expected = SessionAuth()
+        elif auth['AuthMethod'] == "password" or \
+             auth['AuthMethod'] == "capability":
+            expected = PasswordAuth()
+        elif auth['AuthMethod'] == "gpg":
+            expected = GPGAuth()
+        elif auth['AuthMethod'] == "hmac":
+            expected = BootAuth()
+        elif auth['AuthMethod'] == "anonymous":
+            expected = AnonymousAuth()
+        else:
+            raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', or 'anonymous'", "AuthMethod")
+
+        # Re-check using the specified authentication method
+        method.type_check("auth", auth, expected, (auth,) + args)
+
+class GPGAuth(Auth):
+    """
+    Proposed PlanetLab federation authentication structure.
+    """
+
+    def __init__(self):
+        Auth.__init__(self, {
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
+            'name': Parameter(str, "Peer or user name", optional = False),
+            'signature': Parameter(str, "Message signature", optional = False)
+            })
+
+    def check(self, method, auth, *args):
+        try:
+            peers = Peers(method.api, [auth['name']])
+            if peers:
+                if 'peer' not in method.roles:
+                    raise PLCAuthenticationFailure, "Not allowed to call method"
+
+                method.caller = peer = peers[0]
+                keys = [peer['key']]
+            else:
+                persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
+                if not persons:
+                    raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
+
+                if not set(person['roles']).intersection(method.roles):
+                    raise PLCAuthenticationFailure, "Not allowed to call method"
+
+                method.caller = person = persons[0]
+                keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
+
+            if not keys:
+                raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
+
+            for key in keys:
+                try:
+                    from PLC.GPG import gpg_verify
+                    gpg_verify(args, key, auth['signature'], method.name)
+                    return
+                except PLCAuthenticationFailure, fault:
+                    pass
+
+            raise fault
+
+        except PLCAuthenticationFailure, fault:
+            # XXX Send e-mail
+            raise fault
+
+class SessionAuth(Auth):
+    """
+    Secondary authentication method. After authenticating with a
+    primary authentication method, call GetSession() to generate a
+    session key that may be used for subsequent calls.
+    """
+
+    def __init__(self):
+        Auth.__init__(self, {
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
+            'session': Parameter(str, "Session key", optional = 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('session')
+
+        # Get session record
+        sessions = Sessions(method.api, [auth['session']], expires = None)
+        if not sessions:
+            raise PLCAuthenticationFailure, "No such session"
+        session = sessions[0]
+
+        try:
+            if session['node_id'] is not None:
+                nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
+                if not nodes:
+                    raise PLCAuthenticationFailure, "No such node"
+                node = nodes[0]
+
+                if 'node' not in method.roles:
+                    raise PLCAuthenticationFailure, "Not allowed to call method"
+
+                method.caller = node
+
+            elif session['person_id'] is not None and session['expires'] > time.time():
+                persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
+                if not persons:
+                    raise PLCAuthenticationFailure, "No such account"
+                person = persons[0]
+
+                if not set(person['roles']).intersection(method.roles):
+                    raise PLCPermissionDenied, "Not allowed to call method"
+
+                method.caller = persons[0]
+
+            else:
+                raise PLCAuthenticationFailure, "Invalid session"
+
+        except PLCAuthenticationFailure, fault:
+            session.delete()
+            raise fault
+
+class BootAuth(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.
+
+    The original parameter serialization code did not define the byte
+    encoding of strings, or the string encoding of all other types. We
+    define the byte encoding to be UTF-8, and the string encoding of
+    all other types to be however Python version 2.3 unicode() encodes
+    them.
+    """
+
+    def __init__(self):
+        Auth.__init__(self, {
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
+            'node_id': Parameter(int, "Node identifier", optional = False),
+            'value': Parameter(str, "HMAC of node key and method call", optional = False)
+            })
+
+    def canonicalize(self, args):
+        values = []
+
+        for arg in args:
+            if isinstance(arg, list) or isinstance(arg, tuple):
+                # The old implementation did not recursively handle
+                # lists of lists. But neither did the old API itself.
+                values += self.canonicalize(arg)
+            elif isinstance(arg, dict):
+                # Yes, the comments in the old implementation are
+                # misleading. Keys of dicts are not included in the
+                # hash.
+                values += self.canonicalize(arg.values())
+            else:
+                # We use unicode() instead of str().
+                values.append(unicode(arg))
+
+        return values
+
+    def check(self, method, auth, *args):
+        # Method.type_check() should have checked that all of the
+        # mandatory fields were present.
+        assert auth.has_key('node_id')
+
+        if 'node' not in method.roles:
+            raise PLCAuthenticationFailure, "Not allowed to call method"
+
+        try:
+            nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
+            if not nodes:
+                raise PLCAuthenticationFailure, "No such node"
+            node = nodes[0]
+
+            if node['key']:
+                key = node['key']
+            elif node['boot_nonce']:
+                # Allow very old nodes that do not have a node key in
+                # their configuration files to use their "boot nonce"
+                # instead. The boot nonce is a random value generated
+                # by the node itself and POSTed by the Boot CD when it
+                # requests the Boot Manager. This is obviously not
+                # very secure, so we only allow it to be used if the
+                # requestor IP is the same as the IP address we have
+                # on record for the node.
+                key = node['boot_nonce']
+
+                nodenetwork = None
+                if node['nodenetwork_ids']:
+                    nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
+                    for nodenetwork in nodenetworks:
+                        if nodenetwork['is_primary']:
+                            break
+            
+                if not nodenetwork or not nodenetwork['is_primary']:
+                    raise PLCAuthenticationFailure, "No primary network interface on record"
+            
+                if method.source is None:
+                    raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
+
+                if nodenetwork['ip'] != method.source[0]:
+                    raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
+                          (method.source[0], nodenetwork['ip'])
+            else:
+                raise PLCAuthenticationFailure, "No node key or boot nonce"
+
+            # Yes, this is the "canonicalization" method used.
+            args = self.canonicalize(args)
+            args.sort()
+            msg = "[" + "".join(args) + "]"
+
+            # We encode in UTF-8 before calculating the HMAC, which is
+            # an 8-bit algorithm.
+            digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
+
+            if digest != auth['value']:
+                raise PLCAuthenticationFailure, "Call could not be authenticated"
+
+            method.caller = node
+
+        except PLCAuthenticationFailure, fault:
+            if nodes:
+                notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
+            raise fault
+
+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):
+        if 'anonymous' not in method.roles:
+            raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
+
+        method.caller = None
+
+class PasswordAuth(Auth):
+    """
+    PlanetLab version 3.x password authentication structure.
+    """
+
+    def __init__(self):
+        Auth.__init__(self, {
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
+            'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
+            'AuthString': Parameter(str, "Authentication string, typically a password", optional = 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, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
+        if len(persons) != 1:
+            raise PLCAuthenticationFailure, "No such account"
+
+        person = persons[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:
+            # Compare encrypted plaintext against encrypted password stored in the DB
+            plaintext = auth['AuthString'].encode(method.api.encoding)
+            password = person['password']
+
+            # Protect against blank passwords in the DB
+            if password is None or password[:12] == "" or \
+               crypt.crypt(plaintext, password[:12]) != password:
+                raise PLCAuthenticationFailure, "Password verification failed"
+
+        if not set(person['roles']).intersection(method.roles):
+           raise PLCAuthenticationFailure, "Not allowed to call method"
+
+        method.caller = person
diff --git a/PLC/Boot.py b/PLC/Boot.py
new file mode 100644 (file)
index 0000000..5f25354
--- /dev/null
@@ -0,0 +1,61 @@
+#
+# Boot Manager support
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2007 The Trustees of Princeton University
+#
+# $Id: Boot.py 6955 2007-11-19 15:40:41Z soltesz $
+#
+
+from PLC.Faults import *
+from PLC.Debug import log
+from PLC.Messages import Message, Messages
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.sendmail import sendmail
+
+def notify_owners(method, node, message_id,
+                  include_pis = False, include_techs = False, include_support = False,
+                  fault = None):
+    messages = Messages(method.api, [message_id], enabled = True)
+    if not messages:
+        print >> log, "No such message template '%s'" % message_id
+        return 1
+    message = messages[0]
+
+    To = []
+
+    if method.api.config.PLC_MAIL_BOOT_ADDRESS:
+        To.append(("Boot Messages", method.api.config.PLC_MAIL_BOOT_ADDRESS))
+
+    if include_support and method.api.config.PLC_MAIL_SUPPORT_ADDRESS:
+        To.append(("%s Support" % method.api.config.PLC_NAME,
+                   method.api.config.PLC_MAIL_SUPPORT_ADDRESS))
+
+    if include_pis or include_techs:
+        sites = Sites(method.api, [node['site_id']])
+        if not sites:
+            raise PLCAPIError, "No site associated with node"
+        site = sites[0]
+
+        persons = Persons(method.api, site['person_ids'])
+        for person in persons:
+            if (include_pis and 'pi' in person['roles'] and person['enabled']) or \
+               (include_techs and 'tech' in person['roles'] and person['enabled']) :
+                To.append(("%s %s" % (person['first_name'], person['last_name']), person['email']))
+
+    # Send email
+    params = {'node_id': node['node_id'],
+              'hostname': node['hostname'],
+              'PLC_WWW_HOST': method.api.config.PLC_WWW_HOST,
+              'PLC_WWW_SSL_PORT': method.api.config.PLC_WWW_SSL_PORT,
+              'fault': fault}
+
+    sendmail(method.api, To = To,
+             Subject = message['subject'] % params,
+             Body = message['template'] % params)
+
+    # Logging variables
+    method.object_type = "Node"
+    method.object_ids = [node['node_id']]
+    method.message = "Sent message %s" % message_id
diff --git a/PLC/BootStates.py b/PLC/BootStates.py
new file mode 100644 (file)
index 0000000..014b61a
--- /dev/null
@@ -0,0 +1,53 @@
+#
+# Functions for interacting with the boot_states table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: BootStates.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+
+class BootState(Row):
+    """
+    Representation of a row in the boot_states table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'boot_states'
+    primary_key = 'boot_state'
+    join_tables = ['nodes']
+    fields = {
+        'boot_state': Parameter(str, "Boot state", max = 20),
+        }
+
+    def validate_boot_state(self, name):
+       # Make sure name is not blank
+        if not len(name):
+            raise PLCInvalidArgument, "Boot state must be specified"
+       
+       # Make sure boot state does not alredy exist
+       conflicts = BootStates(self.api, [name])
+        if conflicts:
+            raise PLCInvalidArgument, "Boot state name already in use"
+
+       return name
+
+class BootStates(Table):
+    """
+    Representation of the boot_states table in the database.
+    """
+
+    def __init__(self, api, boot_states = None):
+        Table.__init__(self, api, BootState)
+
+        sql = "SELECT %s FROM boot_states" % \
+              ", ".join(BootState.fields)
+        
+        if boot_states:
+            sql += " WHERE boot_state IN (%s)" % ", ".join(map(api.db.quote, boot_states))
+
+        self.selectall(sql)
diff --git a/PLC/ConfFiles.py b/PLC/ConfFiles.py
new file mode 100644 (file)
index 0000000..f15a574
--- /dev/null
@@ -0,0 +1,155 @@
+#
+# Functions for interacting with the conf_files table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: ConfFiles.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+from PLC.Nodes import Node, Nodes
+from PLC.NodeGroups import NodeGroup, NodeGroups
+
+class ConfFile(Row):
+    """
+    Representation of a row in the conf_files table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'conf_files'
+    primary_key = 'conf_file_id'
+    join_tables = ['conf_file_node', 'conf_file_nodegroup']
+    fields = {
+        'conf_file_id': Parameter(int, "Configuration file identifier"),
+        'enabled': Parameter(bool, "Configuration file is active"),
+        'source': Parameter(str, "Relative path on the boot server where file can be downloaded", max = 255),
+        'dest': Parameter(str, "Absolute path where file should be installed", max = 255),
+        'file_permissions': Parameter(str, "chmod(1) permissions", max = 20),
+        'file_owner': Parameter(str, "chown(1) owner", max = 50),
+        'file_group': Parameter(str, "chgrp(1) owner", max = 50),
+        'preinstall_cmd': Parameter(str, "Shell command to execute prior to installing", max = 1024, nullok = True),
+        'postinstall_cmd': Parameter(str, "Shell command to execute after installing", max = 1024, nullok = True),
+        'error_cmd': Parameter(str, "Shell command to execute if any error occurs", max = 1024, nullok = True),
+        'ignore_cmd_errors': Parameter(bool, "Install file anyway even if an error occurs"),
+        'always_update': Parameter(bool, "Always attempt to install file even if unchanged"),
+        'node_ids': Parameter(int, "List of nodes linked to this file"),
+        'nodegroup_ids': Parameter(int, "List of node groups linked to this file"),
+        }
+
+    def add_node(self, node, commit = True):
+        """
+        Add configuration file to node.
+        """
+
+        assert 'conf_file_id' in self
+        assert isinstance(node, Node)
+        assert 'node_id' in node
+
+        conf_file_id = self['conf_file_id']
+        node_id = node['node_id']
+
+        if node_id not in self['node_ids']:
+            self.api.db.do("INSERT INTO conf_file_node (conf_file_id, node_id)" \
+                           " VALUES(%(conf_file_id)d, %(node_id)d)",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['node_ids'].append(node_id)
+            node['conf_file_ids'].append(conf_file_id)
+
+    def remove_node(self, node, commit = True):
+        """
+        Remove configuration file from node.
+        """
+
+        assert 'conf_file_id' in self
+        assert isinstance(node, Node)
+        assert 'node_id' in node
+
+        conf_file_id = self['conf_file_id']
+        node_id = node['node_id']
+
+        if node_id in self['node_ids']:
+            self.api.db.do("DELETE FROM conf_file_node" \
+                           " WHERE conf_file_id = %(conf_file_id)d" \
+                           " AND node_id = %(node_id)d",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['node_ids'].remove(node_id)
+            node['conf_file_ids'].remove(conf_file_id)
+
+    def add_nodegroup(self, nodegroup, commit = True):
+        """
+        Add configuration file to node group.
+        """
+
+        assert 'conf_file_id' in self
+        assert isinstance(nodegroup, NodeGroup)
+        assert 'nodegroup_id' in nodegroup
+
+        conf_file_id = self['conf_file_id']
+        nodegroup_id = nodegroup['nodegroup_id']
+
+        if nodegroup_id not in self['nodegroup_ids']:
+            self.api.db.do("INSERT INTO conf_file_nodegroup (conf_file_id, nodegroup_id)" \
+                           " VALUES(%(conf_file_id)d, %(nodegroup_id)d)",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['nodegroup_ids'].append(nodegroup_id)
+            nodegroup['conf_file_ids'].append(conf_file_id)
+
+    def remove_nodegroup(self, nodegroup, commit = True):
+        """
+        Remove configuration file from node group.
+        """
+
+        assert 'conf_file_id' in self
+        assert isinstance(nodegroup, NodeGroup)
+        assert 'nodegroup_id' in nodegroup
+
+        conf_file_id = self['conf_file_id']
+        nodegroup_id = nodegroup['nodegroup_id']
+
+        if nodegroup_id in self['nodegroup_ids']:
+            self.api.db.do("DELETE FROM conf_file_nodegroup" \
+                           " WHERE conf_file_id = %(conf_file_id)d" \
+                           " AND nodegroup_id = %(nodegroup_id)d",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['nodegroup_ids'].remove(nodegroup_id)
+            nodegroup['conf_file_ids'].remove(conf_file_id)
+
+class ConfFiles(Table):
+    """
+    Representation of the conf_files table in the database.
+    """
+
+    def __init__(self, api, conf_file_filter = None, columns = None):
+       Table.__init__(self, api, ConfFile, columns)
+
+        sql = "SELECT %s FROM view_conf_files WHERE True" % \
+              ", ".join(self.columns)
+
+        if conf_file_filter is not None:
+            if isinstance(conf_file_filter, (list, tuple, set)):
+                conf_file_filter = Filter(ConfFile.fields, {'conf_file_id': conf_file_filter})
+            elif isinstance(conf_file_filter, dict):
+                conf_file_filter = Filter(ConfFile.fields, conf_file_filter)
+            sql += " AND (%s) %s" % conf_file_filter.sql(api)
+
+        self.selectall(sql)
diff --git a/PLC/Config.py b/PLC/Config.py
new file mode 100644 (file)
index 0000000..693ba22
--- /dev/null
@@ -0,0 +1,96 @@
+#!/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: Config.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+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(myplc + os.sep + "plc_config", self.__dict__)
+            except:
+                raise PLCAPIError("Could not find plc_config in " + \
+                                  file + ", " + \
+                                  myplc + os.sep + "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 + os.sep + "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())
diff --git a/PLC/Debug.py b/PLC/Debug.py
new file mode 100644 (file)
index 0000000..b8dac85
--- /dev/null
@@ -0,0 +1,54 @@
+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__":
+    def sleep(seconds = 1):
+        time.sleep(seconds)
+
+    sleep = profile(sleep)
+
+    sleep(1)
diff --git a/PLC/EventObjects.py b/PLC/EventObjects.py
new file mode 100644 (file)
index 0000000..52b44c7
--- /dev/null
@@ -0,0 +1,63 @@
+#
+# Functions for interacting with the events table in the database
+#
+# Tony Mack <tmack@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: EventObjects.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+
+class EventObject(Row):
+    """
+    Representation of a row in the event_object table. 
+    """
+    
+    table_name = 'event_object'
+    primary_key = 'event_id'
+    fields = {
+        'event_id': Parameter(int, "Event identifier"),
+        'person_id': Parameter(int, "Identifier of person responsible for event, if any"),
+        'node_id': Parameter(int, "Identifier of node responsible for event, if any"),
+        'fault_code': Parameter(int, "Event fault code"),
+       'call_name': Parameter(str, "Call responsible for this event"),
+       'call': Parameter(str, "Call responsible for this event, including paramters"),
+       'message': Parameter(str, "High level description of this event"),
+        'runtime': Parameter(float, "Runtime of event"),
+        'time': Parameter(int, "Date and time that the event took place, in seconds since UNIX epoch", ro = True),
+        'object_id': Parameter(int, "ID of objects affected by this event"),
+       'object_type': Parameter(str, "What type of object is this event affecting")
+       }    
+
+class EventObjects(Table):
+    """
+    Representation of row(s) from the event_object table in the database. 
+    """
+
+    def __init__(self, api, event_filter = None, columns = None):
+        Table.__init__(self, api, EventObject, columns)
+       
+       sql = "SELECT %s FROM view_event_objects WHERE True" % \
+            ", ".join(self.columns)
+        
+       if event_filter is not None:
+            if isinstance(event_filter, (list, tuple, set)):
+                event_filter = Filter(EventObject.fields, {'event_id': event_filter})
+                sql += " AND (%s) %s" % event_filter.sql(api, "OR")
+            elif isinstance(event_filter, dict):
+                event_filter = Filter(EventObject.fields, event_filter)
+                sql += " AND (%s) %s" % event_filter.sql(api, "AND")
+            elif isinstance (event_filter, int):
+                event_filter = Filter(EventObject.fields, {'event_id':[event_filter]})
+                sql += " AND (%s) %s" % event_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong event object filter %r"%event_filter
+# with new filtering, caller needs to set this explicitly
+#      sql += " ORDER BY %s" % EventObject.primary_key
+        
+       self.selectall(sql)
diff --git a/PLC/Events.py b/PLC/Events.py
new file mode 100644 (file)
index 0000000..0d319cf
--- /dev/null
@@ -0,0 +1,79 @@
+#
+# Functions for interacting with the events table in the database
+#
+# Tony Mack <tmack@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Events.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+
+class Event(Row):
+    """
+    Representation of a row in the events table. 
+    """
+    
+    table_name = 'events'
+    primary_key = 'event_id'
+    fields = {
+        'event_id': Parameter(int, "Event identifier"),
+        'person_id': Parameter(int, "Identifier of person responsible for event, if any"),
+        'node_id': Parameter(int, "Identifier of node responsible for event, if any"),
+       'auth_type': Parameter(int, "Type of auth used. i.e. AuthMethod"),
+        'fault_code': Parameter(int, "Event fault code"),
+       'call_name': Parameter(str, "Call responsible for this event"),
+       'call': Parameter(str, "Call responsible for this event, including paramters"),
+       'message': Parameter(str, "High level description of this event"),
+        'runtime': Parameter(float, "Runtime of event"),
+        'time': Parameter(int, "Date and time that the event took place, in seconds since UNIX epoch", ro = True),
+        'object_ids': Parameter([int], "IDs of objects affected by this event"),
+       'object_types': Parameter([str], "What type of object were affected by this event")
+       }    
+
+    def add_object(self, object_type, object_id, commit = True):
+        """
+        Relate object to this event.
+        """
+
+        assert 'event_id' in self
+
+        event_id = self['event_id']
+
+        if 'object_ids' not in self:
+            self['object_ids'] = []
+
+        if object_id not in self['object_ids']:
+            self.api.db.do("INSERT INTO event_object (event_id, object_id, object_type)" \
+                           " VALUES(%(event_id)d, %(object_id)d, %(object_type)s)",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['object_ids'].append(object_id)
+    
+class Events(Table):
+    """
+    Representation of row(s) from the events table in the database. 
+    """
+
+    def __init__(self, api, event_filter = None, columns = None):
+        Table.__init__(self, api, Event, columns)
+
+        sql = "SELECT %s FROM view_events WHERE True" % \
+              ", ".join(self.columns)
+
+        if event_filter is not None:
+            if isinstance(event_filter, (list, tuple, set)):
+                event_filter = Filter(Event.fields, {'event_id': event_filter})
+            elif isinstance(event_filter, dict):
+                event_filter = Filter(Event.fields, event_filter)
+            sql += " AND (%s) %s" % event_filter.sql(api)
+# with new filtering, caller needs to set this explicitly
+#      sql += " ORDER BY %s" % Event.primary_key
+        self.selectall(sql)
diff --git a/PLC/Faults.py b/PLC/Faults.py
new file mode 100644 (file)
index 0000000..e3d46e5
--- /dev/null
@@ -0,0 +1,67 @@
+#
+# 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: Faults.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+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 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 PLCNotImplemented(PLCFault):
+    def __init__(self, extra = None):
+        faultString = "Not fully implemented"
+        PLCFault.__init__(self, 109, faultString, extra)
+
+class PLCAPIError(PLCFault):
+    def __init__(self, extra = None):
+        faultString = "Internal API error"
+        PLCFault.__init__(self, 111, faultString, extra)
diff --git a/PLC/Filter.py b/PLC/Filter.py
new file mode 100644 (file)
index 0000000..195c4e6
--- /dev/null
@@ -0,0 +1,203 @@
+from types import StringTypes
+try:
+    set
+except NameError:
+    from sets import Set
+    set = Set
+
+import time
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed, python_type
+
+class Filter(Parameter, dict):
+    """
+    A type of parameter that represents a filter on one or more
+    columns of a database table.
+    Special features provide support for negation, upper and lower bounds, 
+    as well as sorting and clipping.
+
+
+    fields should be a dictionary of field names and types
+    Only filters on non-sequence type fields are supported.
+    example : fields = {'node_id': Parameter(int, "Node identifier"),
+                        'hostname': Parameter(int, "Fully qualified hostname", max = 255),
+                        ...}
+
+
+    filter should be a dictionary of field names and values
+    representing  the criteria for filtering. 
+    example : filter = { 'hostname' : '*.edu' , site_id : [34,54] }
+    Whether the filter represents an intersection (AND) or a union (OR) 
+    of these criteria is determined by the join_with argument 
+    provided to the sql method below
+
+    Special features:
+
+    * a field starting with the ~ character means negation.
+    example :  filter = { '~peer_id' : None }
+
+    * a field starting with < [  ] or > means lower than or greater than
+      < > uses strict comparison
+      [ ] is for using <= or >= instead
+    example :  filter = { ']event_id' : 2305 }
+    example :  filter = { '>time' : 1178531418 }
+      in this example the integer value denotes a unix timestamp
+
+    * if a value is a sequence type, then it should represent 
+      a list of possible values for that field
+    example : filter = { 'node_id' : [12,34,56] }
+
+    * a (string) value containing either a * or a % character is
+      treated as a (sql) pattern; * are replaced with % that is the
+      SQL wildcard character.
+    example :  filter = { 'hostname' : '*.jp' } 
+
+    * fields starting with - are special and relate to row selection, i.e. sorting and clipping
+    * '-SORT' : a field name, or an ordered list of field names that are used for sorting
+      these fields may start with + (default) or - for denoting increasing or decreasing order
+    example : filter = { '-SORT' : [ '+node_id', '-hostname' ] }
+    * '-OFFSET' : the number of first rows to be ommitted
+    * '-LIMIT' : the amount of rows to be returned 
+    example : filter = { '-OFFSET' : 100, '-LIMIT':25}
+
+    A realistic example would read
+    GetNodes ( { 'hostname' : '*.edu' , '-SORT' : 'hostname' , '-OFFSET' : 30 , '-LIMIT' : 25 } )
+    and that would return nodes matching '*.edu' in alphabetical order from 31th to 55th
+    """
+
+    def __init__(self, fields = {}, filter = {}, doc = "Attribute filter"):
+        # Store the filter in our dict instance
+        dict.__init__(self, filter)
+
+        # Declare ourselves as a type of parameter that can take
+        # either a value or a list of values for each of the specified
+        # fields.
+        self.fields = {}
+
+        for field, expected in fields.iteritems():
+            # Cannot filter on sequences
+            if python_type(expected) in (list, tuple, set):
+                continue
+            
+            # Accept either a value or a list of values of the specified type
+            self.fields[field] = Mixed(expected, [expected])
+
+        # Null filter means no filter
+        Parameter.__init__(self, self.fields, doc = doc, nullok = True)
+
+    # this code is not used anymore
+    # at some point the select in the DB for event objects was done on
+    # the events table directly, that is stored as a timestamp, thus comparisons
+    # needed to be done based on SQL timestamps as well
+    def unix2timestamp (self,unix):
+       s = time.gmtime(unix)
+       return "TIMESTAMP'%04d-%02d-%02d %02d:%02d:%02d'" % (s.tm_year,s.tm_mon,s.tm_mday,
+                                                            s.tm_hour,s.tm_min,s.tm_sec)
+
+    def sql(self, api, join_with = "AND"):
+        """
+        Returns a SQL conditional that represents this filter.
+        """
+
+        # So that we always return something
+        if join_with == "AND":
+            conditionals = ["True"]
+        elif join_with == "OR":
+            conditionals = ["False"]
+        else:
+            assert join_with in ("AND", "OR")
+
+        # init 
+        sorts = []
+        clips = []
+
+        for field, value in self.iteritems():
+           # handle negation, numeric comparisons
+           # simple, 1-depth only mechanism
+
+           modifiers={'~' : False, 
+                      '<' : False, '>' : False,
+                      '[' : False, ']' : False,
+                       '-' : False,
+                      }
+
+           for char in modifiers.keys():
+               if field[0] == char:
+                   modifiers[char]=True;
+                   field = field[1:]
+                   break
+
+            # filter on fields
+            if not modifiers['-']:
+                if field not in self.fields:
+                    raise PLCInvalidArgument, "Invalid filter field '%s'" % field
+
+                if isinstance(value, (list, tuple, set)):
+                    # Turn empty list into (NULL) instead of invalid ()
+                    if not value:
+                        value = [None]
+
+                    operator = "IN"
+                    value = map(str, map(api.db.quote, value))
+                    value = "(%s)" % ", ".join(value)
+                else:
+                    if value is None:
+                        operator = "IS"
+                        value = "NULL"
+                    elif isinstance(value, StringTypes) and \
+                            (value.find("*") > -1 or value.find("%") > -1):
+                        operator = "LIKE"
+                        value = str(api.db.quote(value.replace("*", "%")))
+                    else:
+                        operator = "="
+                        if modifiers['<']:
+                            operator='<'
+                        if modifiers['>']:
+                            operator='>'
+                        if modifiers['[']:
+                            operator='<='
+                        if modifiers[']']:
+                            operator='>='
+                        else:
+                            value = str(api.db.quote(value))
+                clause = "%s %s %s" % (field, operator, value)
+
+                if modifiers['~']:
+                    clause = " ( NOT %s ) " % (clause)
+
+                conditionals.append(clause)
+            # sorting and clipping
+            else:
+                if field not in ('SORT','OFFSET','LIMIT'):
+                    raise PLCInvalidArgument, "Invalid filter, unknown sort and clip field %r"%field
+                # sorting
+                if field == 'SORT':
+                    if not isinstance(value,(list,tuple,set)):
+                        value=[value]
+                    for field in value:
+                        order = 'ASC'
+                        if field[0] == '+':
+                            field = field[1:]
+                        elif field[0] == '-':
+                            field = field[1:]
+                            order = 'DESC'
+                        if field not in self.fields:
+                            raise PLCInvalidArgument, "Invalid field %r in SORT filter"%field
+                        sorts.append("%s %s"%(field,order))
+                # clipping
+                elif field == 'OFFSET':
+                    clips.append("OFFSET %d"%value)
+                # clipping continued
+                elif field == 'LIMIT' :
+                    clips.append("LIMIT %d"%value)
+
+        where_part = (" %s " % join_with).join(conditionals)
+        clip_part = ""
+        if sorts:
+            clip_part += " ORDER BY " + ",".join(sorts)
+        if clips:
+            clip_part += " " + " ".join(clips)
+#      print 'where_part=',where_part,'clip_part',clip_part
+        return (where_part,clip_part)
diff --git a/PLC/GPG.py b/PLC/GPG.py
new file mode 100644 (file)
index 0000000..457c32b
--- /dev/null
@@ -0,0 +1,183 @@
+#
+# Python "binding" for GPG. I'll write GPGME bindings eventually. The
+# intent is to use GPG to sign method calls, as a way of identifying
+# and authenticating peers. Calls should still go over an encrypted
+# transport such as HTTPS, with certificate checking.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: GPG.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+import os
+import xmlrpclib
+import shutil
+from types import StringTypes
+from StringIO import StringIO
+from xml.dom import minidom
+from xml.dom.ext import Canonicalize
+from subprocess import Popen, PIPE, call
+from tempfile import NamedTemporaryFile, mkdtemp
+
+from PLC.Faults import *
+
+def canonicalize(args, methodname = None, methodresponse = False):
+    """
+    Returns a canonicalized XML-RPC representation of the specified
+    method call (methodname != None) or response (methodresponse =
+    True).
+    """
+
+    xml = xmlrpclib.dumps(args, methodname, methodresponse, encoding = 'utf-8', allow_none = 1)
+    dom = minidom.parseString(xml)
+
+    # Canonicalize(), though it claims to, does not encode unicode
+    # nodes to UTF-8 properly and throws an exception unless you write
+    # the stream to a file object, so just encode it ourselves.
+    buf = StringIO()
+    Canonicalize(dom, output = buf)
+    xml = buf.getvalue().encode('utf-8')
+
+    return xml
+
+def gpg_export(keyring, armor = True):
+    """
+    Exports the specified public keyring file.
+    """
+
+    homedir = mkdtemp()
+    args = ["gpg", "--batch", "--no-tty",
+            "--homedir", homedir,
+            "--no-default-keyring",
+            "--keyring", keyring,
+            "--export"]
+    if armor:
+        args.append("--armor")
+
+    p = Popen(args, stdin = PIPE, stdout = PIPE, stderr = PIPE, close_fds = True)
+    export = p.stdout.read()
+    err = p.stderr.read()
+    rc = p.wait()
+
+    # Clean up
+    shutil.rmtree(homedir)
+
+    if rc:
+        raise PLCAuthenticationFailure, "GPG export failed with return code %d: %s" % (rc, err)
+
+    return export
+
+def gpg_sign(args, secret_keyring, keyring, methodname = None, methodresponse = False, detach_sign = True):
+    """
+    Signs the specified method call (methodname != None) or response
+    (methodresponse == True) using the specified GPG keyring files. If
+    args is not a tuple representing the arguments to the method call
+    or the method response value, then it should be a string
+    representing a generic message to sign (detach_sign == True) or
+    sign/encrypt (detach_sign == False) specified). Returns the
+    detached signature (detach_sign == True) or signed/encrypted
+    message (detach_sign == False).
+    """
+
+    # Accept either an opaque string blob or a Python tuple
+    if isinstance(args, StringTypes):
+        message = args
+    elif isinstance(args, tuple):
+        message = canonicalize(args, methodname, methodresponse)
+
+    # Use temporary trustdb
+    homedir = mkdtemp()
+
+    cmd = ["gpg", "--batch", "--no-tty",
+           "--homedir", homedir,
+           "--no-default-keyring",
+           "--secret-keyring", secret_keyring,
+           "--keyring", keyring,
+           "--armor"]
+
+    if detach_sign:
+        cmd.append("--detach-sign")
+    else:
+        cmd.append("--sign")
+
+    p = Popen(cmd, stdin = PIPE, stdout = PIPE, stderr = PIPE)
+    p.stdin.write(message)
+    p.stdin.close()
+    signature = p.stdout.read()
+    err = p.stderr.read()
+    rc = p.wait()
+
+    # Clean up
+    shutil.rmtree(homedir)
+
+    if rc:
+        raise PLCAuthenticationFailure, "GPG signing failed with return code %d: %s" % (rc, err)
+
+    return signature
+
+def gpg_verify(args, key, signature = None, methodname = None, methodresponse = False):
+    """
+    Verifies the signature of the specified method call (methodname !=
+    None) or response (methodresponse = True) using the specified
+    public key material. If args is not a tuple representing the
+    arguments to the method call or the method response value, then it
+    should be a string representing a generic message to verify (if
+    signature is specified) or verify/decrypt (if signature is not
+    specified).
+    """
+
+    # Accept either an opaque string blob or a Python tuple
+    if isinstance(args, StringTypes):
+        message = args
+    else:
+        message = canonicalize(args, methodname, methodresponse)
+
+    # Write public key to temporary file
+    if os.path.exists(key):
+        keyfile = None
+        keyfilename = key
+    else:
+        keyfile = NamedTemporaryFile(suffix = '.pub')
+        keyfile.write(key)
+        keyfile.flush()
+        keyfilename = keyfile.name
+
+    # Import public key into temporary keyring
+    homedir = mkdtemp()
+    call(["gpg", "--batch", "--no-tty", "--homedir", homedir, "--import", keyfilename],
+         stdin = PIPE, stdout = PIPE, stderr = PIPE)
+
+    cmd = ["gpg", "--batch", "--no-tty",
+           "--homedir", homedir]
+
+    if signature is not None:
+        # Write detached signature to temporary file
+        sigfile = NamedTemporaryFile()
+        sigfile.write(signature)
+        sigfile.flush()
+        cmd += ["--verify", sigfile.name, "-"]
+    else:
+        # Implicit signature
+        sigfile = None
+        cmd.append("--decrypt")
+
+    p = Popen(cmd, stdin = PIPE, stdout = PIPE, stderr = PIPE)
+    p.stdin.write(message)
+    p.stdin.close()
+    if signature is None:
+        message = p.stdout.read()
+    err = p.stderr.read()
+    rc = p.wait()
+
+    # Clean up
+    shutil.rmtree(homedir)
+    if sigfile:
+        sigfile.close()
+    if keyfile:
+        keyfile.close()
+
+    if rc:
+        raise PLCAuthenticationFailure, "GPG verification failed with return code %d: %s" % (rc, err)
+
+    return message
diff --git a/PLC/InitScripts.py b/PLC/InitScripts.py
new file mode 100644 (file)
index 0000000..9f864d2
--- /dev/null
@@ -0,0 +1,66 @@
+#
+# Functions for interacting with the initscripts table in the database
+#
+# Tony Mack <tmack@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+#
+
+from types import StringTypes
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+
+class InitScript(Row):
+    """
+    Representation of a row in the initscripts table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'initscripts'
+    primary_key = 'initscript_id'
+    join_tables = []
+    fields = {
+        'initscript_id': Parameter(int, "Initscript identifier"),
+        'name': Parameter(str, "Initscript name", max = 254),
+        'enabled': Parameter(bool, "Initscript is active"),
+        'script': Parameter(str, "Initscript"),
+        }
+
+    def validate_name(self, name):
+       """ 
+       validates the script name 
+       """
+       
+       conflicts = InitScripts(self.api, [name])
+       for initscript in conflicts:
+           if 'initscript_id' not in self or self['initscript_id'] != initscript['initscript_id']:
+               raise PLCInvalidArgument, "Initscript name already in use"
+
+       return name
+
+
+class InitScripts(Table):
+    """
+    Representation of the initscipts table in the database.
+    """
+
+    def __init__(self, api, initscript_filter = None, columns = None):
+       Table.__init__(self, api, InitScript, columns)
+
+        sql = "SELECT %s FROM initscripts WHERE True" % \
+              ", ".join(self.columns)
+
+        if initscript_filter is not None:
+            if isinstance(initscript_filter, (list, tuple, set)):
+               # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), initscript_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), initscript_filter)
+                initscript_filter = Filter(InitScript.fields, {'initscript_id': ints, 'name': strs })
+               sql += " AND (%s) %s" % initscript_filter.sql(api, "OR")
+            elif isinstance(initscript_filter, dict):
+                initscript_filter = Filter(InitScript.fields, initscript_filter)
+               sql += " AND (%s) %s" % initscript_filter.sql(api, "AND")
+
+        self.selectall(sql)
diff --git a/PLC/KeyTypes.py b/PLC/KeyTypes.py
new file mode 100644 (file)
index 0000000..920662b
--- /dev/null
@@ -0,0 +1,53 @@
+#
+# Functions for interacting with the key_types table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: KeyTypes.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+
+class KeyType(Row):
+    """
+    Representation of a row in the key_types table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'key_types'
+    primary_key = 'key_type'
+    join_tables = ['keys']
+    fields = {
+        'key_type': Parameter(str, "Key type", max = 20),
+        }
+
+    def validate_key_type(self, name):
+       # Make sure name is not blank
+        if not len(name):
+            raise PLCInvalidArgument, "Key type must be specified"
+       
+       # Make sure key type does not alredy exist
+       conflicts = KeyTypes(self.api, [name])
+        if conflicts:
+            raise PLCInvalidArgument, "Key type name already in use"
+
+       return name
+        
+class KeyTypes(Table):
+    """
+    Representation of the key_types table in the database.
+    """
+
+    def __init__(self, api, key_types = None):
+        Table.__init__(self, api, KeyType)
+
+        sql = "SELECT %s FROM key_types" % \
+              ", ".join(KeyType.fields)
+        
+        if key_types:
+            sql += " WHERE key_type IN (%s)" % ", ".join(map(api.db.quote, key_types))
+
+        self.selectall(sql)
diff --git a/PLC/Keys.py b/PLC/Keys.py
new file mode 100644 (file)
index 0000000..8d22dc2
--- /dev/null
@@ -0,0 +1,122 @@
+import re
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.KeyTypes import KeyType, KeyTypes
+
+class Key(Row):
+    """
+    Representation of a row in the keys table. To use, instantiate with a 
+    dict of values. Update as you would a dict. Commit to the database 
+    with sync().
+    """
+
+    table_name = 'keys'
+    primary_key = 'key_id'
+    join_tables = ['person_key', 'peer_key']
+    fields = {
+        'key_id': Parameter(int, "Key identifier"),
+        'key_type': Parameter(str, "Key type"),
+        'key': Parameter(str, "Key value", max = 4096),
+        'person_id': Parameter(int, "User to which this key belongs", nullok = True),
+        'peer_id': Parameter(int, "Peer to which this key belongs", nullok = True),
+        'peer_key_id': Parameter(int, "Foreign key identifier at peer", nullok = True),
+        }
+
+    # for Cache
+    class_key= 'key'
+    foreign_fields = ['key_type']
+    foreign_xrefs = []
+
+    def validate_key_type(self, key_type):
+        key_types = [row['key_type'] for row in KeyTypes(self.api)]
+        if key_type not in key_types:
+            raise PLCInvalidArgument, "Invalid key type"
+       return key_type
+
+    def validate_key(self, key):
+       # Key must not be blacklisted
+       rows = self.api.db.selectall("SELECT 1 from keys" \
+                                    " WHERE key = %(key)s" \
+                                     " AND is_blacklisted IS True",
+                                     locals())
+       if rows:
+            raise PLCInvalidArgument, "Key is blacklisted and cannot be used"
+
+       return key
+
+    def validate(self):
+        # Basic validation
+        Row.validate(self)
+
+        assert 'key' in self
+        key = self['key']
+
+        if self['key_type'] == 'ssh':
+            # Accept only SSH version 2 keys without options. From
+            # sshd(8):
+            #
+            # Each protocol version 2 public key consists of: options,
+            # keytype, base64 encoded key, comment.  The options field
+            # is optional...The comment field is not used for anything
+            # (but may be convenient for the user to identify the
+            # key). For protocol version 2 the keytype is ``ssh-dss''
+            # or ``ssh-rsa''.
+
+            good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
+            if not re.match(good_ssh_key, key, re.IGNORECASE):
+                raise PLCInvalidArgument, "Invalid SSH version 2 public key"
+
+    def blacklist(self, commit = True):
+        """
+       Permanently blacklist key (and all other identical keys),
+       preventing it from ever being added again. Because this could
+       affect multiple keys associated with multiple accounts, it
+       should be admin only.        
+       """
+
+       assert 'key_id' in self
+        assert 'key' in self
+
+        # Get all matching keys
+        rows = self.api.db.selectall("SELECT key_id FROM keys WHERE key = %(key)s",
+                                     self)
+        key_ids = [row['key_id'] for row in rows]
+        assert key_ids
+        assert self['key_id'] in key_ids
+
+        # Keep the keys in the table
+        self.api.db.do("UPDATE keys SET is_blacklisted = True" \
+                       " WHERE key_id IN (%s)" % ", ".join(map(str, key_ids)))
+
+       # But disassociate them from all join tables
+        for table in self.join_tables:
+            self.api.db.do("DELETE FROM %s WHERE key_id IN (%s)" % \
+                           (table, ", ".join(map(str, key_ids))))
+
+        if commit:
+            self.api.db.commit()
+
+class Keys(Table):
+    """
+    Representation of row(s) from the keys table in the
+    database.
+    """
+
+    def __init__(self, api, key_filter = None, columns = None):
+        Table.__init__(self, api, Key, columns)
+       
+       sql = "SELECT %s FROM view_keys WHERE is_blacklisted IS False" % \
+              ", ".join(self.columns)
+
+        if key_filter is not None:
+            if isinstance(key_filter, (list, tuple, set)):
+                key_filter = Filter(Key.fields, {'key_id': key_filter})
+            elif isinstance(key_filter, dict):
+                key_filter = Filter(Key.fields, key_filter)
+            sql += " AND (%s) %s" % key_filter.sql(api)
+
+       self.selectall(sql)
diff --git a/PLC/Messages.py b/PLC/Messages.py
new file mode 100644 (file)
index 0000000..0620ac0
--- /dev/null
@@ -0,0 +1,50 @@
+#
+# Functions for interacting with the messages table in the database
+#
+# Tony Mack <tmack@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Messages.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+from PLC.Filter import Filter
+
+class Message(Row):
+    """
+    Representation of a row in the messages table. 
+    """
+    
+    table_name = 'messages'
+    primary_key = 'message_id'
+    fields = {
+        'message_id': Parameter(str, "Message identifier"),
+        'subject': Parameter(str, "Message summary", nullok = True),
+        'template': Parameter(str, "Message template", nullok = True),
+        'enabled': Parameter(bool, "Message is enabled"),
+        }
+    
+class Messages(Table):
+    """
+    Representation of row(s) from the messages table in the database. 
+    """
+
+    def __init__(self, api, message_filter = None, columns = None, enabled = None):
+        Table.__init__(self, api, Message, columns)
+    
+        sql = "SELECT %s from messages WHERE True" % \
+              ", ".join(self.columns)
+
+        if enabled is not None:
+            sql += " AND enabled IS %s" % enabled
+
+        if message_filter is not None:
+            if isinstance(message_filter, (list, tuple, set)):
+                message_filter = Filter(Message.fields, {'message_id': message_filter})
+                sql += " AND (%s) %s" % message_filter.sql(api, "OR")
+            elif isinstance(message_filter, dict):
+                message_filter = Filter(Message.fields, message_filter)
+                sql += " AND (%s) %s" % message_filter.sql(api, "AND")
+
+        self.selectall(sql)
diff --git a/PLC/Method.py b/PLC/Method.py
new file mode 100644 (file)
index 0000000..5e7d09a
--- /dev/null
@@ -0,0 +1,372 @@
+#
+# Base class for all PLCAPI functions
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Method.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+import xmlrpclib
+from types import *
+import textwrap
+import os
+import time
+import pprint
+
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed, python_type, xmlrpc_type
+from PLC.Auth import Auth
+from PLC.Debug import profile, log
+from PLC.Events import Event, Events
+from PLC.Nodes import Node, Nodes
+from PLC.Persons import Person, Persons
+
+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, *args):
+        """
+        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, **kwds):
+        """
+        Main entry point for all PLCAPI functions. Type checks
+        arguments, authenticates, and executes call().
+        """
+
+        try:
+           start = time.time()
+            (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, args)
+       
+           result = self.call(*args, **kwds)
+           runtime = time.time() - start
+
+            if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
+               self.log(None, runtime, *args)
+               
+           return result
+
+        except PLCFault, fault:
+       
+           caller = ""
+           if isinstance(self.caller, Person):
+               caller = 'person_id %s'  % self.caller['person_id']
+            elif isinstance(self.caller, Node):
+                caller = 'node_id %s'  % self.caller['node_id']
+
+            # Prepend caller and method name to expected faults
+            fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
+           runtime = time.time() - start
+           self.log(fault, runtime, *args)
+            raise fault
+
+    def log(self, fault, runtime, *args):
+        """
+        Log the transaction 
+        """    
+
+       # Do not log system or Get calls
+        #if self.name.startswith('system') or self.name.startswith('Get'):
+        #    return False
+
+        # Create a new event
+        event = Event(self.api)
+       event['fault_code'] = 0
+       if fault:
+            event['fault_code'] = fault.faultCode
+        event['runtime'] = runtime
+
+        # Redact passwords and sessions
+        if args and isinstance(args[0], dict):
+           # what type of auth this is
+           if args[0].has_key('AuthMethod'):
+               auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
+               auth_method = args[0]['AuthMethod']
+               if auth_method in auth_methods:
+                   event['auth_type'] = auth_method
+            for password in 'AuthString', 'session':
+                if args[0].has_key(password):
+                    auth = args[0].copy()
+                    auth[password] = "Removed by API"
+                    args = (auth,) + args[1:]
+
+        # Log call representation
+        # XXX Truncate to avoid DoS
+        event['call'] = self.name + pprint.saferepr(args)
+       event['call_name'] = self.name
+
+        # Both users and nodes can call some methods
+        if isinstance(self.caller, Person):
+            event['person_id'] = self.caller['person_id']
+        elif isinstance(self.caller, Node):
+            event['node_id'] = self.caller['node_id']
+
+        event.sync(commit = False)
+
+        if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
+            for key in self.event_objects.keys():
+               for object_id in self.event_objects[key]:
+                    event.add_object(key, object_id, commit = False)
+       
+
+       # Set the message for this event
+       if fault:
+           event['message'] = fault.faultString
+       elif hasattr(self, 'message'):
+            event['message'] = self.message    
+       
+        # Commit
+        event.sync()
+
+    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, set)):
+                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, args):
+        """
+        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, args)
+                    return
+                except PLCInvalidArgument, fault:
+                    pass
+            raise fault
+
+        # If an authentication structure is expected, save it and
+        # authenticate after basic type checking is done.
+        if isinstance(expected, Auth):
+            auth = expected
+        else:
+            auth = None
+
+        # Get actual expected type from within the Parameter structure
+        if isinstance(expected, Parameter):
+            min = expected.min
+            max = expected.max
+            nullok = expected.nullok
+            expected = expected.type
+        else:
+            min = None
+            max = None
+            nullok = False
+
+        expected_type = python_type(expected)
+
+        # If value can be NULL
+        if value is None and nullok:
+            return
+
+        # 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 minimum or maximum (length, value) has been specified
+        if expected_type in StringTypes:
+            if min is not None and \
+               len(value.encode(self.api.encoding)) < min:
+                raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
+            if max is not None and \
+               len(value.encode(self.api.encoding)) > max:
+                raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
+        elif expected_type in (list, tuple, set):
+            if min is not None and len(value) < min:
+                raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
+            if max is not None and len(value) > max:
+                raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
+        else:
+            if min is not None and value < min:
+                raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
+            if max is not None and value > max:
+                raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
+
+        # If a list with particular types of items is expected
+        if isinstance(expected, (list, tuple, set)):
+            for i in range(len(value)):
+                if i >= len(expected):
+                    j = len(expected) - 1
+                else:
+                    j = i
+                self.type_check(name + "[]", value[i], expected[j], args)
+
+        # 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], args)
+            for key, subparam in expected.iteritems():
+                if isinstance(subparam, Parameter) and \
+                   subparam.optional is not None and \
+                   not subparam.optional and key not in value.keys():
+                    raise PLCInvalidArgument("'%s' not specified" % key, name)
+
+        if auth is not None:
+            auth.check(self, *args)
diff --git a/PLC/Methods/.cvsignore b/PLC/Methods/.cvsignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
diff --git a/PLC/Methods/AddAddressType.py b/PLC/Methods/AddAddressType.py
new file mode 100644 (file)
index 0000000..9fc771b
--- /dev/null
@@ -0,0 +1,36 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.AddressTypes import AddressType, AddressTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in ['address_type_id']
+
+class AddAddressType(Method):
+    """
+    Adds a new address type. Fields specified in address_type_fields
+    are used.
+
+    Returns the new address_type_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    address_type_fields = dict(filter(can_update, AddressType.fields.items()))
+
+    accepts = [
+        Auth(),
+        address_type_fields
+        ]
+
+    returns = Parameter(int, 'New address_type_id (> 0) if successful')
+        
+
+    def call(self, auth, address_type_fields):
+        address_type_fields = dict(filter(can_update, address_type_fields.items()))
+        address_type = AddressType(self.api, address_type_fields)
+        address_type.sync()
+
+       self.event_objects = {'AddressType' : [address_type['address_type_id']]}
+        
+       return address_type['address_type_id']
diff --git a/PLC/Methods/AddAddressTypeToAddress.py b/PLC/Methods/AddAddressTypeToAddress.py
new file mode 100644 (file)
index 0000000..d69e627
--- /dev/null
@@ -0,0 +1,47 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.AddressTypes import AddressType, AddressTypes
+from PLC.Addresses import Address, Addresses
+from PLC.Auth import Auth
+
+class AddAddressTypeToAddress(Method):
+    """
+    Adds an address type to the specified address.
+
+    PIs may only update addresses of their own sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Mixed(AddressType.fields['address_type_id'],
+              AddressType.fields['name']),
+        Address.fields['address_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, address_type_id_or_name, address_id):
+       address_types = AddressTypes(self.api, [address_type_id_or_name])
+        if not address_types:
+            raise PLCInvalidArgument, "No such address type"
+        address_type = address_types[0]
+
+        addresses = Addresses(self.api, [address_id])
+        if not addresses:
+            raise PLCInvalidArgument, "No such address"
+        address = addresses[0]
+
+        if 'admin' not in self.caller['roles']:
+            if address['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Address must be associated with one of your sites"
+
+        address.add_address_type(address_type)
+       self.event_objects = {'Address': [address['address_id']]}
+
+        return 1
diff --git a/PLC/Methods/AddBootState.py b/PLC/Methods/AddBootState.py
new file mode 100644 (file)
index 0000000..fc75254
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.BootStates import BootState, BootStates
+from PLC.Auth import Auth
+
+class AddBootState(Method):
+    """
+    Adds a new node boot state.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        BootState.fields['boot_state']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    
+    def call(self, auth, name):
+        boot_state = BootState(self.api)
+        boot_state['boot_state'] = name
+        boot_state.sync(insert = True)
+
+        return 1
diff --git a/PLC/Methods/AddConfFile.py b/PLC/Methods/AddConfFile.py
new file mode 100644 (file)
index 0000000..5604cef
--- /dev/null
@@ -0,0 +1,37 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in \
+             ['conf_file_id', 'node_ids', 'nodegroup_ids']
+
+class AddConfFile(Method):
+    """
+    Adds a new node configuration file. Any fields specified in
+    conf_file_fields are used, otherwise defaults are used.
+
+    Returns the new conf_file_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    conf_file_fields = dict(filter(can_update, ConfFile.fields.items()))
+
+    accepts = [
+        Auth(),
+        conf_file_fields
+        ]
+
+    returns = Parameter(int, 'New conf_file_id (> 0) if successful')
+
+
+    def call(self, auth, conf_file_fields):
+        conf_file_fields = dict(filter(can_update, conf_file_fields.items()))
+        conf_file = ConfFile(self.api, conf_file_fields)
+        conf_file.sync()
+
+       self.event_objects = {'ConfFile': [conf_file['conf_file_id']]}
+
+        return conf_file['conf_file_id']
diff --git a/PLC/Methods/AddConfFileToNode.py b/PLC/Methods/AddConfFileToNode.py
new file mode 100644 (file)
index 0000000..2d1542c
--- /dev/null
@@ -0,0 +1,51 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Nodes import Node, Nodes
+from PLC.Auth import Auth
+
+class AddConfFileToNode(Method):
+    """
+    Adds a configuration file to the specified node. If the node is
+    already linked to the configuration file, no errors are returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        ConfFile.fields['conf_file_id'],
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, conf_file_id, node_id_or_hostname):
+       # Get configuration file
+        conf_files = ConfFiles(self.api, [conf_file_id])
+        if not conf_files:
+            raise PLCInvalidArgument, "No such configuration file"
+        conf_file = conf_files[0]
+
+        # Get node
+       nodes = Nodes(self.api, [node_id_or_hostname])
+       if not nodes:
+               raise PLCInvalidArgument, "No such node"
+       node = nodes[0]
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+       
+       # Link configuration file to node
+        if node['node_id'] not in conf_file['node_ids']:
+            conf_file.add_node(node)
+
+        # Log affected objects
+        self.event_objects = {'ConfFile': [conf_file_id], 
+                             'Node': [node['node_id']] }
+
+        return 1
diff --git a/PLC/Methods/AddConfFileToNodeGroup.py b/PLC/Methods/AddConfFileToNodeGroup.py
new file mode 100644 (file)
index 0000000..6ff642c
--- /dev/null
@@ -0,0 +1,50 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Auth import Auth
+
+class AddConfFileToNodeGroup(Method):
+    """
+    Adds a configuration file to the specified node group. If the node
+    group is already linked to the configuration file, no errors are
+    returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        ConfFile.fields['conf_file_id'],
+        Mixed(NodeGroup.fields['nodegroup_id'],
+              NodeGroup.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, conf_file_id, nodegroup_id_or_name):
+       # Get configuration file
+        conf_files = ConfFiles(self.api, [conf_file_id])
+        if not conf_files:
+            raise PLCInvalidArgument, "No such configuration file"
+        conf_file = conf_files[0]
+
+        # Get node
+       nodegroups = NodeGroups(self.api, [nodegroup_id_or_name])
+       if not nodegroups:
+            raise PLCInvalidArgument, "No such node group"
+       nodegroup = nodegroups[0]
+       
+       # Link configuration file to node
+        if nodegroup['nodegroup_id'] not in conf_file['nodegroup_ids']:
+            conf_file.add_nodegroup(nodegroup)
+
+        # Log affected objects
+        self.event_objects = {'ConfFile': [conf_file_id], 
+                             'NodeGroup': [nodegroup['nodegroup_id']] }
+
+        return 1
diff --git a/PLC/Methods/AddInitScript.py b/PLC/Methods/AddInitScript.py
new file mode 100644 (file)
index 0000000..8c247cb
--- /dev/null
@@ -0,0 +1,37 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.InitScripts import InitScript, InitScripts
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in \
+             ['initscript_id']
+
+class AddInitScript(Method):
+    """
+    Adds a new initscript. Any fields specified in initscript_fields
+    are used, otherwise defaults are used.
+
+    Returns the new initscript_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    initscript_fields = dict(filter(can_update, InitScript.fields.items()))
+
+    accepts = [
+        Auth(),
+        initscript_fields
+        ]
+
+    returns = Parameter(int, 'New initscript_id (> 0) if successful')
+
+
+    def call(self, auth, initscript_fields):
+        initscript_fields = dict(filter(can_update, initscript_fields.items()))
+        initscript = InitScript(self.api, initscript_fields)
+        initscript.sync()
+
+       self.event_objects = {'InitScript': [initscript['initscript_id']]}
+
+        return initscript['initscript_id']
diff --git a/PLC/Methods/AddKeyType.py b/PLC/Methods/AddKeyType.py
new file mode 100644 (file)
index 0000000..b3690a8
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.KeyTypes import KeyType, KeyTypes
+from PLC.Auth import Auth
+
+class AddKeyType(Method):
+    """
+    Adds a new key type.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        KeyType.fields['key_type']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, name):
+        key_type = KeyType(self.api)
+        key_type['key_type'] = name
+        key_type.sync(insert = True)
+
+        return 1
diff --git a/PLC/Methods/AddMessage.py b/PLC/Methods/AddMessage.py
new file mode 100644 (file)
index 0000000..62a2da7
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter
+from PLC.Messages import Message, Messages
+from PLC.Auth import Auth
+
+class AddMessage(Method):
+    """
+    Adds a new message template. Any values specified in
+    message_fields are used, otherwise defaults are used.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Message.fields,
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, message_fields):
+        message = Message(self.api, message_fields)
+        message.sync(insert = True)
+
+        return 1
diff --git a/PLC/Methods/AddNetworkMethod.py b/PLC/Methods/AddNetworkMethod.py
new file mode 100644 (file)
index 0000000..11f3845
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NetworkMethods import NetworkMethod, NetworkMethods
+from PLC.Auth import Auth
+
+class AddNetworkMethod(Method):
+    """
+    Adds a new network method.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        NetworkMethod.fields['method']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, name):
+        network_method = NetworkMethod(self.api)
+        network_method['method'] = name
+        network_method.sync(insert = True)
+
+        return 1
diff --git a/PLC/Methods/AddNetworkType.py b/PLC/Methods/AddNetworkType.py
new file mode 100644 (file)
index 0000000..6533053
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NetworkTypes import NetworkType, NetworkTypes
+from PLC.Auth import Auth
+
+class AddNetworkType(Method):
+    """
+    Adds a new network type.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        NetworkType.fields['type']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, name):
+        network_type = NetworkType(self.api)
+        network_type['type'] = name
+        network_type.sync(insert = True)
+
+        return 1
diff --git a/PLC/Methods/AddNode.py b/PLC/Methods/AddNode.py
new file mode 100644 (file)
index 0000000..8d253df
--- /dev/null
@@ -0,0 +1,66 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Sites import Site, Sites
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['hostname', 'boot_state', 'model', 'version']
+
+class AddNode(Method):
+    """
+    Adds a new node. Any values specified in node_fields 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']
+
+    node_fields = dict(filter(can_update, Node.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base']),
+        node_fields
+        ]
+
+    returns = Parameter(int, 'New node_id (> 0) if successful')
+
+    def call(self, auth, site_id_or_login_base, node_fields):
+        node_fields = dict(filter(can_update, node_fields.items()))
+
+        # Get site information
+        sites = Sites(self.api, [site_id_or_login_base])
+        if not sites:
+            raise PLCInvalidArgument, "No such site"
+
+        site = sites[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, node_fields)
+        node['site_id'] = site['site_id']
+        node.sync()
+
+       self.event_objects = {'Site': [site['site_id']],
+                            'Node': [node['node_id']]} 
+       self.message = "Node %s created" % node['node_id']
+
+        return node['node_id']
diff --git a/PLC/Methods/AddNodeGroup.py b/PLC/Methods/AddNodeGroup.py
new file mode 100644 (file)
index 0000000..34f5f97
--- /dev/null
@@ -0,0 +1,39 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['name', 'description']
+
+class AddNodeGroup(Method):
+    """
+    Adds a new node group. Any values specified in nodegroup_fields
+    are used, otherwise defaults are used.
+
+    Returns the new nodegroup_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    nodegroup_fields = dict(filter(can_update, NodeGroup.fields.items()))
+
+    accepts = [
+        Auth(),
+        nodegroup_fields
+        ]
+
+    returns = Parameter(int, 'New nodegroup_id (> 0) if successful')
+
+
+    def call(self, auth, nodegroup_fields):
+        nodegroup_fields = dict(filter(can_update, nodegroup_fields.items()))
+        nodegroup = NodeGroup(self.api, nodegroup_fields)
+        nodegroup.sync()
+
+       # Logging variables
+       self.event_objects = {'NodeGroup': [nodegroup['nodegroup_id']]}
+       self.message = 'Node group %d created' % nodegroup['nodegroup_id']
+        return nodegroup['nodegroup_id']
diff --git a/PLC/Methods/AddNodeNetwork.py b/PLC/Methods/AddNodeNetwork.py
new file mode 100644 (file)
index 0000000..6e24bce
--- /dev/null
@@ -0,0 +1,73 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in ['nodenetwork_id', 'node_id']
+
+class AddNodeNetwork(Method):
+    """
+
+    Adds a new network for a node. Any values specified in
+    nodenetwork_fields are used, otherwise defaults are
+    used. Acceptable values for method may be retrieved via
+    GetNetworkMethods. Acceptable values for type may be retrieved via
+    GetNetworkTypes.
+
+    If type is static, ip, gateway, network, broadcast, netmask, and
+    dns1 must all be specified in nodenetwork_fields. If type is dhcp,
+    these parameters, even if specified, are ignored.
+
+    PIs and techs may only add networks to their own nodes. Admins may
+    add networks to any node.
+
+    Returns the new nodenetwork_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    nodenetwork_fields = dict(filter(can_update, NodeNetwork.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+        nodenetwork_fields
+        ]
+
+    returns = Parameter(int, 'New nodenetwork_id (> 0) if successful')
+
+    
+    def call(self, auth, node_id_or_hostname, nodenetwork_fields):
+        nodenetwork_fields = dict(filter(can_update, nodenetwork_fields.items()))
+
+        # Check if node exists
+        nodes = Nodes(self.api, [node_id_or_hostname])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node"
+       node = nodes[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 where the node exists.
+        if 'admin' not in self.caller['roles']:
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to add node network for specified node"
+
+        # Add node network
+       nodenetwork = NodeNetwork(self.api, nodenetwork_fields)
+        nodenetwork['node_id'] = node['node_id']
+       # if this is the first node network, make it primary
+       if not node['nodenetwork_ids']:
+               nodenetwork['is_primary'] = True
+        nodenetwork.sync()
+       
+       # Logging variables
+       self.object_ids = [node['node_id'], nodenetwork['nodenetwork_id']]      
+       self.messgage = "Node network %d added" % nodenetwork['nodenetwork_id']
+
+        return nodenetwork['nodenetwork_id']
diff --git a/PLC/Methods/AddNodeNetworkSetting.py b/PLC/Methods/AddNodeNetworkSetting.py
new file mode 100644 (file)
index 0000000..f02670d
--- /dev/null
@@ -0,0 +1,89 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.NodeNetworkSettingTypes import NodeNetworkSettingType, NodeNetworkSettingTypes
+from PLC.NodeNetworkSettings import NodeNetworkSetting, NodeNetworkSettings
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+
+from PLC.Nodes import Nodes
+from PLC.Sites import Sites
+
+class AddNodeNetworkSetting(Method):
+    """
+    Sets the specified setting for the specified nodenetwork
+    to the specified value.
+
+    In general only tech(s), PI(s) and of course admin(s) are allowed to
+    do the change, but this is defined in the nodenetwork setting type object.
+
+    Returns the new nodenetwork_setting_id (> 0) if successful, faults
+    otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    accepts = [
+        Auth(),
+        # no other way to refer to a nodenetwork
+        NodeNetworkSetting.fields['nodenetwork_id'],
+        Mixed(NodeNetworkSettingType.fields['nodenetwork_setting_type_id'],
+              NodeNetworkSettingType.fields['name']),
+        NodeNetworkSetting.fields['value'],
+        ]
+
+    returns = Parameter(int, 'New nodenetwork_setting_id (> 0) if successful')
+
+    object_type = 'NodeNetwork'
+
+
+    def call(self, auth, nodenetwork_id, nodenetwork_setting_type_id_or_name, value):
+        nodenetworks = NodeNetworks(self.api, [nodenetwork_id])
+        if not nodenetworks:
+            raise PLCInvalidArgument, "No such nodenetwork %r"%nodenetwork_id
+        nodenetwork = nodenetworks[0]
+
+        nodenetwork_setting_types = NodeNetworkSettingTypes(self.api, [nodenetwork_setting_type_id_or_name])
+        if not nodenetwork_setting_types:
+            raise PLCInvalidArgument, "No such nodenetwork setting type %r"%nodenetwork_setting_type_id_or_name
+        nodenetwork_setting_type = nodenetwork_setting_types[0]
+
+       # checks for existence - does not allow several different settings
+        conflicts = NodeNetworkSettings(self.api,
+                                        {'nodenetwork_id':nodenetwork['nodenetwork_id'],
+                                         'nodenetwork_setting_type_id':nodenetwork_setting_type['nodenetwork_setting_type_id']})
+
+        if len(conflicts) :
+            raise PLCInvalidArgument, "Nodenetwork %d already has setting %d"%(nodenetwork['nodenetwork_id'],
+                                                                               nodenetwork_setting_type['nodenetwork_setting_type_id'])
+
+       # check permission : it not admin, is the user affiliated with the right site
+       if 'admin' not in self.caller['roles']:
+           # locate node
+           node = Nodes (self.api,[nodenetwork['node_id']])[0]
+           # locate site
+           site = Sites (self.api, [node['site_id']])[0]
+           # check caller is affiliated with this site
+           if self.caller['person_id'] not in site['person_ids']:
+               raise PLCPermissionDenied, "Not a member of the hosting site %s"%site['abbreviated_site']
+           
+           required_min_role = nodenetwork_setting_type ['min_role_id']
+           if required_min_role is not None and \
+                   min(self.caller['role_ids']) > required_min_role:
+               raise PLCPermissionDenied, "Not allowed to modify the specified nodenetwork setting, requires role %d",required_min_role
+
+        nodenetwork_setting = NodeNetworkSetting(self.api)
+        nodenetwork_setting['nodenetwork_id'] = nodenetwork['nodenetwork_id']
+        nodenetwork_setting['nodenetwork_setting_type_id'] = nodenetwork_setting_type['nodenetwork_setting_type_id']
+        nodenetwork_setting['value'] = value
+
+        nodenetwork_setting.sync()
+       self.object_ids = [nodenetwork_setting['nodenetwork_setting_id']]
+
+        return nodenetwork_setting['nodenetwork_setting_id']
diff --git a/PLC/Methods/AddNodeNetworkSettingType.py b/PLC/Methods/AddNodeNetworkSettingType.py
new file mode 100644 (file)
index 0000000..1c3cc2c
--- /dev/null
@@ -0,0 +1,45 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeNetworkSettingTypes import NodeNetworkSettingType, NodeNetworkSettingTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['name', 'description', 'category', 'min_role_id']
+
+class AddNodeNetworkSettingType(Method):
+    """
+    Adds a new type of nodenetwork setting.
+    Any fields specified are used, otherwise defaults are used.
+
+    Returns the new nodenetwork_setting_id (> 0) if successful,
+    faults otherwise.
+    """
+
+    roles = ['admin']
+
+    nodenetwork_setting_type_fields = dict(filter(can_update, NodeNetworkSettingType.fields.items()))
+
+    accepts = [
+        Auth(),
+        nodenetwork_setting_type_fields
+        ]
+
+    returns = Parameter(int, 'New nodenetwork_setting_id (> 0) if successful')
+
+
+    def call(self, auth, nodenetwork_setting_type_fields):
+        nodenetwork_setting_type_fields = dict(filter(can_update, nodenetwork_setting_type_fields.items()))
+        nodenetwork_setting_type = NodeNetworkSettingType(self.api, nodenetwork_setting_type_fields)
+        nodenetwork_setting_type.sync()
+
+       self.object_ids = [nodenetwork_setting_type['nodenetwork_setting_type_id']]
+
+        return nodenetwork_setting_type['nodenetwork_setting_type_id']
diff --git a/PLC/Methods/AddNodeToNodeGroup.py b/PLC/Methods/AddNodeToNodeGroup.py
new file mode 100644 (file)
index 0000000..a552b11
--- /dev/null
@@ -0,0 +1,55 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Nodes import Node, Nodes
+from PLC.Auth import Auth
+
+class AddNodeToNodeGroup(Method):
+    """
+    Add a node to the specified node group. If the node is
+    already a member of the nodegroup, no errors are returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+       Mixed(Node.fields['node_id'],
+             Node.fields['hostname']),
+        Mixed(NodeGroup.fields['nodegroup_id'],
+             NodeGroup.fields['name']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, node_id_or_hostname, nodegroup_id_or_name):
+        # Get node info
+       nodes = Nodes(self.api, [node_id_or_hostname])
+       if not nodes:
+               raise PLCInvalidArgument, "No such node"
+       node = nodes[0]
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+
+       # Get nodegroup info
+        nodegroups = NodeGroups(self.api, [nodegroup_id_or_name])
+        if not nodegroups:
+            raise PLCInvalidArgument, "No such nodegroup"
+
+        nodegroup = nodegroups[0]
+       
+       # add node to nodegroup
+        if node['node_id'] not in nodegroup['node_ids']:
+            nodegroup.add_node(node)
+       
+       # Logging variables
+       self.event_objects = {'NodeGroup': [nodegroup['nodegroup_id']],
+                             'Node': [node['node_id']]}
+       self.message = 'Node %d added to node group %d' % \
+               (node['node_id'], nodegroup['nodegroup_id'])
+        return 1
diff --git a/PLC/Methods/AddNodeToPCU.py b/PLC/Methods/AddNodeToPCU.py
new file mode 100644 (file)
index 0000000..c0d5eff
--- /dev/null
@@ -0,0 +1,74 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.PCUs import PCU, PCUs
+from PLC.Sites import Site, Sites
+from PLC.Auth import Auth
+
+class AddNodeToPCU(Method):
+    """
+    Adds a node to a port on a PCU. Faults if the node has already
+    been added to the PCU or if the port is already in use.
+
+    Non-admins may only update PCUs at their sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+       Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+        PCU.fields['pcu_id'],
+        Parameter(int, 'PCU port number')
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, node_id_or_hostname, pcu_id, port):
+        # Get node
+        nodes = Nodes(self.api, [node_id_or_hostname])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node"
+        node = nodes[0]
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+
+        # Get PCU
+        pcus = PCUs(self.api, [pcu_id])
+        if not pcus:
+            raise PLCInvalidArgument, "No such PCU"
+        pcu = pcus[0]
+
+        if 'admin' not in self.caller['roles']:
+            ok = False
+            sites = Sites(self.api, self.caller['site_ids'])
+            for site in sites:
+                if pcu['pcu_id'] in site['pcu_ids']:
+                    ok = True
+                    break
+            if not ok:
+                raise PLCPermissionDenied, "Not allowed to update that PCU"
+       
+       # Add node to PCU
+        if node['node_id'] in pcu['node_ids']:
+            raise PLCInvalidArgument, "Node already controlled by PCU"
+
+        if node['site_id'] != pcu['site_id']:
+            raise PLCInvalidArgument, "Node is at a different site than this PCU"
+
+        if port in pcu['ports']:
+            raise PLCInvalidArgument, "PCU port already in use"
+
+        pcu.add_node(node, port)
+
+       # Logging variables
+       self.event_objects = {'Node': [node['node_id']],
+                             'PCU': [pcu['pcu_id']]}
+       self.message = 'Node %d added to pcu %d on port %d' % \
+               (node['node_id'], pcu['pcu_id'], port)
+        return 1
diff --git a/PLC/Methods/AddPCU.py b/PLC/Methods/AddPCU.py
new file mode 100644 (file)
index 0000000..3c46194
--- /dev/null
@@ -0,0 +1,61 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUs import PCU, PCUs
+from PLC.Auth import Auth
+from PLC.Sites import Site, Sites
+
+can_update = lambda (field, value): field in \
+             ['ip', 'hostname', 'protocol',
+              'username', 'password',
+              'model', 'notes']
+
+class AddPCU(Method):
+    """
+    Adds a new power control unit (PCU) to the specified site. Any
+    fields specified in pcu_fields are used, otherwise defaults are
+    used.
+
+    PIs and technical contacts may only add PCUs to their own sites.
+
+    Returns the new pcu_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    pcu_fields = dict(filter(can_update, PCU.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base']),
+        pcu_fields
+        ]
+
+    returns = Parameter(int, 'New pcu_id (> 0) if successful')
+    
+
+    def call(self, auth, site_id_or_login_base, pcu_fields):
+        pcu_fields = dict(filter(can_update, pcu_fields.items()))
+
+        # Get associated site details
+        sites = Sites(self.api, [site_id_or_login_base])
+        if not sites:
+            raise PLCInvalidArgument, "No such site"
+        site = sites[0]
+
+        if 'admin' not in self.caller['roles']:
+            if site['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to add a PCU to that site"
+
+        pcu = PCU(self.api, pcu_fields)
+        pcu['site_id'] = site['site_id']
+        pcu.sync()
+
+       # Logging variables
+       self.event_objects = {'Site': [site['site_id']],
+                             'PCU': [pcu['pcu_id']]}
+       self.message = 'PCU %d added site %s' % \
+               (pcu['pcu_id'], site['site_id'])
+
+        return pcu['pcu_id']
diff --git a/PLC/Methods/AddPCUProtocolType.py b/PLC/Methods/AddPCUProtocolType.py
new file mode 100644 (file)
index 0000000..76dad3b
--- /dev/null
@@ -0,0 +1,55 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUProtocolTypes import PCUProtocolType, PCUProtocolTypes
+from PLC.PCUTypes import PCUType, PCUTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+            ['pcu_type_id', 'port', 'protocol', 'supported']   
+
+class AddPCUProtocolType(Method):
+    """
+    Adds a new pcu protocol type.
+
+    Returns the new pcu_protocol_type_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    protocol_type_fields = dict(filter(can_update, PCUProtocolType.fields.items()))
+
+    accepts = [
+        Auth(),
+       Mixed(PCUType.fields['pcu_type_id'],
+              PCUType.fields['model']),
+        protocol_type_fields
+       ]
+
+    returns = Parameter(int, 'New pcu_protocol_type_id (> 0) if successful')
+
+    def call(self, auth, pcu_type_id_or_model, protocol_type_fields):
+
+       # Check if pcu type exists
+       pcu_types = PCUTypes(self.api, [pcu_type_id_or_model])
+       if not pcu_types:
+           raise PLCInvalidArgument, "No such pcu type"
+       pcu_type = pcu_types[0]
+
+       
+       # Check if this port is already used
+       if 'port' not in protocol_type_fields:
+           raise PLCInvalidArgument, "Must specify a port"
+       else:
+           protocol_types = PCUProtocolTypes(self.api, {'pcu_type_id': pcu_type['pcu_type_id']})
+           for protocol_type in protocol_types:
+               if protocol_type['port'] == protocol_type_fields['port']:
+                   raise PLCInvalidArgument, "Port alreay in use" 
+
+       protocol_type_fields = dict(filter(can_update, protocol_type_fields.items()))
+        protocol_type = PCUProtocolType(self.api, protocol_type_fields)
+       protocol_type['pcu_type_id'] = pcu_type['pcu_type_id']
+       protocol_type.sync()
+       self.event_object = {'PCUProtocolType': [protocol_type['pcu_protocol_type_id']]}        
+
+        return protocol_type['pcu_protocol_type_id']
diff --git a/PLC/Methods/AddPCUType.py b/PLC/Methods/AddPCUType.py
new file mode 100644 (file)
index 0000000..106791a
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUTypes import PCUType, PCUTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+            ['model', 'name']  
+
+class AddPCUType(Method):
+    """
+    Adds a new pcu type.
+
+    Returns the new pcu_type_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    pcu_type_fields = dict(filter(can_update, PCUType.fields.items()))
+
+    accepts = [
+        Auth(),
+        pcu_type_fields
+       ]
+
+    returns = Parameter(int, 'New pcu_type_id (> 0) if successful')
+
+    
+    def call(self, auth, pcu_type_fields):
+       pcu_type_fields = dict(filter(can_update, pcu_type_fields.items()))
+        pcu_type = PCUType(self.api, pcu_type_fields)
+       pcu_type.sync()
+       self.event_object = {'PCUType': [pcu_type['pcu_type_id']]}      
+
+        return pcu_type['pcu_type_id']
diff --git a/PLC/Methods/AddPeer.py b/PLC/Methods/AddPeer.py
new file mode 100644 (file)
index 0000000..d6dc576
--- /dev/null
@@ -0,0 +1,36 @@
+#
+# Thierry Parmentelat - INRIA
+# 
+
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Peers import Peer, Peers
+
+can_update = lambda (field, value): field in \
+             ['peername', 'peer_url', 'key', 'cacert']
+
+class AddPeer(Method):
+    """
+    Adds a new peer.
+
+    Returns the new peer_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    peer_fields = dict(filter(can_update, Peer.fields.items()))
+
+    accepts = [
+        Auth(),
+        peer_fields
+        ]
+
+    returns = Parameter(int, "New peer_id (> 0) if successful")
+
+    def call(self, auth, peer_fields):
+       peer = Peer(self.api, peer_fields);
+       peer.sync()
+       self.event_objects = {'Peer': [peer['peer_id']]}
+
+       return peer['peer_id']
diff --git a/PLC/Methods/AddPerson.py b/PLC/Methods/AddPerson.py
new file mode 100644 (file)
index 0000000..cb0aa71
--- /dev/null
@@ -0,0 +1,43 @@
+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 Auth
+
+can_update = lambda (field, value): field in \
+             ['first_name', 'last_name', 'title',
+              'email', 'password', 'phone', 'url', 'bio']
+
+class AddPerson(Method):
+    """
+    Adds a new account. Any fields specified in person_fields are
+    used, otherwise defaults are used.
+
+    Accounts are disabled by default. To enable an account, use
+    UpdatePerson().
+
+    Returns the new person_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    person_fields = dict(filter(can_update, Person.fields.items()))
+
+    accepts = [
+        Auth(),
+        person_fields
+        ]
+
+    returns = Parameter(int, 'New person_id (> 0) if successful')
+
+    def call(self, auth, person_fields):
+        person_fields = dict(filter(can_update, person_fields.items()))
+        person_fields['enabled'] = False
+        person = Person(self.api, person_fields)
+        person.sync()
+
+       # Logging variables
+       self.event_objects = {'Person': [person['person_id']]}
+       self.message = 'Person %d added' % person['person_id']  
+
+        return person['person_id']
diff --git a/PLC/Methods/AddPersonKey.py b/PLC/Methods/AddPersonKey.py
new file mode 100644 (file)
index 0000000..aa4ed00
--- /dev/null
@@ -0,0 +1,59 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Keys import Key, Keys
+from PLC.Persons import Person, Persons
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in ['key_type','key']
+
+class AddPersonKey(Method):
+    """
+    Adds a new key to the specified account.
+
+    Non-admins can only modify their own keys.
+
+    Returns the new key_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    key_fields = dict(filter(can_update, Key.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        key_fields
+        ]
+
+    returns = Parameter(int, 'New key_id (> 0) if successful')
+
+    def call(self, auth, person_id_or_email, key_fields):
+        key_fields = dict(filter(can_update, key_fields.items()))
+
+        # Get account details
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+       # If we are not admin, make sure caller is adding a key to their account
+        if 'admin' not in self.caller['roles']:
+            if person['person_id'] != self.caller['person_id']:
+                raise PLCPermissionDenied, "You may only modify your own keys"
+
+        key = Key(self.api, key_fields)
+        key.sync(commit = False)
+        person.add_key(key, commit = True)
+
+        # Logging variables
+       self.event_objects = {'Person': [person['person_id']],
+                             'Key': [key['key_id']]}
+       self.message = 'Key %d added to person %d' % \
+               (key['key_id'], person['person_id'])
+
+        return key['key_id']
diff --git a/PLC/Methods/AddPersonToSite.py b/PLC/Methods/AddPersonToSite.py
new file mode 100644 (file)
index 0000000..7d5ac10
--- /dev/null
@@ -0,0 +1,56 @@
+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 Auth
+
+class AddPersonToSite(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 = [
+        Auth(),
+        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[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        # Get site information
+        sites = Sites(self.api, [site_id_or_login_base])
+        if not sites:
+            raise PLCInvalidArgument, "No such site"
+        site = sites[0]
+
+        if site['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local site"
+
+        if site['site_id'] not in person['site_ids']:
+            site.add_person(person)
+
+       # Logging variables
+       self.event_objects = {'Site': [site['site_id']],
+                             'Person': [person['person_id']]}
+       self.message = 'Person %d added to site %d' % \
+               (person['person_id'], site['site_id'])
+        return 1
diff --git a/PLC/Methods/AddPersonToSlice.py b/PLC/Methods/AddPersonToSlice.py
new file mode 100644 (file)
index 0000000..e3392eb
--- /dev/null
@@ -0,0 +1,61 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+
+class AddPersonToSlice(Method):
+    """
+    Adds the specified person to the specified slice. If the person is
+    already a member of the slice, no errors are returned. 
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, person_id_or_email, slice_id_or_name):
+        # Get account information
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        # Get slice information
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        # N.B. Allow foreign users to be added to local slices and
+        # local users to be added to foreign slices (and, of course,
+        # local users to be added to local slices).
+        if person['peer_id'] is not None and slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Cannot add foreign users to foreign slices"
+
+        # If we are not admin, make sure the caller is a PI
+        # of the site associated with the slice
+       if 'admin' not in self.caller['roles']:
+            if slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to add users to this slice"
+
+       if slice['slice_id'] not in person['slice_ids']:
+            slice.add_person(person)
+
+        # Logging variables
+       self.event_objects = {'Person': [person['person_id']],
+                             'Slice': [slice['slice_id']]}     
+       self.object_ids = [slice['slice_id']]
+
+        return 1
diff --git a/PLC/Methods/AddRole.py b/PLC/Methods/AddRole.py
new file mode 100644 (file)
index 0000000..7266180
--- /dev/null
@@ -0,0 +1,32 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Roles import Role, Roles
+from PLC.Auth import Auth
+
+class AddRole(Method):
+    """
+    Adds a new role.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Role.fields['role_id'],
+        Role.fields['name']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, role_id, name):
+        role = Role(self.api)
+        role['role_id'] = role_id
+        role['name'] = name
+        role.sync(insert = True)
+       self.event_objects = {'Role': [role['role_id']]}
+
+        return 1
diff --git a/PLC/Methods/AddRoleToPerson.py b/PLC/Methods/AddRoleToPerson.py
new file mode 100644 (file)
index 0000000..5a8e241
--- /dev/null
@@ -0,0 +1,66 @@
+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 Auth
+from PLC.Roles import Role, Roles
+
+class AddRoleToPerson(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 = [
+        Auth(),
+        Mixed(Role.fields['role_id'],
+              Role.fields['name']),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, role_id_or_name, person_id_or_email):
+        # Get role
+        roles = Roles(self.api, [role_id_or_name])
+        if not roles:
+            raise PLCInvalidArgument, "Invalid role '%s'" % unicode(role_id_or_name)
+        role = roles[0]
+
+        # Get account information
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        # 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['role_id'] <= min(self.caller['role_ids']):
+            raise PLCInvalidArgument, "Not allowed to grant that role"
+
+        if role['role_id'] not in person['role_ids']:
+            person.add_role(role)
+
+       self.event_objects = {'Person': [person['person_id']],
+                             'Role': [role['role_id']]}
+       self.message = "Role %d granted to person %d" % \
+                       (role['role_id'], person['person_id'])
+
+        return 1
diff --git a/PLC/Methods/AddSession.py b/PLC/Methods/AddSession.py
new file mode 100644 (file)
index 0000000..6f5bc88
--- /dev/null
@@ -0,0 +1,37 @@
+import time
+
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Sessions import Session, Sessions
+from PLC.Persons import Person, Persons
+
+class AddSession(Method):
+    """
+    Creates and returns a new session key for the specified user. 
+    (Used for website 'user sudo')
+    """
+
+    roles = ['admin']
+    accepts = [
+       Auth(),
+       Mixed(Person.fields['person_id'],
+              Person.fields['email'])
+       ]
+    returns = Session.fields['session_id']
+    
+
+    def call(self, auth, person_id_or_email):
+        
+       persons = Persons(self.api, [person_id_or_email], ['person_id', 'email'])
+       
+       if not persons:
+           raise PLCInvalidArgument, "No such person"
+       
+       person = persons[0]
+       session = Session(self.api)
+        session['expires'] = int(time.time()) + (24 * 60 * 60)
+       session.sync(commit = False)
+       session.add_person(person, commit = True)
+
+        return session['session_id']
diff --git a/PLC/Methods/AddSite.py b/PLC/Methods/AddSite.py
new file mode 100644 (file)
index 0000000..475c8b1
--- /dev/null
@@ -0,0 +1,41 @@
+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 Auth
+
+can_update = lambda (field, value): field in \
+             ['name', 'abbreviated_name', 'login_base',
+              'is_public', 'latitude', 'longitude', 'url',
+              'max_slices', 'max_slivers', 'enabled']
+
+class AddSite(Method):
+    """
+    Adds a new site, and creates a node group for that site. Any
+    fields specified in site_fields are used, otherwise defaults are
+    used.
+
+    Returns the new site_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    site_fields = dict(filter(can_update, Site.fields.items()))
+
+    accepts = [
+        Auth(),
+        site_fields
+        ]
+
+    returns = Parameter(int, 'New site_id (> 0) if successful')
+
+    def call(self, auth, site_fields):
+        site_fields = dict(filter(can_update, site_fields.items()))
+        site = Site(self.api, site_fields)
+        site.sync()
+       
+       # Logging variables 
+       self.event_objects = {'Site': [site['site_id']]}
+        self.message = 'Site %d created' % site['site_id']
+       
+       return site['site_id']
diff --git a/PLC/Methods/AddSiteAddress.py b/PLC/Methods/AddSiteAddress.py
new file mode 100644 (file)
index 0000000..a3a4eff
--- /dev/null
@@ -0,0 +1,58 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Addresses import Address, Addresses
+from PLC.Auth import Auth
+from PLC.Sites import Site, Sites
+
+can_update = lambda (field, value): field in \
+             ['line1', 'line2', 'line3',
+              'city', 'state', 'postalcode', 'country']
+
+class AddSiteAddress(Method):
+    """
+    Adds a new address to a site. Fields specified in
+    address_fields are used; some are not optional.
+
+    PIs may only add addresses to their own sites.
+
+    Returns the new address_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    address_fields = dict(filter(can_update, Address.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base']),
+        address_fields
+        ]
+
+    returns = Parameter(int, 'New address_id (> 0) if successful')
+
+    def call(self, auth, site_id_or_login_base, address_fields):
+        address_fields = dict(filter(can_update, address_fields.items()))
+
+        # Get associated site details
+        sites = Sites(self.api, [site_id_or_login_base])
+        if not sites:
+            raise PLCInvalidArgument, "No such site"
+        site = sites[0]
+
+        if 'admin' not in self.caller['roles']:
+            if site['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Address must be associated with one of your sites"
+
+        address = Address(self.api, address_fields)
+        address.sync(commit = False)
+        site.add_address(address, commit = True)
+
+       # Logging variables
+       self.event_objects = {'Site': [site['site_id']], 
+                             'Address': [address['address_id']]}
+       self.message = 'Address %d assigned to Site %d' % \
+               (address['address_id'], site['site_id'])
+
+        return address['address_id']
diff --git a/PLC/Methods/AddSlice.py b/PLC/Methods/AddSlice.py
new file mode 100644 (file)
index 0000000..6cc056c
--- /dev/null
@@ -0,0 +1,80 @@
+import re
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Sites import Site, Sites
+
+can_update = lambda (field, value): field in \
+             ['name', 'instantiation', 'url', 'description', 'max_nodes']
+
+class AddSlice(Method):
+    """
+    Adds a new slice. Any fields specified in slice_fields are used,
+    otherwise defaults are used.
+
+    Valid slice names are lowercase and begin with the login_base
+    (slice prefix) of a valid site, followed by a single
+    underscore. Thereafter, only letters, numbers, or additional
+    underscores may be used.
+
+    PIs may only add slices associated with their own sites (i.e.,
+    slice prefixes must always be the login_base of one of their
+    sites).
+
+    Returns the new slice_id (> 0) if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    slice_fields = dict(filter(can_update, Slice.fields.items()))
+
+    accepts = [
+        Auth(),
+        slice_fields
+        ]
+
+    returns = Parameter(int, 'New slice_id (> 0) if successful')
+
+    def call(self, auth, slice_fields):
+        slice_fields = dict(filter(can_update, slice_fields.items()))
+
+        # 1. Lowercase.
+        # 2. Begins with login_base (letters or numbers).
+        # 3. Then single underscore after login_base.
+        # 4. Then letters, numbers, or underscores.
+        name = slice_fields['name']
+        good_name = r'^[a-z0-9]+_[a-zA-Z0-9_]+$'
+        if not name or \
+           not re.match(good_name, name):
+            raise PLCInvalidArgument, "Invalid slice name"
+
+        # Get associated site details
+        login_base = name.split("_")[0]
+        sites = Sites(self.api, [login_base])
+        if not sites:
+            raise PLCInvalidArgument, "Invalid slice prefix %s in %s"%(login_base,name)
+        site = sites[0]
+
+        if 'admin' not in self.caller['roles']:
+            if site['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Slice prefix %s must be the same as the login_base of one of your sites"%login_base
+
+        if len(site['slice_ids']) >= site['max_slices']:
+            raise PLCInvalidArgument, "Site %s has reached (%d) its maximum allowable slice count (%d)"%(site['name'],
+                                                                                                         len(site['slice_ids']),
+                                                                                                         site['max_slices'])
+
+       if not site['enabled']:
+           raise PLCInvalidArgument, "Site %s is disabled can cannot create slices" % (site['name'])
+        
+        slice = Slice(self.api, slice_fields)
+        slice['creator_person_id'] = self.caller['person_id']
+        slice['site_id'] = site['site_id']
+        slice.sync()
+
+       self.event_objects = {'Slice': [slice['slice_id']]}
+
+        return slice['slice_id']
diff --git a/PLC/Methods/AddSliceAttribute.py b/PLC/Methods/AddSliceAttribute.py
new file mode 100644 (file)
index 0000000..ad32437
--- /dev/null
@@ -0,0 +1,113 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceAttributeTypes import SliceAttributeType, SliceAttributeTypes
+from PLC.Slices import Slice, Slices
+from PLC.Nodes import Node, Nodes
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.InitScripts import InitScript, InitScripts
+from PLC.Auth import Auth
+
+class AddSliceAttribute(Method):
+    """
+    Sets the specified attribute of the slice (or sliver, if
+    node_id_or_hostname is specified) to the specified value.
+
+    Attributes may require the caller to have a particular role in
+    order to be set or changed. Users may only set attributes of
+    slices or slivers of which they are members. PIs may only set
+    attributes of slices or slivers at their sites, or of which they
+    are members. Admins may set attributes of any slice or sliver.
+
+    Returns the new slice_attribute_id (> 0) if successful, faults
+    otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Mixed(SliceAttribute.fields['slice_id'],
+              SliceAttribute.fields['name']),
+        Mixed(SliceAttribute.fields['attribute_type_id'],
+              SliceAttribute.fields['name']),
+        Mixed(SliceAttribute.fields['value'],
+             InitScript.fields['name']),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname'],
+             None),
+       Mixed(NodeGroup.fields['nodegroup_id'],
+              NodeGroup.fields['name'])
+        ]
+
+    returns = Parameter(int, 'New slice_attribute_id (> 0) if successful')
+
+    def call(self, auth, slice_id_or_name, attribute_type_id_or_name, value, node_id_or_hostname = None, nodegroup_id_or_name = None):
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        attribute_types = SliceAttributeTypes(self.api, [attribute_type_id_or_name])
+        if not attribute_types:
+            raise PLCInvalidArgument, "No such slice attribute type"
+        attribute_type = attribute_types[0]
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+
+            if attribute_type['min_role_id'] is not None and \
+               min(self.caller['role_ids']) > attribute_type['min_role_id']:
+                raise PLCPermissionDenied, "Not allowed to set the specified slice attribute"
+
+       # if initscript is specified, validate value
+       if attribute_type['name'] in ['initscript']:
+           initscripts = InitScripts(self.api, {'enabled': True, 'name': value})
+           if not initscripts: 
+               raise PLCInvalidArgument, "No such plc initscript"      
+
+        slice_attribute = SliceAttribute(self.api)
+        slice_attribute['slice_id'] = slice['slice_id']
+        slice_attribute['attribute_type_id'] = attribute_type['attribute_type_id']
+        slice_attribute['value'] = unicode(value)
+
+        # Sliver attribute if node is specified
+        if node_id_or_hostname is not None:
+            nodes = Nodes(self.api, [node_id_or_hostname])
+            if not nodes:
+                raise PLCInvalidArgument, "No such node"
+            node = nodes[0]
+            
+            if node['node_id'] not in slice['node_ids']:
+                raise PLCInvalidArgument, "Node not in the specified slice"
+            slice_attribute['node_id'] = node['node_id']
+
+       # Sliver attribute shared accross nodes if nodegroup is sepcified
+       if nodegroup_id_or_name is not None:
+           nodegroups = NodeGroups(self.api, [nodegroup_id_or_name])
+           if not nodegroups:
+               raise PLCInvalidArgument, "No such nodegroup"
+           nodegroup = nodegroups[0]
+       
+           slice_attribute['nodegroup_id'] = nodegroup['nodegroup_id']
+
+       # Check if slice attribute alreay exists
+        slice_attributes_check = SliceAttributes(self.api, {'slice_id': slice['slice_id'], 'name': attribute_type['name'], 'value': value})
+        for slice_attribute_check in slice_attributes_check:
+            if 'node_id' in slice_attribute and slice_attribute['node_id'] == slice_attribute_check['node_id']:
+               raise PLCInvalidArgument, "Sliver attribute already exists"
+           if 'nodegroup_id' in slice_attribute and slice_attribute['nodegroup_id'] == slice_attribute_check['nodegroup_id']:
+               raise PLCInvalidArgument, "Slice attribute already exists for this nodegroup"
+            if node_id_or_hostname is None and nodegroup_id_or_name is None:
+                raise PLCInvalidArgument, "Slice attribute already exists"
+
+        slice_attribute.sync()
+       self.event_objects = {'SliceAttribute': [slice_attribute['slice_attribute_id']]}
+
+        return slice_attribute['slice_attribute_id']
diff --git a/PLC/Methods/AddSliceAttributeType.py b/PLC/Methods/AddSliceAttributeType.py
new file mode 100644 (file)
index 0000000..095ae83
--- /dev/null
@@ -0,0 +1,38 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceAttributeTypes import SliceAttributeType, SliceAttributeTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['name', 'description', 'min_role_id']
+
+class AddSliceAttributeType(Method):
+    """
+    Adds a new type of slice attribute. Any fields specified in
+    attribute_type_fields are used, otherwise defaults are used.
+
+    Returns the new attribute_type_id (> 0) if successful, faults
+    otherwise.
+    """
+
+    roles = ['admin']
+
+    attribute_type_fields = dict(filter(can_update, SliceAttributeType.fields.items()))
+
+    accepts = [
+        Auth(),
+        attribute_type_fields
+        ]
+
+    returns = Parameter(int, 'New attribute_id (> 0) if successful')
+
+
+    def call(self, auth, attribute_type_fields):
+        attribute_type_fields = dict(filter(can_update, attribute_type_fields.items()))
+        attribute_type = SliceAttributeType(self.api, attribute_type_fields)
+        attribute_type.sync()
+
+       self.event_objects = {'AttributeType': [attribute_type['attribute_type_id']]}
+
+        return attribute_type['attribute_type_id']
diff --git a/PLC/Methods/AddSliceInstantiation.py b/PLC/Methods/AddSliceInstantiation.py
new file mode 100644 (file)
index 0000000..0374957
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceInstantiations import SliceInstantiation, SliceInstantiations
+from PLC.Auth import Auth
+
+class AddSliceInstantiation(Method):
+    """
+    Adds a new slice instantiation state.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        SliceInstantiation.fields['instantiation']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, name):
+        slice_instantiation = SliceInstantiation(self.api)
+        slice_instantiation['instantiation'] = name
+        slice_instantiation.sync(insert = True)
+
+        return 1
diff --git a/PLC/Methods/AddSliceToNodes.py b/PLC/Methods/AddSliceToNodes.py
new file mode 100644 (file)
index 0000000..d5a2c8c
--- /dev/null
@@ -0,0 +1,69 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Slices import Slice, Slices
+from PLC.Persons import Person, Persons
+from PLC.Auth import Auth
+
+class AddSliceToNodes(Method):
+    """
+    Adds the specified slice to the specified nodes. Nodes may be
+    either local or foreign nodes.
+
+    If the slice is already associated with a node, no errors are
+    returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+       [Mixed(Node.fields['node_id'],
+               Node.fields['hostname'])]
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_id_or_name, node_id_or_hostname_list):
+        # Get slice information
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        if slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local slice"
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+       
+        # Get specified nodes, add them to the slice         
+        nodes = Nodes(self.api, node_id_or_hostname_list, ['node_id', 'hostname', 'slice_ids', 'slice_ids_whitelist', 'site_id'])
+       
+       for node in nodes:
+           # check the slice whitelist on each node first
+           # allow  users at site to add node to slice, ignoring whitelist
+           if node['slice_ids_whitelist'] and \
+              slice['slice_id'] not in node['slice_ids_whitelist'] and \
+              not set(self.caller['site_ids']).intersection([node['site_id']]):
+               raise PLCInvalidArgument, "%s is not allowed on %s (not on the whitelist)" % \
+                 (slice['name'], node['hostname'])
+            if slice['slice_id'] not in node['slice_ids']:
+                slice.add_node(node, commit = False)
+
+        slice.sync()
+
+       self.event_objects = {'Node': [node['node_id'] for node in nodes],
+                             'Slice': [slice['slice_id']]}
+
+        return 1
diff --git a/PLC/Methods/AddSliceToNodesWhitelist.py b/PLC/Methods/AddSliceToNodesWhitelist.py
new file mode 100644 (file)
index 0000000..a6b4bd1
--- /dev/null
@@ -0,0 +1,54 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+
+class AddSliceToNodesWhitelist(Method):
+    """
+    Adds the specified slice to the whitelist on the specified nodes. Nodes may be
+    either local or foreign nodes.
+
+    If the slice is already associated with a node, no errors are
+    returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+       [Mixed(Node.fields['node_id'],
+               Node.fields['hostname'])]
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_id_or_name, node_id_or_hostname_list):
+        # Get slice information
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        if slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local slice"
+
+        # Get specified nodes, add them to the slice         
+        nodes = Nodes(self.api, node_id_or_hostname_list)
+       for node in nodes:
+           if node['peer_id'] is not None:
+                raise PLCInvalidArgument, "%s not a local node" % node['hostname']
+            if slice['slice_id'] not in node['slice_ids_whitelist']:
+                slice.add_to_node_whitelist(node, commit = False)
+           
+        slice.sync()
+
+       self.event_objects = {'Node': [node['node_id'] for node in nodes],
+                             'Slice': [slice['slice_id']]}
+
+        return 1
diff --git a/PLC/Methods/AdmAddAddressType.py b/PLC/Methods/AdmAddAddressType.py
new file mode 100644 (file)
index 0000000..e0cd09d
--- /dev/null
@@ -0,0 +1,21 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.AddressTypes import AddressType, AddressTypes
+from PLC.Auth import Auth
+from PLC.Methods.AddAddressType import AddAddressType
+
+class AdmAddAddressType(AddAddressType):
+    """
+    Deprecated. See AddAddressType.
+    """
+
+    status = "deprecated"
+
+    accepts = [
+        Auth(),
+        AddressType.fields['name']
+        ]
+
+    def call(self, auth, name):
+        return AddAddressType.call(self, auth, {'name': name})
diff --git a/PLC/Methods/AdmAddNode.py b/PLC/Methods/AdmAddNode.py
new file mode 100644 (file)
index 0000000..dda5c99
--- /dev/null
@@ -0,0 +1,33 @@
+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 Auth
+from PLC.Methods.AddNode import AddNode
+
+can_update = lambda (field, value): field in \
+             ['model', 'version']
+
+class AdmAddNode(AddNode):
+    """
+    Deprecated. See AddNode.
+    """
+
+    status = "deprecated"
+
+    node_fields = dict(filter(can_update, Node.fields.items()))
+
+    accepts = [
+        Auth(),
+        Site.fields['site_id'],
+        Node.fields['hostname'],
+        Node.fields['boot_state'],
+        node_fields
+        ]
+
+    def call(self, auth, site_id, hostname, boot_state, node_fields = {}):
+        node_fields['site_id'] = site_id
+        node_fields['hostname'] = hostname
+        node_fields['boot_state'] = boot_state
+        return AddNode.call(self, auth, node_fields)
diff --git a/PLC/Methods/AdmAddNodeGroup.py b/PLC/Methods/AdmAddNodeGroup.py
new file mode 100644 (file)
index 0000000..6bbb59e
--- /dev/null
@@ -0,0 +1,22 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Auth import Auth
+from PLC.Methods.AddNodeGroup import AddNodeGroup
+
+class AdmAddNodeGroup(AddNodeGroup):
+    """
+    Deprecated. See AddNodeGroup.
+    """
+
+    status = "deprecated"
+
+    accepts = [
+        Auth(),
+        NodeGroup.fields['name'],
+        NodeGroup.fields['description']
+        ]
+
+    def call(self, auth, name, description):
+        return AddNodeGroup.call(self, auth, {'name': name, 'description': description})
diff --git a/PLC/Methods/AdmAddNodeNetwork.py b/PLC/Methods/AdmAddNodeNetwork.py
new file mode 100644 (file)
index 0000000..c309a77
--- /dev/null
@@ -0,0 +1,31 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+from PLC.Methods.AddNodeNetwork import AddNodeNetwork
+
+can_update = lambda (field, value): field not in ['nodenetwork_id', 'node_id', 'method', 'type']
+
+class AdmAddNodeNetwork(AddNodeNetwork):
+    """
+    Deprecated. See AddNodeNetwork.
+    """
+
+    status = "deprecated"
+
+    nodenetwork_fields = dict(filter(can_update, NodeNetwork.fields.items()))
+
+    accepts = [
+        Auth(),
+        NodeNetwork.fields['node_id'],
+        NodeNetwork.fields['method'],
+        NodeNetwork.fields['type'],
+        nodenetwork_fields
+        ]
+
+    def call(self, auth, node_id, method, type, nodenetwork_fields = {}):
+        nodenetwork_fields['node_id'] = node_id
+        nodenetwork_fields['method'] = method
+        nodenetwork_fields['type'] = type
+        return AddNodeNetwork.call(self, auth, nodenetwork_fields)
diff --git a/PLC/Methods/AdmAddNodeToNodeGroup.py b/PLC/Methods/AdmAddNodeToNodeGroup.py
new file mode 100644 (file)
index 0000000..dc7eab4
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.AddNodeToNodeGroup import AddNodeToNodeGroup
+
+class AdmAddNodeToNodeGroup(AddNodeToNodeGroup):
+    """
+    Deprecated. See AddNodeToNodeGroup.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmAddPerson.py b/PLC/Methods/AdmAddPerson.py
new file mode 100644 (file)
index 0000000..2b90f61
--- /dev/null
@@ -0,0 +1,30 @@
+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 Auth
+from PLC.Methods.AddPerson import AddPerson
+
+can_update = lambda (field, value): field in \
+             ['title', 'email', 'password', 'phone', 'url', 'bio']
+
+class AdmAddPerson(AddPerson):
+    """
+    Deprecated. See AddPerson.
+    """
+
+    status = "deprecated"
+
+    person_fields = dict(filter(can_update, Person.fields.items()))
+
+    accepts = [
+        Auth(),
+        Person.fields['first_name'],
+        Person.fields['last_name'],
+        person_fields
+        ]
+
+    def call(self, auth, first_name, last_name, person_fields = {}):
+        person_fields['first_name'] = first_name
+        person_fields['last_name'] = last_name
+        return AddPerson.call(self, auth, person_fields)
diff --git a/PLC/Methods/AdmAddPersonKey.py b/PLC/Methods/AdmAddPersonKey.py
new file mode 100644 (file)
index 0000000..05d0a69
--- /dev/null
@@ -0,0 +1,28 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Keys import Key, Keys
+from PLC.Persons import Person, Persons
+from PLC.Auth import Auth
+from PLC.Methods.AddPersonKey import AddPersonKey
+
+class AdmAddPersonKey(AddPersonKey):
+    """
+    Deprecated. See AddPersonKey. Keys can no longer be marked as
+    primary, i.e. the is_primary argument does nothing.
+    """
+
+    status = "deprecated"
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        Key.fields['key_type'],
+        Key.fields['key'],
+        Parameter(int, "Make this key the primary key")
+        ]
+
+    def call(self, auth, person_id_or_email, key_type, key_value, is_primary):
+        key_fields = {'key_type': key_type, 'key_value': key_value}
+        return AddPersonKey.call(self, auth, person_id_or_email, key_fields)
diff --git a/PLC/Methods/AdmAddPersonToSite.py b/PLC/Methods/AdmAddPersonToSite.py
new file mode 100644 (file)
index 0000000..948b06f
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.AddPersonToSite import AddPersonToSite
+
+class AdmAddPersonToSite(AddPersonToSite):
+    """
+    Deprecated. See AddPersonToSite.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmAddSite.py b/PLC/Methods/AdmAddSite.py
new file mode 100644 (file)
index 0000000..929c0a7
--- /dev/null
@@ -0,0 +1,32 @@
+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 Auth
+from PLC.Methods.AddSite import AddSite
+
+can_update = lambda (field, value): field in \
+             ['is_public', 'latitude', 'longitude', 'url']
+
+class AdmAddSite(AddSite):
+    """
+    Deprecated. See AddSite.
+    """
+
+    status = "deprecated"
+
+    site_fields = dict(filter(can_update, Site.fields.items()))
+
+    accepts = [
+        Auth(),
+        Site.fields['name'],
+        Site.fields['abbreviated_name'],
+        Site.fields['login_base'],
+        site_fields
+        ]
+
+    def call(self, auth, name, abbreviated_name, login_base, site_fields = {}):
+        site_fields['name'] = name
+        site_fields['abbreviated_name'] = abbreviated_name
+        site_fields['login_base'] = login_base
+        return AddSite.call(self, auth, site_fields)
diff --git a/PLC/Methods/AdmAddSitePowerControlUnit.py b/PLC/Methods/AdmAddSitePowerControlUnit.py
new file mode 100644 (file)
index 0000000..e9e452e
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.AddPCU import AddPCU
+
+class AdmAddSitePowerControlUnit(AddPCU):
+    """
+    Deprecated. See AddPCU.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmAssociateNodeToPowerControlUnitPort.py b/PLC/Methods/AdmAssociateNodeToPowerControlUnitPort.py
new file mode 100644 (file)
index 0000000..9e955be
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.PCUs import PCU, PCUs
+from PLC.Auth import Auth
+from PLC.Methods.AddNodeToPCU import AddNodeToPCU
+
+class AdmAssociateNodeToPowerControlUnitPort(AddNodeToPCU):
+    """
+    Deprecated. See AddNodeToPCU.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        PCU.fields['pcu_id'],
+        Parameter(int, 'PCU port number'),
+       Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, pcu_id, port, node_id_or_hostname):
+        return AddNodeToPCU(self, auth, node_id_or_hostname, pcu_id, port)
diff --git a/PLC/Methods/AdmAuthCheck.py b/PLC/Methods/AdmAuthCheck.py
new file mode 100644 (file)
index 0000000..63defa5
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.AuthCheck import AuthCheck
+
+class AdmAuthCheck(AuthCheck):
+    """
+    Deprecated. See AuthCheck.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmDeleteAddressType.py b/PLC/Methods/AdmDeleteAddressType.py
new file mode 100644 (file)
index 0000000..12f0625
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.DeleteAddressType import DeleteAddressType
+
+class AdmDeleteAddressType(DeleteAddressType):
+    """
+    Deprecated. See DeleteAddressType.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmDeleteAllPersonKeys.py b/PLC/Methods/AdmDeleteAllPersonKeys.py
new file mode 100644 (file)
index 0000000..9f038f9
--- /dev/null
@@ -0,0 +1,55 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Keys import Key, Keys
+from PLC.Auth import Auth
+
+class AdmDeleteAllPersonKeys(Method):
+    """
+    Deprecated. Functionality can be implemented with GetPersons and
+    DeleteKey.
+
+    Deletes all of the keys associated with an account. Non-admins may
+    only delete their own keys.
+
+    Non-admins may only delete their own keys.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    accepts = [
+        Auth(),
+        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[0]
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] != person['person_id']:
+                raise PLCPermissionDenied, "Not allowed to update specified account"
+
+        key_ids = person['key_ids']
+        if not key_ids:
+            return 1
+
+        # Get associated key details
+        keys = Keys(self.api, key_ids)
+
+        for key in keys:
+            key.delete()
+
+        return 1
diff --git a/PLC/Methods/AdmDeleteNode.py b/PLC/Methods/AdmDeleteNode.py
new file mode 100644 (file)
index 0000000..2ec9ff1
--- /dev/null
@@ -0,0 +1,9 @@
+from PLC.Methods.DeleteNode import DeleteNode
+
+class AdmDeleteNode(DeleteNode):
+    """
+    Deprecated. See DeleteNode.
+    """
+
+    status = "deprecated"
+
diff --git a/PLC/Methods/AdmDeleteNodeGroup.py b/PLC/Methods/AdmDeleteNodeGroup.py
new file mode 100644 (file)
index 0000000..b5b2cb6
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.DeleteNodeGroup import DeleteNodeGroup
+
+class AdmDeleteNodeGroup(DeleteNodeGroup):
+    """
+    Deprecated. See DeleteNodeGroup.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmDeleteNodeNetwork.py b/PLC/Methods/AdmDeleteNodeNetwork.py
new file mode 100644 (file)
index 0000000..d566504
--- /dev/null
@@ -0,0 +1,24 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Methods.DeleteNodeNetwork import DeleteNodeNetwork
+
+class AdmDeleteNodeNetwork(DeleteNodeNetwork):
+    """
+    Deprecated. See DeleteNodeNetwork.
+    """
+
+    status = "deprecated"
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+             Node.fields['hostname']),
+       NodeNetwork.fields['nodenetwork_id']
+        ]
+
+    def call(self, auth, node_id_or_hostname, nodenetwork_id):
+        return DeleteNodeNetwork.call(self, auth, nodenetwork_id)
diff --git a/PLC/Methods/AdmDeletePerson.py b/PLC/Methods/AdmDeletePerson.py
new file mode 100644 (file)
index 0000000..ff29e8b
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.DeletePerson import DeletePerson
+
+class AdmDeletePerson(DeletePerson):
+    """
+    Deprecated. See DeletePerson.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmDeletePersonKeys.py b/PLC/Methods/AdmDeletePersonKeys.py
new file mode 100644 (file)
index 0000000..fd24eef
--- /dev/null
@@ -0,0 +1,56 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Keys import Key, Keys
+from PLC.Auth import Auth
+
+class AdmDeletePersonKeys(Method):
+    """
+    Deprecated. Functionality can be implemented with GetPersons and
+    DeleteKey.
+
+    Deletes the specified keys. Non-admins may only delete their own
+    keys.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        [Key.fields['key_id']]
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, person_id_or_email, key_ids):
+        # Get account information
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] != person['person_id']:
+                raise PLCPermissionDenied, "Not allowed to update specified account"
+
+        key_ids = set(key_ids).intersection(person['key_ids'])
+        if not key_ids:
+            return 1
+
+        # Get associated key details
+        keys = Keys(self.api, key_ids)
+
+        for key in keys:
+            key.delete()
+
+        return 1
diff --git a/PLC/Methods/AdmDeleteSite.py b/PLC/Methods/AdmDeleteSite.py
new file mode 100644 (file)
index 0000000..7501ad5
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.DeleteSite import DeleteSite
+
+class AdmDeleteSite(DeleteSite):
+    """
+    Deprecated. See DeleteSite.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmDeleteSitePowerControlUnit.py b/PLC/Methods/AdmDeleteSitePowerControlUnit.py
new file mode 100644 (file)
index 0000000..2865224
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.DeletePCU import DeletePCU
+
+class AdmDeleteSitePowerControlUnit(DeletePCU):
+    """
+    Deprecated. See DeletePCU.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmDisassociatePowerControlUnitPort.py b/PLC/Methods/AdmDisassociatePowerControlUnitPort.py
new file mode 100644 (file)
index 0000000..5f7c448
--- /dev/null
@@ -0,0 +1,37 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.PCUs import PCU, PCUs
+from PLC.Auth import Auth
+from PLC.Methods.DeleteNodeFromPCU import DeleteNodeFromPCU
+
+class AdmDisassociatePowerControlUnitPort(DeleteNodeFromPCU):
+    """
+    Deprecated. See DeleteNodeFromPCU.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        PCU.fields['pcu_id'],
+        Parameter(int, 'PCU port number'),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, pcu_id, port):
+        pcus = PCUs(self.api, [pcu_id])
+        if not pcus:
+            raise PLCInvalidArgument, "No such PCU"
+
+        pcu = pcus[0]
+
+        ports = dict(zip(pcu['ports'], pcu['node_ids']))
+        if port not in ports:
+            raise PLCInvalidArgument, "No node on that port or no such port"
+
+        return DeleteNodeFromPCU(self, auth, ports[port], pcu_id)
diff --git a/PLC/Methods/AdmGenerateNodeConfFile.py b/PLC/Methods/AdmGenerateNodeConfFile.py
new file mode 100644 (file)
index 0000000..85789bd
--- /dev/null
@@ -0,0 +1,110 @@
+import random
+import base64
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+
+class AdmGenerateNodeConfFile(Method):
+    """
+    Deprecated. Functionality can be implemented with GetNodes,
+    GetNodeNetworks, and UpdateNode.
+
+    Creates a new node configuration file if all network settings are
+    present. This function will generate a new node key for the
+    specified node, effectively invalidating any old configuration
+    files.
+
+    Non-admins can only generate files for nodes at their sites.
+
+    Returns the contents of the file if successful, faults otherwise.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname'])
+        ]
+
+    returns = Parameter(str, "Node configuration file")
+
+    def call(self, auth, node_id_or_hostname):
+        # Get node information
+        nodes = Nodes(self.api, [node_id_or_hostname])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node"
+        node = nodes[0]
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+
+        # 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']:
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to generate a configuration file for that node"
+
+       # Get node networks for this node
+        primary = None
+        nodenetworks = NodeNetworks(self.api, node['nodenetwork_ids'])
+        for nodenetwork in nodenetworks:
+            if nodenetwork['is_primary']:
+                primary = nodenetwork
+                break
+        if primary is None:
+            raise PLCInvalidArgument, "No primary network configured"
+
+        # Split hostname into host and domain parts
+        parts = node['hostname'].split(".", 1)
+        if len(parts) < 2:
+            raise PLCInvalidArgument, "Node hostname is invalid"
+        host = parts[0]
+        domain = parts[1]
+
+        # Generate 32 random bytes
+        bytes = random.sample(xrange(0, 256), 32)
+        # Base64 encode their string representation
+        node['key'] = base64.b64encode("".join(map(chr, bytes)))
+        # XXX Boot Manager cannot handle = in the key
+        node['key'] = node['key'].replace("=", "")
+        # Save it
+        node.sync()
+
+        # Generate node configuration file suitable for BootCD
+        file = ""
+
+        file += 'NODE_ID="%d"\n' % node['node_id']
+        file += 'NODE_KEY="%s"\n' % node['key']
+
+        if primary['mac']:
+            file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
+
+        file += 'IP_METHOD="%s"\n' % primary['method']
+
+        if primary['method'] == 'static':
+            file += 'IP_ADDRESS="%s"\n' % primary['ip']
+            file += 'IP_GATEWAY="%s"\n' % primary['gateway']
+            file += 'IP_NETMASK="%s"\n' % primary['netmask']
+            file += 'IP_NETADDR="%s"\n' % primary['network']
+            file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
+            file += 'IP_DNS1="%s"\n' % primary['dns1']
+            file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
+
+        file += 'HOST_NAME="%s"\n' % host
+        file += 'DOMAIN_NAME="%s"\n' % domain
+
+        for nodenetwork in nodenetworks:
+            if nodenetwork['method'] == 'ipmi':
+                file += 'IPMI_ADDRESS="%s"\n' % nodenetwork['ip']
+                if nodenetwork['mac']:
+                    file += 'IPMI_MAC="%s"\n' % nodenetwork['mac'].lower()
+                break
+
+        return file
diff --git a/PLC/Methods/AdmGetAllAddressTypes.py b/PLC/Methods/AdmGetAllAddressTypes.py
new file mode 100644 (file)
index 0000000..ca4748b
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.GetAddressTypes import GetAddressTypes
+
+class AdmGetAllAddressTypes(GetAddressTypes):
+    """
+    Deprecated. See GetAddressTypes.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmGetAllKeyTypes.py b/PLC/Methods/AdmGetAllKeyTypes.py
new file mode 100644 (file)
index 0000000..4383f84
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.GetKeyTypes import GetKeyTypes
+
+class AdmGetAllKeyTypes(GetKeyTypes):
+    """
+    Deprecated. See GetKeyTypes.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmGetAllNodeNetworks.py b/PLC/Methods/AdmGetAllNodeNetworks.py
new file mode 100644 (file)
index 0000000..c00bdec
--- /dev/null
@@ -0,0 +1,37 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+from PLC.Methods.GetNodeNetworks import GetNodeNetworks
+
+class AdmGetAllNodeNetworks(GetNodeNetworks):
+    """
+    Deprecated. Functionality can be implemented with GetNodes and
+    GetNodeNetworks.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname'])
+        ]
+
+    returns = [NodeNetwork.fields]
+
+    def call(self, auth, node_id_or_hostname):
+        # Get node information
+        nodes = Nodes(self.api, [node_id_or_hostname])
+       if not nodes:
+            raise PLCInvalidArgument, "No such node"
+       node = nodes[0]
+
+        if not node['nodenetwork_ids']:
+            return []
+
+        return GetNodeNetworks.call(self, auth, node['nodenetwork_ids'])
diff --git a/PLC/Methods/AdmGetAllRoles.py b/PLC/Methods/AdmGetAllRoles.py
new file mode 100644 (file)
index 0000000..2b88714
--- /dev/null
@@ -0,0 +1,32 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter
+from PLC.Auth import Auth
+from PLC.Methods.GetRoles import GetRoles
+
+class AdmGetAllRoles(GetRoles):
+    """
+    Deprecated. See GetRoles.
+
+    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.
+    """
+
+    status = "deprecated"
+
+    returns = dict
+
+    def call(self, auth):
+        roles_list = GetRoles.call(self, auth)
+
+        roles_dict = {}
+        for role in roles_list:
+            # Stringify the keys!
+            roles_dict[str(role['role_id'])] = role['name']
+
+        return roles_dict
diff --git a/PLC/Methods/AdmGetNodeGroupNodes.py b/PLC/Methods/AdmGetNodeGroupNodes.py
new file mode 100644 (file)
index 0000000..51c392a
--- /dev/null
@@ -0,0 +1,36 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.NodeGroups import NodeGroup, NodeGroups
+
+class AdmGetNodeGroupNodes(Method):
+    """
+    Deprecated. See GetNodeGroups.
+
+    Returns a list of node_ids for the node group specified.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(NodeGroup.fields['nodegroup_id'],
+             NodeGroup.fields['name'])
+        ]
+
+    returns = NodeGroup.fields['node_ids']
+
+    def call(self, auth, nodegroup_id_or_name):
+        # Get nodes in this nodegroup
+       nodegroups = NodeGroups(self.api, [nodegroup_id_or_name])
+       if not nodegroups:
+            raise PLCInvalidArgument, "No such node group"
+
+       # Get the info for the node group specified
+       nodegroup = nodegroups[0]
+
+       # Return the list of node_ids
+        return nodegroup['node_ids']
diff --git a/PLC/Methods/AdmGetNodeGroups.py b/PLC/Methods/AdmGetNodeGroups.py
new file mode 100644 (file)
index 0000000..fa1ad59
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.GetNodeGroups import GetNodeGroups
+
+class AdmGetNodeGroups(GetNodeGroups):
+    """
+    Deprecated. See GetNodeGroups.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmGetNodes.py b/PLC/Methods/AdmGetNodes.py
new file mode 100644 (file)
index 0000000..74d8489
--- /dev/null
@@ -0,0 +1,11 @@
+from PLC.Methods.GetNodes import GetNodes
+
+class AdmGetNodes(GetNodes):
+    """
+    Deprecated. See GetNodes. All fields are now always returned.
+    """
+
+    status = "deprecated"
+
+    def call(self, auth, node_id_or_hostname_list = None, return_fields = None):
+        return GetNodes.call(self, auth, node_id_or_hostname_list)
diff --git a/PLC/Methods/AdmGetPersonKeys.py b/PLC/Methods/AdmGetPersonKeys.py
new file mode 100644 (file)
index 0000000..946230a
--- /dev/null
@@ -0,0 +1,40 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Keys import Key, Keys
+from PLC.Auth import Auth
+from PLC.Methods.GetKeys import GetKeys
+
+class AdmGetPersonKeys(GetKeys):
+    """
+    Deprecated. Functionality can be implemented with GetPersons and
+    GetKeys.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        [Key.fields['key_id']]
+        ]
+
+    returns = [Key.fields]
+
+    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[0]
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] != person['person_id']:
+                raise PLCPermissionDenied, "Not allowed to view keys for specified account"
+
+        return GetKeys.call(self, auth, person['key_ids'])
diff --git a/PLC/Methods/AdmGetPersonRoles.py b/PLC/Methods/AdmGetPersonRoles.py
new file mode 100644 (file)
index 0000000..024b93c
--- /dev/null
@@ -0,0 +1,55 @@
+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 Auth
+
+class AdmGetPersonRoles(Method):
+    """
+    Deprecated. See GetPersons.
+
+    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.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email'])
+        ]
+
+    returns = dict
+
+    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[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))
diff --git a/PLC/Methods/AdmGetPersonSites.py b/PLC/Methods/AdmGetPersonSites.py
new file mode 100644 (file)
index 0000000..79324f8
--- /dev/null
@@ -0,0 +1,47 @@
+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 Auth
+
+class AdmGetPersonSites(Method):
+    """
+    Deprecated. See GetPersons.
+
+    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.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email'])
+        ]
+
+    returns = Person.fields['site_ids']
+
+    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[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"
+
+        return person['site_ids']
diff --git a/PLC/Methods/AdmGetPersons.py b/PLC/Methods/AdmGetPersons.py
new file mode 100644 (file)
index 0000000..35e94a0
--- /dev/null
@@ -0,0 +1,11 @@
+from PLC.Methods.GetPersons import GetPersons
+
+class AdmGetPersons(GetPersons):
+    """
+    Deprecated. See GetPersons.
+    """
+
+    status = "deprecated"
+
+    def call(self, auth, person_id_or_email_list = None, return_fields = None):
+        return GetPersons.call(self, auth, person_id_or_email_list)
diff --git a/PLC/Methods/AdmGetPowerControlUnitNodes.py b/PLC/Methods/AdmGetPowerControlUnitNodes.py
new file mode 100644 (file)
index 0000000..af298ee
--- /dev/null
@@ -0,0 +1,41 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUs import PCU, PCUs
+from PLC.Auth import Auth
+
+class AdmGetPowerControlUnitNodes(Method):
+    """
+    Deprecated. See GetPCUs.
+
+    Returns a list of the nodes, and the ports they are assigned to,
+    on the specified PCU.
+    
+    Admin may query all PCUs. Non-admins may only query the PCUs at
+    their sites.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        PCU.fields['pcu_id']
+        ]
+
+    returns = [{'node_id': Parameter(int, "Node identifier"),
+                'port_number': Parameter(int, "Port number")}]
+
+    def call(self, auth, pcu_id):
+        pcus = PCUs(self.api, [pcu_id])
+        if not pcus:
+            raise PLCInvalidArgument, "No such PCU"
+        pcu = pcus[0]
+
+        if 'admin' not in self.caller['roles']:
+            if pcu['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to view that PCU"
+
+        return [{'node_id': node_id, 'port_number': port} \
+                for (node_id, port) in zip(pcu['node_ids'], pcu['ports'])]
diff --git a/PLC/Methods/AdmGetPowerControlUnits.py b/PLC/Methods/AdmGetPowerControlUnits.py
new file mode 100644 (file)
index 0000000..8f7e0c7
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.GetPCUs import GetPCUs
+
+class AdmGetPowerControlUnits(GetPCUs):
+    """
+    Deprecated. See GetPCUs.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmGetSiteNodes.py b/PLC/Methods/AdmGetSiteNodes.py
new file mode 100644 (file)
index 0000000..b366c80
--- /dev/null
@@ -0,0 +1,44 @@
+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 Auth
+
+class AdmGetSiteNodes(Method):
+    """
+    Deprecated. See GetSites.
+
+    Return a struct containing an array of node_ids for each of the
+    sites specified. Note that the keys of the struct are strings, not
+    integers, because of XML-RPC marshalling limitations.
+
+    Admins may retrieve details about all nodes on a site by not specifying
+    site_id_or_name 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.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        [Mixed(Site.fields['site_id'],
+               Site.fields['name'])],
+        ]
+
+    returns = dict
+
+    def call(self, auth, site_id_or_name_list = None):
+        # Get site information
+       sites = Sites(self.api, site_id_or_name_list)   
+       if not sites:
+            raise PLCInvalidArgument, "No such site"
+        
+       # Convert to {str(site_id): [node_id]}
+       site_nodes = {}
+       for site in sites:
+            site_nodes[str(site['site_id'])] = site['node_ids']
+               
+        return site_nodes
diff --git a/PLC/Methods/AdmGetSitePIs.py b/PLC/Methods/AdmGetSitePIs.py
new file mode 100644 (file)
index 0000000..d35ee88
--- /dev/null
@@ -0,0 +1,44 @@
+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.Auth import Auth
+
+class AdmGetSitePIs(Method):
+    """
+    Deprecated. Functionality can be implemented with GetSites and
+    GetPersons.
+
+    Return a list of person_ids of the PIs for the site specified.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base'])
+        ]
+
+    returns = Site.fields['person_ids']
+
+    def call(self, auth, site_id_or_login_base):
+        # Authenticated function
+        assert self.caller is not None
+
+        # Get site information
+       sites = Sites(self.api, [site_id_or_login_base])
+       if not sites:
+            raise PLCInvalidArgument, "No such site"
+
+       site = sites[0]
+
+        persons = Persons(self.api, site['person_ids'])
+
+        has_pi_role = lambda person: 'pi' in person['roles']
+        pis = filter(has_pi_role, persons)
+
+       return [pi['person_id'] for pi in pis]
diff --git a/PLC/Methods/AdmGetSitePersons.py b/PLC/Methods/AdmGetSitePersons.py
new file mode 100644 (file)
index 0000000..8122528
--- /dev/null
@@ -0,0 +1,44 @@
+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 Auth
+
+class AdmGetSitePersons(Method):
+    """
+    Deprecated. See GetSites.
+
+    Return a list of person_ids for the site specified.
+
+    PIs may only retrieve the person_ids of accounts at their
+    site. Admins may retrieve the person_ids of accounts at any site.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base'])
+        ]
+
+    returns = Site.fields['person_ids']
+
+    def call(self, auth, site_id_or_login_base):
+        # Authenticated function
+        assert self.caller is not None
+
+        # Get site information
+       sites = Sites(self.api, [site_id_or_login_base])
+       if not sites:
+            raise PLCInvalidArgument, "No such site"
+
+       site = sites[0]
+
+       if 'admin' not in self.caller['roles']: 
+            if site['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to view accounts at that site"
+
+       return site['person_ids']
diff --git a/PLC/Methods/AdmGetSitePowerControlUnits.py b/PLC/Methods/AdmGetSitePowerControlUnits.py
new file mode 100644 (file)
index 0000000..b95f298
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUs import PCU, PCUs
+from PLC.Sites import Site, Sites
+from PLC.Auth import Auth
+
+class AdmGetSitePowerControlUnits(Method):
+    """
+    Deprecated. Functionality can be implemented with GetSites and GetPCUs.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base'])
+        ]
+
+    returns = [PCU.fields]
+
+    def call(self, auth, site_id_or_login_base):
+        sites = Sites(self.api, [site_id_or_login_base])
+        if not sites:
+            raise PLCInvalidArgument, "No such site"
+        site = sites[0]
+
+        if 'admin' not in self.caller['roles']:
+            if site['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to view the PCUs at that site"
+
+        return PCUs(self.api, site['pcu_ids'])
diff --git a/PLC/Methods/AdmGetSiteTechContacts.py b/PLC/Methods/AdmGetSiteTechContacts.py
new file mode 100644 (file)
index 0000000..f531db5
--- /dev/null
@@ -0,0 +1,45 @@
+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.Auth import Auth
+
+class AdmGetSiteTechContacts(Method):
+    """
+    Deprecated. Functionality can be implemented with GetSites and
+    GetPersons.
+
+    Return a list of person_ids of the technical contacts for the site
+    specified.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base'])
+        ]
+
+    returns = Site.fields['person_ids']
+
+    def call(self, auth, site_id_or_login_base):
+        # Authenticated function
+        assert self.caller is not None
+
+        # Get site information
+       sites = Sites(self.api, [site_id_or_login_base])
+       if not sites:
+            raise PLCInvalidArgument, "No such site"
+
+       site = sites[0]
+
+        persons = Persons(self.api, site['person_ids'])
+
+        has_tech_role = lambda person: 'tech' in person['roles']
+        techs = filter(has_tech_role, persons)
+
+       return [tech['person_id'] for tech in techs]
diff --git a/PLC/Methods/AdmGetSites.py b/PLC/Methods/AdmGetSites.py
new file mode 100644 (file)
index 0000000..cf5b0cd
--- /dev/null
@@ -0,0 +1,11 @@
+from PLC.Methods.GetSites import GetSites
+
+class AdmGetSites(GetSites):
+    """
+    Deprecated. See GetSites.
+    """
+
+    status = "deprecated"
+
+    def call(self, auth, site_id_or_login_base_list = None, return_fields = None):
+        return GetSites.call(self, auth, site_id_or_login_base_list)
diff --git a/PLC/Methods/AdmGrantRoleToPerson.py b/PLC/Methods/AdmGrantRoleToPerson.py
new file mode 100644 (file)
index 0000000..36e2e25
--- /dev/null
@@ -0,0 +1,11 @@
+from PLC.Methods.AddRoleToPerson import AddRoleToPerson
+
+class AdmGrantRoleToPerson(AddRoleToPerson):
+    """
+    Deprecated. See AddRoleToPerson.
+    """
+
+    status = "deprecated"
+
+    def call(self, auth, person_id_or_email, role_id_or_name):
+        return AddRoleToPerson.call(self, auth, role_id_or_name, person_id_or_email)
diff --git a/PLC/Methods/AdmIsPersonInRole.py b/PLC/Methods/AdmIsPersonInRole.py
new file mode 100644 (file)
index 0000000..b32ab03
--- /dev/null
@@ -0,0 +1,67 @@
+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 Auth
+from PLC.Roles import Role, Roles
+
+class AdmIsPersonInRole(Method):
+    """
+    Deprecated. Functionality can be implemented with GetPersons.
+
+    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.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        Mixed(Parameter(int, "Role identifier"),
+              Parameter(str, "Role name"))
+        ]
+
+    returns = Parameter(int, "1 if account has role, 0 otherwise")
+
+    def call(self, auth, person_id_or_email, role_id_or_name):
+        # 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 = {}
+        for role in Roles(self.api):
+            roles[role['role_id']] = role['name']
+            roles[role['name']] = role['role_id']
+
+        if role_id_or_name not in roles:
+            raise PLCInvalidArgument, "Invalid role identifier or name"
+
+        if isinstance(role_id_or_name, int):
+            role_id = role_id_or_name
+        else:
+            role_id = roles[role_id_or_name]
+
+        if 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[0]
+
+        if role_id in person['role_ids']:
+            return 1
+
+        return 0
diff --git a/PLC/Methods/AdmQueryConfFile.py b/PLC/Methods/AdmQueryConfFile.py
new file mode 100644 (file)
index 0000000..6cf5d99
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Auth import Auth
+
+class AdmQueryConfFile(Method):
+    """
+    Deprecated. See GetConfFiles.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        {'node_id': Node.fields['node_id']}
+        ]
+
+    returns = [ConfFile.fields['conf_file_id']]
+
+    def call(self, auth, search_vals):
+        if 'node_id' in search_vals:
+            conf_files = ConfFiles(self.api)
+
+            conf_files = filter(lambda conf_file: \
+                                search_vals['node_id'] in conf_file['node_ids'],
+                                conf_files)
+
+            if conf_files:
+                return [conf_file['conf_file_id'] for conf_file in conf_files]
+
+        return []
diff --git a/PLC/Methods/AdmQueryNode.py b/PLC/Methods/AdmQueryNode.py
new file mode 100644 (file)
index 0000000..f41d04f
--- /dev/null
@@ -0,0 +1,67 @@
+import socket
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks, valid_ip
+from PLC.Auth import Auth
+
+class AdmQueryNode(Method):
+    """
+    Deprecated. Functionality can be implemented with GetNodes and
+    GetNodeNetworks.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        {'node_hostname': Node.fields['hostname'],
+         'nodenetwork_ip': NodeNetwork.fields['ip'],
+         'nodenetwork_mac': NodeNetwork.fields['mac'],
+         'nodenetwork_method': NodeNetwork.fields['method']}
+        ]
+
+    returns = [Node.fields['node_id']]
+
+    def call(self, auth, search_vals):
+        # Get possible nodenetworks
+        if 'node_hostname' in search_vals:
+            nodes = Nodes(self.api, [search_vals['node_hostname']])
+            if not nodes:
+                return []
+
+            # No network interface filters specified
+            if 'nodenetwork_ip' not in search_vals and \
+               'nodenetwork_mac' not in search_vals and \
+               'nodenetwork_method' not in search_vals:
+                return [nodes[0]['node_id']]
+
+            if nodes[0]['nodenetwork_ids']:
+                nodenetworks = NodeNetworks(self.api, nodes[0]['nodenetwork_ids'])
+            else:
+                nodenetworks = []
+        else:
+            nodenetworks = NodeNetworks(self.api)
+
+        if 'nodenetwork_ip' in search_vals:
+            if not valid_ip(search_vals['nodenetwork_ip']):
+                raise PLCInvalidArgument, "Invalid IP address"
+            nodenetworks = filter(lambda nodenetwork: \
+                                  socket.inet_aton(nodenetwork['ip']) == socket.inet_aton(search_vals['nodenetwork_ip']),
+                                  nodenetworks)
+
+        if 'nodenetwork_mac' in search_vals:
+            nodenetworks = filter(lambda nodenetwork: \
+                                 nodenetwork['mac'].lower() == search_vals['nodenetwork_mac'].lower(),
+                                 nodenetworks)
+
+        if 'nodenetwork_method' in search_vals:
+            nodenetworks = filter(lambda nodenetwork: \
+                                  nodenetwork['method'].lower() == search_vals['nodenetwork_method'].lower(),
+                                  nodenetworks)
+
+        return [nodenetwork['node_id'] for nodenetwork in nodenetworks]
diff --git a/PLC/Methods/AdmQueryPerson.py b/PLC/Methods/AdmQueryPerson.py
new file mode 100644 (file)
index 0000000..b41d0a5
--- /dev/null
@@ -0,0 +1,29 @@
+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 Auth
+
+class AdmQueryPerson(Method):
+    """
+    Deprecated. See GetPersons.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        {'email': Person.fields['email']}
+        ]
+
+    returns = [Person.fields['person_id']]
+
+    def call(self, auth, search_vals):
+        if 'email' in search_vals:
+            persons = Persons(self.api, [search_vals['email']])
+            if persons:
+                return [persons[0]['person_id']]
+
+        return []
diff --git a/PLC/Methods/AdmQueryPowerControlUnit.py b/PLC/Methods/AdmQueryPowerControlUnit.py
new file mode 100644 (file)
index 0000000..8fc2f42
--- /dev/null
@@ -0,0 +1,59 @@
+import socket
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUs import PCU, PCUs
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks, valid_ip
+from PLC.Auth import Auth
+
+class AdmQueryPowerControlUnit(Method):
+    """
+    Deprecated. Functionality can be implemented with GetPCUs or
+    GetNodes.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        {'pcu_hostname': PCU.fields['hostname'],
+         'pcu_ip': PCU.fields['ip'],
+         'node_hostname': Node.fields['hostname'],
+         'node_id': Node.fields['node_id']}
+        ]
+
+    returns = [PCU.fields['pcu_id']]
+
+    def call(self, auth, search_vals):
+        # Get all PCUs. This is a stupid function. The API should not
+        # be used for DB mining.
+        pcus = PCUs(self.api)
+
+        if 'pcu_hostname' in search_vals:
+            pcus = filter(lambda pcu: \
+                          pcu['hostname'].lower() == search_vals['pcu_hostname'].lower(),
+                          pcus)
+
+        if 'pcu_ip' in search_vals:
+            if not valid_ip(search_vals['pcu_ip']):
+                raise PLCInvalidArgument, "Invalid IP address"
+            pcus = filter(lambda pcu: \
+                          socket.inet_aton(pcu['ip']) == socket.inet_aton(search_vals['pcu_ip']),
+                          pcus)
+
+        if 'node_id' in search_vals:
+            pcus = filter(lambda pcu: \
+                          search_vals['node_id'] in pcu['node_ids'],
+                          pcus)
+
+        if 'node_hostname' in search_vals:
+            pcus = filter(lambda pcu: \
+                          search_vals['node_hostname'] in \
+                          [node['hostname'] for node in Nodes(self.api, pcu['node_ids'])],
+                          pcus)
+
+        return [pcu['pcu_id'] for pcu in pcus]
diff --git a/PLC/Methods/AdmQuerySite.py b/PLC/Methods/AdmQuerySite.py
new file mode 100644 (file)
index 0000000..cad6b8c
--- /dev/null
@@ -0,0 +1,87 @@
+import socket
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Sites import Site, Sites
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks, valid_ip
+from PLC.Auth import Auth
+
+class AdmQuerySite(Method):
+    """
+    Deprecated. Functionality can be implemented with GetSites and
+    GetNodes.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        {'site_name': Site.fields['name'],
+         'site_abbreviatedname': Site.fields['abbreviated_name'],
+         'site_loginbase': Site.fields['login_base'],
+         'node_hostname': Node.fields['hostname'],
+         'node_id': Node.fields['node_id'],
+         'nodenetwork_ip': NodeNetwork.fields['ip'],
+         'nodenetwork_mac': NodeNetwork.fields['mac']}
+        ]
+
+    returns = [Site.fields['site_id']]
+
+    def call(self, auth, search_vals):
+        if 'site_loginbase' in search_vals:
+            sites = Sites(self.api, [search_vals['site_loginbase']])
+        else:
+            sites = Sites(self.api)
+            
+        if 'site_name' in search_vals:
+            sites = filter(lambda site: \
+                           site['name'] == search_vals['site_name'],
+                           sites)
+
+        if 'site_abbreviatedname' in search_vals:
+            sites = filter(lambda site: \
+                           site['abbreviatedname'] == search_vals['site_abbreviatedname'],
+                           sites)
+
+        if 'node_id' in search_vals:
+            sites = filter(lambda site: \
+                           search_vals['node_id'] in site['node_ids'],
+                           sites)
+
+        if 'node_hostname' in search_vals or \
+           'nodenetwork_ip' in search_vals or \
+           'nodenetwork_mac' in search_vals:
+            for site in sites:
+                site['hostnames'] = []
+                site['ips'] = []
+                site['macs'] = []
+                if site['node_ids']:
+                    nodes = Nodes(self.api, site['node_ids'])
+                    for node in nodes:
+                        site['hostnames'].append(node['hostname'])
+                        if 'nodenetwork_ip' in search_vals or \
+                           'nodenetwork_mac' in search_vals:
+                            nodenetworks = NodeNetworks(self.api, node['nodenetwork_ids'])
+                            site['ips'] += [nodenetwork['ip'] for nodenetwork in nodenetworks]
+                            site['macs'] += [nodenetwork['mac'] for nodenetwork in nodenetworks]
+
+            if 'node_hostname' in search_vals:
+                sites = filter(lambda site: \
+                               search_vals['node_hostname'] in site['hostnames'],
+                               sites)
+
+            if 'nodenetwork_ip' in search_vals:
+                sites = filter(lambda site: \
+                               search_vals['nodenetwork_ip'] in site['ips'],
+                               sites)
+
+            if 'nodenetwork_mac' in search_vals:
+                sites = filter(lambda site: \
+                               search_vals['nodenetwork_mac'] in site['macs'],
+                               sites)
+
+        return [site['site_id'] for site in sites]
diff --git a/PLC/Methods/AdmRebootNode.py b/PLC/Methods/AdmRebootNode.py
new file mode 100644 (file)
index 0000000..c8368e3
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.RebootNode import RebootNode
+
+class AdmRebootNode(RebootNode):
+    """
+    Deprecated. See RebootNode.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmRemoveNodeFromNodeGroup.py b/PLC/Methods/AdmRemoveNodeFromNodeGroup.py
new file mode 100644 (file)
index 0000000..f905a16
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.DeleteNodeFromNodeGroup import DeleteNodeFromNodeGroup
+
+class AdmRemoveNodeFromNodeGroup(DeleteNodeFromNodeGroup):
+    """
+    Deprecated. See DeleteNodeFromNodeGroup.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmRemovePersonFromSite.py b/PLC/Methods/AdmRemovePersonFromSite.py
new file mode 100644 (file)
index 0000000..54d3f1d
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
+
+class AdmRemovePersonFromSite(DeletePersonFromSite):
+    """
+    Deprecated. See DeletePersonFromSite.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmRevokeRoleFromPerson.py b/PLC/Methods/AdmRevokeRoleFromPerson.py
new file mode 100644 (file)
index 0000000..2631a3a
--- /dev/null
@@ -0,0 +1,11 @@
+from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
+
+class AdmRevokeRoleFromPerson(DeleteRoleFromPerson):
+    """
+    Deprecated. See DeleteRoleFromPerson.
+    """
+
+    status = "deprecated"
+
+    def call(self, auth, person_id_or_email, role_id_or_name):
+        return DeleteRoleFromPerson.call(self, auth, role_id_or_name, person_id_or_email)
diff --git a/PLC/Methods/AdmSetPersonEnabled.py b/PLC/Methods/AdmSetPersonEnabled.py
new file mode 100644 (file)
index 0000000..2009f00
--- /dev/null
@@ -0,0 +1,23 @@
+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 Auth
+from PLC.Methods.UpdatePerson import UpdatePerson
+
+class AdmSetPersonEnabled(UpdatePerson):
+    """
+    Deprecated. See UpdatePerson.
+    """
+
+    status = "deprecated"
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        Person.fields['enabled']
+        ]
+
+    def call(self, auth, person_id_or_email, enabled):
+        return UpdatePerson.call(self, auth, person_id_or_email, {'enabled': enabled})
diff --git a/PLC/Methods/AdmSetPersonPrimarySite.py b/PLC/Methods/AdmSetPersonPrimarySite.py
new file mode 100644 (file)
index 0000000..c631a95
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.SetPersonPrimarySite import SetPersonPrimarySite
+
+class AdmSetPersonPrimarySite(SetPersonPrimarySite):
+    """
+    Deprecated. See SetPersonPrimarySite.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmUpdateNode.py b/PLC/Methods/AdmUpdateNode.py
new file mode 100644 (file)
index 0000000..ba4b3f1
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.UpdateNode import UpdateNode
+
+class AdmUpdateNode(UpdateNode):
+    """
+    Deprecated. See UpdateNode.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmUpdateNodeGroup.py b/PLC/Methods/AdmUpdateNodeGroup.py
new file mode 100644 (file)
index 0000000..b53198a
--- /dev/null
@@ -0,0 +1,27 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Auth import Auth
+from PLC.Methods.UpdateNodeGroup import UpdateNodeGroup
+
+class AdmUpdateNodeGroup(UpdateNodeGroup):
+    """
+    Deprecated. See UpdateNodeGroup.
+    """
+
+    status = "deprecated"
+
+    accepts = [
+        Auth(),
+        Mixed(NodeGroup.fields['nodegroup_id'],
+             NodeGroup.fields['name']),
+        NodeGroup.fields['name'],
+       NodeGroup.fields['description']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, nodegroup_id_or_name, name, description):
+        return UpdateNodeGroup.call(self, auth, nodegroup_id_or_name,
+                                    {'name': name, 'description': description})
diff --git a/PLC/Methods/AdmUpdateNodeNetwork.py b/PLC/Methods/AdmUpdateNodeNetwork.py
new file mode 100644 (file)
index 0000000..a85d62a
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.UpdateNodeNetwork import UpdateNodeNetwork
+
+class AdmUpdateNodeNetwork(UpdateNodeNetwork):
+    """
+    Deprecated. See UpdateNodeNetwork.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmUpdatePerson.py b/PLC/Methods/AdmUpdatePerson.py
new file mode 100644 (file)
index 0000000..066fe6d
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.UpdatePerson import UpdatePerson
+
+class AdmUpdatePerson(UpdatePerson):
+    """
+    Deprecated. See UpdatePerson.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmUpdateSite.py b/PLC/Methods/AdmUpdateSite.py
new file mode 100644 (file)
index 0000000..0b6c26a
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.UpdateSite import UpdateSite
+
+class AdmUpdateSite(UpdateSite):
+    """
+    Deprecated. See UpdateSite.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AdmUpdateSitePowerControlUnit.py b/PLC/Methods/AdmUpdateSitePowerControlUnit.py
new file mode 100644 (file)
index 0000000..ed564fb
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.UpdatePCU import UpdatePCU
+
+class AdmUpdateSitePowerControlUnit(UpdatePCU):
+    """
+    Deprecated. See UpdatePCU.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/AnonAdmGetNodeGroups.py b/PLC/Methods/AnonAdmGetNodeGroups.py
new file mode 100644 (file)
index 0000000..b223e1e
--- /dev/null
@@ -0,0 +1,11 @@
+from PLC.Methods.GetNodeGroups import GetNodeGroups
+class AnonAdmGetNodeGroups(GetNodeGroups):
+    """
+    Deprecated. See GetNodeGroups. All fields are now always returned
+    """
+    
+    status = "deprecated"
+
+    def call(self, auth, nodegroup_id_or_name_list =  None, return_fields = None):
+       return GetNodeGroups.call(self, auth, nodegroup_id_or_name_list)
diff --git a/PLC/Methods/AuthCheck.py b/PLC/Methods/AuthCheck.py
new file mode 100644 (file)
index 0000000..0a4c260
--- /dev/null
@@ -0,0 +1,16 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth, BootAuth
+
+class AuthCheck(Method):
+    """
+    Returns 1 if the user or node authenticated successfully, faults
+    otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+    accepts = [Auth()]
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth):
+        return 1
diff --git a/PLC/Methods/BlacklistKey.py b/PLC/Methods/BlacklistKey.py
new file mode 100644 (file)
index 0000000..7953e7a
--- /dev/null
@@ -0,0 +1,42 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Keys import Key, Keys
+from PLC.Auth import Auth
+
+class BlacklistKey(Method):
+    """
+    Blacklists a key, disassociating it and all others identical to it
+    from all accounts and preventing it from ever being added again.
+
+    WARNING: Identical keys associated with other accounts with also
+    be blacklisted.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Key.fields['key_id'],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+   
+    def call(self, auth, key_id):
+        # Get associated key details
+        keys = Keys(self.api, [key_id])
+        if not keys:
+            raise PLCInvalidArgument, "No such key"
+        key = keys[0]
+
+        # N.B.: Can blacklist any key, even foreign ones
+
+        key.blacklist()
+       
+       # Logging variables
+       self.event_objects = {'Key': [key['key_id']]}
+       self.message = 'Key %d blacklisted' % key['key_id']
+
+        return 1
diff --git a/PLC/Methods/BootCheckAuthentication.py b/PLC/Methods/BootCheckAuthentication.py
new file mode 100644 (file)
index 0000000..ea9b098
--- /dev/null
@@ -0,0 +1,8 @@
+from PLC.Methods.AuthCheck import AuthCheck
+
+class BootCheckAuthentication(AuthCheck):
+    """
+    Deprecated. See AuthCheck.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/BootGetNodeDetails.py b/PLC/Methods/BootGetNodeDetails.py
new file mode 100644 (file)
index 0000000..2f5056d
--- /dev/null
@@ -0,0 +1,55 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import BootAuth
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Sessions import Session, Sessions
+
+class BootGetNodeDetails(Method):
+    """
+    Returns a set of details about the calling node, including a new
+    node session value.
+    """
+
+    roles = ['node']
+
+    accepts = [BootAuth()]
+
+    returns = {
+        'hostname': Node.fields['hostname'],
+        'boot_state': Node.fields['boot_state'],
+        'model': Node.fields['model'],
+        'networks': [NodeNetwork.fields],
+        'session': Session.fields['session_id'],
+        }
+
+    def call(self, auth):
+        details = {
+            'hostname': self.caller['hostname'],
+            'boot_state': self.caller['boot_state'],
+            # XXX Boot Manager cannot unmarshal None
+            'model': self.caller['model'] or "",
+            }
+
+        # Generate a new session value
+        session = Session(self.api)
+        session.sync(commit = False)
+        session.add_node(self.caller, commit = True)
+
+        details['session'] = session['session_id']
+
+        if self.caller['nodenetwork_ids']:
+            details['networks'] = NodeNetworks(self.api, self.caller['nodenetwork_ids'])
+            # XXX Boot Manager cannot unmarshal None
+            for network in details['networks']:
+                for field in network:
+                    if network[field] is None:
+                        if isinstance(network[field], (int, long)):
+                            network[field] = -1
+                        else:
+                            network[field] = ""
+
+       self.messge = "Node request boot_state (%s) and networks" % \
+               (details['boot_state'])
+        return details
+
diff --git a/PLC/Methods/BootNotifyOwners.py b/PLC/Methods/BootNotifyOwners.py
new file mode 100644 (file)
index 0000000..81a7cb0
--- /dev/null
@@ -0,0 +1,32 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth, BootAuth, SessionAuth
+from PLC.Nodes import Node, Nodes
+from PLC.Messages import Message, Messages
+
+from PLC.Boot import notify_owners
+
+class BootNotifyOwners(Method):
+    """
+    Notify the owners of the node, and/or support about an event that
+    happened on the machine.
+
+    Returns 1 if successful.
+    """
+
+    roles = ['node']
+
+    accepts = [
+        Mixed(BootAuth(), SessionAuth()),
+        Message.fields['message_id'],
+        Parameter(int, "Notify PIs"),
+        Parameter(int, "Notify technical contacts"),
+        Parameter(int, "Notify support")
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, message_id, include_pis, include_techs, include_support):
+        assert isinstance(self.caller, Node)
+        notify_owners(self, self.caller, message_id, include_pis, include_techs, include_support)
+        return 1
diff --git a/PLC/Methods/BootUpdateNode.py b/PLC/Methods/BootUpdateNode.py
new file mode 100644 (file)
index 0000000..52381cb
--- /dev/null
@@ -0,0 +1,64 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth, BootAuth, SessionAuth
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+
+can_update = lambda (field, value): field in \
+             ['method', 'mac', 'gateway', 'network',
+              'broadcast', 'netmask', 'dns1', 'dns2']
+
+class BootUpdateNode(Method):
+    """
+    Allows the calling node to update its own record. Only the primary
+    network can be updated, and the node IP cannot be changed.
+
+    Returns 1 if updated successfully.
+    """
+
+    roles = ['node']
+
+    nodenetwork_fields = dict(filter(can_update, NodeNetwork.fields.items()))
+
+    accepts = [
+        Mixed(BootAuth(), SessionAuth()),
+        {'boot_state': Node.fields['boot_state'],
+         'primary_network': nodenetwork_fields,
+         'ssh_host_key': Node.fields['ssh_rsa_key']}
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, node_fields):
+        # Update node state
+        if node_fields.has_key('boot_state'):
+            self.caller['boot_state'] = node_fields['boot_state']
+        if node_fields.has_key('ssh_host_key'):
+            self.caller['ssh_rsa_key'] = node_fields['ssh_host_key']
+
+        # Update primary node network state
+        if node_fields.has_key('primary_network'):
+            primary_network = node_fields['primary_network'] 
+
+            if 'nodenetwork_id' not in primary_network:
+                raise PLCInvalidArgument, "Node network not specified"
+            if primary_network['nodenetwork_id'] not in self.caller['nodenetwork_ids']:
+                raise PLCInvalidArgument, "Node network not associated with calling node"
+
+            nodenetworks = NodeNetworks(self.api, [primary_network['nodenetwork_id']])
+            if not nodenetworks:
+                raise PLCInvalidArgument, "No such node network"
+            nodenetwork = nodenetworks[0]
+
+            if not nodenetwork['is_primary']:
+                raise PLCInvalidArgument, "Not the primary node network on record"
+
+            nodenetwork_fields = dict(filter(can_update, primary_network.items()))
+            nodenetwork.update(nodenetwork_fields)
+            nodenetwork.sync(commit = False)
+
+        self.caller.sync(commit = True)
+       self.message = "Node updated: %s" % ", ".join(node_fields.keys())
+
+        return 1
diff --git a/PLC/Methods/DeleteAddress.py b/PLC/Methods/DeleteAddress.py
new file mode 100644 (file)
index 0000000..d4f98bc
--- /dev/null
@@ -0,0 +1,43 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Addresses import Address, Addresses
+from PLC.Auth import Auth
+
+class DeleteAddress(Method):
+    """
+    Deletes an address.
+
+    PIs may only delete addresses from their own sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Address.fields['address_id'],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+
+    def call(self, auth, address_id):
+        # Get associated address details
+        addresses = Addresses(self.api, [address_id])
+        if not addresses:
+            raise PLCInvalidArgument, "No such address"
+        address = addresses[0]
+
+        if 'admin' not in self.caller['roles']:
+            if address['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Address must be associated with one of your sites"
+
+        address.delete()
+
+       # Logging variables
+        self.event_objects = {'Address': [address['address_id']]}
+       self.message = 'Address %d deleted' % address['address_id']     
+
+        return 1
diff --git a/PLC/Methods/DeleteAddressType.py b/PLC/Methods/DeleteAddressType.py
new file mode 100644 (file)
index 0000000..4fd1d9b
--- /dev/null
@@ -0,0 +1,33 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.AddressTypes import AddressType, AddressTypes
+from PLC.Auth import Auth
+
+class DeleteAddressType(Method):
+    """
+    Deletes an address type.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(AddressType.fields['address_type_id'],
+              AddressType.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, address_type_id_or_name):
+        address_types = AddressTypes(self.api, [address_type_id_or_name])
+        if not address_types:
+            raise PLCInvalidArgument, "No such address type"
+        address_type = address_types[0]
+        address_type.delete()
+       self.event_objects = {'AddressType': [address_type['address_type_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteAddressTypeFromAddress.py b/PLC/Methods/DeleteAddressTypeFromAddress.py
new file mode 100644 (file)
index 0000000..d4ea928
--- /dev/null
@@ -0,0 +1,48 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.AddressTypes import AddressType, AddressTypes
+from PLC.Addresses import Address, Addresses
+from PLC.Auth import Auth
+
+class DeleteAddressTypeFromAddress(Method):
+    """
+    Deletes an address type from the specified address.
+
+    PIs may only update addresses of their own sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Mixed(AddressType.fields['address_type_id'],
+              AddressType.fields['name']),
+        Address.fields['address_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+
+    def call(self, auth, address_type_id_or_name, address_id):
+        address_types = AddressTypes(self.api, [address_type_id_or_name])
+        if not address_types:
+            raise PLCInvalidArgument, "No such address type"
+        address_type = address_types[0]
+
+        addresses = Addresses(self.api, [address_id])
+        if not addresses:
+            raise PLCInvalidArgument, "No such address"
+        address = addresses[0]
+
+        if 'admin' not in self.caller['roles']:
+            if address['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Address must be associated with one of your sites"
+
+        address.remove_address_type(address_type)
+       self.event_objects = {'Address' : [address['address_id']],
+                             'AddressType': [address_type['address_type_id']]} 
+
+        return 1
diff --git a/PLC/Methods/DeleteBootState.py b/PLC/Methods/DeleteBootState.py
new file mode 100644 (file)
index 0000000..507fc7b
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.BootStates import BootState, BootStates
+from PLC.Auth import Auth
+
+class DeleteBootState(Method):
+    """
+    Deletes a node boot state.
+
+    WARNING: This will cause the deletion of all nodes in this boot
+    state.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        BootState.fields['boot_state']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, name):
+        boot_states = BootStates(self.api, [name])
+        if not boot_states:
+            raise PLCInvalidArgument, "No such boot state"
+        boot_state = boot_states[0]
+
+        boot_state.delete()
+       
+        return 1
diff --git a/PLC/Methods/DeleteConfFile.py b/PLC/Methods/DeleteConfFile.py
new file mode 100644 (file)
index 0000000..f05ae43
--- /dev/null
@@ -0,0 +1,33 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Auth import Auth
+
+class DeleteConfFile(Method):
+    """
+    Returns an array of structs containing details about node
+    configuration files. If conf_file_ids is specified, only the
+    specified configuration files will be queried.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        ConfFile.fields['conf_file_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+
+    def call(self, auth, conf_file_id):
+        conf_files = ConfFiles(self.api, [conf_file_id])
+        if not conf_files:
+            raise PLCInvalidArgument, "No such configuration file"
+
+        conf_file = conf_files[0]
+        conf_file.delete()
+       self.event_objects = {'ConfFile': [conf_file['conf_file_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteConfFileFromNode.py b/PLC/Methods/DeleteConfFileFromNode.py
new file mode 100644 (file)
index 0000000..50b08e6
--- /dev/null
@@ -0,0 +1,48 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Nodes import Node, Nodes
+from PLC.Auth import Auth
+
+class DeleteConfFileFromNode(Method):
+    """
+    Deletes a configuration file from the specified node. If the node
+    is not linked to the configuration file, no errors are returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        ConfFile.fields['conf_file_id'],
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, conf_file_id, node_id_or_hostname):
+       # Get configuration file
+        conf_files = ConfFiles(self.api, [conf_file_id])
+        if not conf_files:
+            raise PLCInvalidArgument, "No such configuration file"
+        conf_file = conf_files[0]
+
+        # Get node
+       nodes = Nodes(self.api, [node_id_or_hostname])
+       if not nodes:
+               raise PLCInvalidArgument, "No such node"
+       node = nodes[0]
+       
+       # Link configuration file to node
+        if node['node_id'] in conf_file['node_ids']:
+            conf_file.remove_node(node)
+
+        # Log affected objects
+        self.event_objects = {'ConfFile': [conf_file_id], 
+                             'Node': [node['node_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteConfFileFromNodeGroup.py b/PLC/Methods/DeleteConfFileFromNodeGroup.py
new file mode 100644 (file)
index 0000000..5504b0f
--- /dev/null
@@ -0,0 +1,49 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Auth import Auth
+
+class DeleteConfFileFromNodeGroup(Method):
+    """
+    Deletes a configuration file from the specified nodegroup. If the nodegroup
+    is not linked to the configuration file, no errors are returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        ConfFile.fields['conf_file_id'],
+        Mixed(NodeGroup.fields['nodegroup_id'],
+              NodeGroup.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, conf_file_id, nodegroup_id_or_name):
+       # Get configuration file
+        conf_files = ConfFiles(self.api, [conf_file_id])
+        if not conf_files:
+            raise PLCInvalidArgument, "No such configuration file"
+        conf_file = conf_files[0]
+
+        # Get nodegroup
+       nodegroups = NodeGroups(self.api, [nodegroup_id_or_name])
+       if not nodegroups:
+               raise PLCInvalidArgument, "No such nodegroup"
+       nodegroup = nodegroups[0]
+       
+       # Link configuration file to nodegroup
+        if nodegroup['nodegroup_id'] in conf_file['nodegroup_ids']:
+            conf_file.remove_nodegroup(nodegroup)
+
+        # Log affected objects
+        self.event_objects = {'ConfFile': [conf_file_id], 
+                             'NodeGroup': [nodegroup['nodegroup_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteInitScript.py b/PLC/Methods/DeleteInitScript.py
new file mode 100644 (file)
index 0000000..47a9993
--- /dev/null
@@ -0,0 +1,33 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.InitScripts import InitScript, InitScripts
+from PLC.Auth import Auth
+
+class DeleteInitScript(Method):
+    """
+    Deletes an existing initscript.  
+    
+    Returns 1 if successfuli, faults otherwise. 
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        InitScript.fields['initscript_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+
+    def call(self, auth, initscript_id):
+        initscripts = InitScripts(self.api, [initscript_id])
+        if not initscripts:
+            raise PLCInvalidArgument, "No such initscript"
+
+        initscript = initscripts[0]
+        initscript.delete()
+       self.event_objects = {'InitScript':  [initscript['initscript_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteKey.py b/PLC/Methods/DeleteKey.py
new file mode 100644 (file)
index 0000000..86c16a5
--- /dev/null
@@ -0,0 +1,46 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Keys import Key, Keys
+from PLC.Auth import Auth
+
+class DeleteKey(Method):
+    """
+    Deletes a key.
+
+    Non-admins may only delete their own keys.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    accepts = [
+        Auth(),
+        Key.fields['key_id'],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, key_id):
+        # Get associated key details
+        keys = Keys(self.api, [key_id])
+        if not keys:
+            raise PLCInvalidArgument, "No such key"
+        key = keys[0]
+
+        if key['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local key"
+
+        if 'admin' not in self.caller['roles']:
+            if key['key_id'] not in self.caller['key_ids']:
+                raise PLCPermissionDenied, "Key must be associated with your account"
+
+        key.delete()
+       
+       # Logging variables
+       self.event_objects = {'Key': [key['key_id']]}
+       self.message = 'Key %d deleted' % key['key_id']
+
+        return 1
diff --git a/PLC/Methods/DeleteKeyType.py b/PLC/Methods/DeleteKeyType.py
new file mode 100644 (file)
index 0000000..e09e5c5
--- /dev/null
@@ -0,0 +1,34 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.KeyTypes import KeyType, KeyTypes
+from PLC.Auth import Auth
+
+class DeleteKeyType(Method):
+    """
+    Deletes a key type.
+
+    WARNING: This will cause the deletion of all keys of this type.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        KeyType.fields['key_type']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+    
+    def call(self, auth, name):
+        key_types = KeyTypes(self.api, [name])
+        if not key_types:
+            raise PLCInvalidArgument, "No such key type"
+        key_type = key_types[0]
+
+        key_type.delete()
+
+        return 1
diff --git a/PLC/Methods/DeleteMessage.py b/PLC/Methods/DeleteMessage.py
new file mode 100644 (file)
index 0000000..4989942
--- /dev/null
@@ -0,0 +1,34 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Messages import Message, Messages
+from PLC.Auth import Auth
+
+class DeleteMessage(Method):
+    """
+    Deletes a message template.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Message.fields['message_id'],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+       
+
+    def call(self, auth, message_id):
+        # Get message information
+        messages = Messages(self.api, [message_id])
+        if not messages:
+            raise PLCInvalidArgument, "No such message"
+        message = messages[0]
+
+        message.delete()
+       self.event_objects = {'Message': [message['message_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteNetworkMethod.py b/PLC/Methods/DeleteNetworkMethod.py
new file mode 100644 (file)
index 0000000..d0f982e
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NetworkMethods import NetworkMethod, NetworkMethods
+from PLC.Auth import Auth
+
+class DeleteNetworkMethod(Method):
+    """
+    Deletes a network method.
+
+    WARNING: This will cause the deletion of all network interfaces
+    that use this method.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        NetworkMethod.fields['method']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+   
+
+    def call(self, auth, name):
+        network_methods = NetworkMethods(self.api, [name])
+        if not network_methods:
+            raise PLCInvalidArgument, "No such network method"
+        network_method = network_methods[0]
+
+        network_method.delete()
+
+        return 1
diff --git a/PLC/Methods/DeleteNetworkType.py b/PLC/Methods/DeleteNetworkType.py
new file mode 100644 (file)
index 0000000..a02f6e0
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NetworkTypes import NetworkType, NetworkTypes
+from PLC.Auth import Auth
+
+class DeleteNetworkType(Method):
+    """
+    Deletes a network type.
+
+    WARNING: This will cause the deletion of all network interfaces
+    that use this type.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        NetworkType.fields['type']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+
+    def call(self, auth, name):
+        network_types = NetworkTypes(self.api, [name])
+        if not network_types:
+            raise PLCInvalidArgument, "No such network type"
+        network_type = network_types[0]
+
+        network_type.delete()
+
+        return 1
diff --git a/PLC/Methods/DeleteNode.py b/PLC/Methods/DeleteNode.py
new file mode 100644 (file)
index 0000000..bc92718
--- /dev/null
@@ -0,0 +1,52 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Nodes import Node, Nodes
+
+class DeleteNode(Method):
+    """
+    Mark an existing node as deleted.
+
+    PIs and techs may only delete nodes at their own sites. ins may
+    delete nodes at any site.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        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[0]
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+
+        # 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()
+
+       # Logging variables
+       self.event_objects = {'Node': [node['node_id']]}
+       self.message = "Node %d deleted" % node['node_id']
+
+        return 1
diff --git a/PLC/Methods/DeleteNodeFromNodeGroup.py b/PLC/Methods/DeleteNodeFromNodeGroup.py
new file mode 100644 (file)
index 0000000..2bc6770
--- /dev/null
@@ -0,0 +1,53 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Nodes import Node, Nodes
+from PLC.Auth import Auth
+
+class DeleteNodeFromNodeGroup(Method):
+    """
+    Removes a node from the specified node group. 
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+       Mixed(Node.fields['node_id'],
+             Node.fields['hostname']),
+        Mixed(NodeGroup.fields['nodegroup_id'],
+             NodeGroup.fields['name']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, node_id_or_hostname, nodegroup_id_or_name):
+        # Get node info
+       nodes = Nodes(self.api, [node_id_or_hostname])
+       if not nodes:
+               raise PLCInvalidArgument, "No such node"
+
+       node = nodes[0]
+
+       # Get nodegroup info
+        nodegroups = NodeGroups(self.api, [nodegroup_id_or_name])
+        if not nodegroups:
+            raise PLCInvalidArgument, "No such nodegroup"
+
+        nodegroup = nodegroups[0]
+
+       # Remove node from nodegroup
+        if node['node_id'] in nodegroup['node_ids']:
+            nodegroup.remove_node(node)
+       
+       # Logging variables
+       self.event_objects = {'NodeGroup': [nodegroup['nodegroup_id']],
+                             'Node': [node['node_id']]}
+       self.message = 'node %d deleted from node group %d' % \
+               (node['node_id'], nodegroup['nodegroup_id'])
+
+        return 1
diff --git a/PLC/Methods/DeleteNodeFromPCU.py b/PLC/Methods/DeleteNodeFromPCU.py
new file mode 100644 (file)
index 0000000..8e728ef
--- /dev/null
@@ -0,0 +1,65 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.PCUs import PCU, PCUs
+from PLC.Sites import Site, Sites
+from PLC.Auth import Auth
+
+class DeleteNodeFromPCU(Method):
+    """
+    Deletes a node from a PCU.
+
+    Non-admins may only update PCUs at their sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+       Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+        PCU.fields['pcu_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, node_id_or_hostname, pcu_id):
+        # Get node
+        nodes = Nodes(self.api, [node_id_or_hostname])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node"
+
+        node = nodes[0]
+
+        # Get PCU
+        pcus = PCUs(self.api, [pcu_id])
+        if not pcus:
+            raise PLCInvalidArgument, "No such PCU"
+
+        pcu = pcus[0]
+
+        if 'admin' not in self.caller['roles']:
+            ok = False
+            sites = Sites(self.api, self.caller['site_ids'])
+            for site in sites:
+                if pcu['pcu_id'] in site['pcu_ids']:
+                    ok = True
+                    break
+            if not ok:
+                raise PLCPermissionDenied, "Not allowed to update that PCU"
+       
+       # Removed node from PCU
+       
+        if node['node_id'] in pcu['node_ids']:
+            pcu.remove_node(node)
+
+       # Logging variables
+       self.event_objects = {'PCU': [pcu['pcu_id']],
+                             'Node': [node['node_id']]}        
+       self.message = 'Node %d removed from PCU %d' % \
+               (node['node_id'], pcu['pcu_id']) 
+       
+       return 1
diff --git a/PLC/Methods/DeleteNodeGroup.py b/PLC/Methods/DeleteNodeGroup.py
new file mode 100644 (file)
index 0000000..7650150
--- /dev/null
@@ -0,0 +1,41 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.NodeGroups import NodeGroup, NodeGroups
+
+class DeleteNodeGroup(Method):
+    """
+    Delete an existing Node Group.
+
+    ins may delete any node group
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(NodeGroup.fields['nodegroup_id'],
+             NodeGroup.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, node_group_id_or_name):
+        # Get account information
+        nodegroups = NodeGroups(self.api, [node_group_id_or_name])
+        if not nodegroups:
+            raise PLCInvalidArgument, "No such node group"
+
+        nodegroup = nodegroups[0]
+
+        nodegroup.delete()
+
+       # Logging variables
+       self.event_objects = {'NodeGroup': [nodegroup['nodegroup_id']]}
+       self.message  = 'Node group %d deleted' % nodegroup['nodegroup_id']
+        return 1
diff --git a/PLC/Methods/DeleteNodeNetwork.py b/PLC/Methods/DeleteNodeNetwork.py
new file mode 100644 (file)
index 0000000..4bdda21
--- /dev/null
@@ -0,0 +1,57 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+
+class DeleteNodeNetwork(Method):
+    """
+    Deletes an existing node network interface.
+
+    Admins may delete any node network. PIs and techs may only delete
+    node network interfaces associated with nodes at their sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+       NodeNetwork.fields['nodenetwork_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, nodenetwork_id):
+
+        # Get node network information
+        nodenetworks = NodeNetworks(self.api, [nodenetwork_id])
+        if not nodenetworks:
+            raise PLCInvalidArgument, "No such node network"
+       nodenetwork = nodenetworks[0]
+       
+       # Get node information
+       nodes = Nodes(self.api, [nodenetwork['node_id']])
+       if not nodes:
+               raise PLCInvalidArgument, "No such node"
+       node = nodes[0]
+
+        # Authenticated functino
+       assert self.caller is not None
+
+        # 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']:
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to delete this node network"
+
+        nodenetwork.delete()
+
+       # Logging variables
+       self.event_objects = {'NodeNetwork': [nodenetwork['nodenetwork_id']]}
+       self.message = "Node network %d deleted" % nodenetwork['nodenetwork_id']
+
+        return 1
diff --git a/PLC/Methods/DeleteNodeNetworkSetting.py b/PLC/Methods/DeleteNodeNetworkSetting.py
new file mode 100644 (file)
index 0000000..e092f37
--- /dev/null
@@ -0,0 +1,73 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.NodeNetworkSettings import NodeNetworkSetting, NodeNetworkSettings
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+
+from PLC.Nodes import Node, Nodes
+from PLC.Sites import Site, Sites
+
+class DeleteNodeNetworkSetting(Method):
+    """
+    Deletes the specified nodenetwork setting
+
+    Attributes may require the caller to have a particular role in order
+    to be deleted, depending on the related nodenetwork setting type.
+    Admins may delete attributes of any slice or sliver.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        NodeNetworkSetting.fields['nodenetwork_setting_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    object_type = 'NodeNetwork'
+
+
+    def call(self, auth, nodenetwork_setting_id):
+        nodenetwork_settings = NodeNetworkSettings(self.api, [nodenetwork_setting_id])
+        if not nodenetwork_settings:
+            raise PLCInvalidArgument, "No such nodenetwork setting %r"%nodenetwork_setting_id
+        nodenetwork_setting = nodenetwork_settings[0]
+
+        ### reproducing a check from UpdateSliceAttribute, looks dumb though
+        nodenetworks = NodeNetworks(self.api, [nodenetwork_setting['nodenetwork_id']])
+        if not nodenetworks:
+            raise PLCInvalidArgument, "No such nodenetwork %r"%nodenetwork_setting['nodenetwork_id']
+        nodenetwork = nodenetworks[0]
+
+        assert nodenetwork_setting['nodenetwork_setting_id'] in nodenetwork['nodenetwork_setting_ids']
+
+       # check permission : it not admin, is the user affiliated with the right site
+       if 'admin' not in self.caller['roles']:
+           # locate node
+           node = Nodes (self.api,[nodenetwork['node_id']])[0]
+           # locate site
+           site = Sites (self.api, [node['site_id']])[0]
+           # check caller is affiliated with this site
+           if self.caller['person_id'] not in site['person_ids']:
+               raise PLCPermissionDenied, "Not a member of the hosting site %s"%site['abbreviated_site']
+           
+           required_min_role = nodenetwork_setting_type ['min_role_id']
+           if required_min_role is not None and \
+                   min(self.caller['role_ids']) > required_min_role:
+               raise PLCPermissionDenied, "Not allowed to modify the specified nodenetwork setting, requires role %d",required_min_role
+
+        nodenetwork_setting.delete()
+       self.object_ids = [nodenetwork_setting['nodenetwork_setting_id']]
+
+        return 1
diff --git a/PLC/Methods/DeleteNodeNetworkSettingType.py b/PLC/Methods/DeleteNodeNetworkSettingType.py
new file mode 100644 (file)
index 0000000..c1ae76b
--- /dev/null
@@ -0,0 +1,39 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 7365 $
+#
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeNetworkSettingTypes import NodeNetworkSettingType, NodeNetworkSettingTypes
+from PLC.Auth import Auth
+
+class DeleteNodeNetworkSettingType(Method):
+    """
+    Deletes the specified nodenetwork setting type.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(NodeNetworkSettingType.fields['nodenetwork_setting_type_id'],
+              NodeNetworkSettingType.fields['name']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, nodenetwork_setting_type_id_or_name):
+        nodenetwork_setting_types = NodeNetworkSettingTypes(self.api, [nodenetwork_setting_type_id_or_name])
+        if not nodenetwork_setting_types:
+            raise PLCInvalidArgument, "No such nodenetwork setting type"
+        nodenetwork_setting_type = nodenetwork_setting_types[0]
+
+        nodenetwork_setting_type.delete()
+       self.object_ids = [nodenetwork_setting_type['nodenetwork_setting_type_id']]
+
+        return 1
diff --git a/PLC/Methods/DeletePCU.py b/PLC/Methods/DeletePCU.py
new file mode 100644 (file)
index 0000000..944882b
--- /dev/null
@@ -0,0 +1,43 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUs import PCU, PCUs
+from PLC.Auth import Auth
+
+class DeletePCU(Method):
+    """
+    Deletes a PCU.
+
+    Non-admins may only delete PCUs at their sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        PCU.fields['pcu_id'],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, pcu_id):
+        # Get associated PCU details
+        pcus = PCUs(self.api, [pcu_id])
+        if not pcus:
+            raise PLCInvalidArgument, "No such PCU"
+        pcu = pcus[0]
+
+        if 'admin' not in self.caller['roles']:
+            if pcu['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to update that PCU"
+
+        pcu.delete()
+       
+       # Logging variables
+       self.event_objects = {'PCU': [pcu['pcu_id']]}
+       self.message = 'PCU %d deleted' % pcu['pcu_id']
+
+        return 1
diff --git a/PLC/Methods/DeletePCUProtocolType.py b/PLC/Methods/DeletePCUProtocolType.py
new file mode 100644 (file)
index 0000000..ab66520
--- /dev/null
@@ -0,0 +1,33 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUProtocolTypes import PCUProtocolType, PCUProtocolTypes
+from PLC.Auth import Auth
+
+class DeletePCUProtocolType(Method):
+    """
+    Deletes a PCU protocol type.
+
+    Returns 1 if successful, faults otherwise. 
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        PCUProtocolType.fields['pcu_protocol_type_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+
+    def call(self, auth, protocol_type_id):
+        protocol_types = PCUProtocolTypes(self.api, [protocol_type_id])
+        if not protocol_types:
+            raise PLCInvalidArgument, "No such pcu protocol type"
+
+        protocol_type = protocol_types[0]
+        protocol_type.delete()
+       self.event_objects = {'PCUProtocolType': [protocol_type['pcu_protocol_type_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeletePCUType.py b/PLC/Methods/DeletePCUType.py
new file mode 100644 (file)
index 0000000..d73c204
--- /dev/null
@@ -0,0 +1,33 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUTypes import PCUType, PCUTypes
+from PLC.Auth import Auth
+
+class DeletePCUType(Method):
+    """
+    Deletes a PCU type.
+
+    Returns 1 if successful, faults otherwise. 
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        PCUType.fields['pcu_type_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+    
+
+    def call(self, auth, pcu_type_id):
+        pcu_types = PCUTypes(self.api, [pcu_type_id])
+        if not pcu_types:
+            raise PLCInvalidArgument, "No such pcu type"
+
+        pcu_type = pcu_types[0]
+        pcu_type.delete()
+       self.event_objects = {'PCUType': [pcu_type['pcu_type_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeletePeer.py b/PLC/Methods/DeletePeer.py
new file mode 100644 (file)
index 0000000..4260d25
--- /dev/null
@@ -0,0 +1,38 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Peers import Peer, Peers
+
+class DeletePeer(Method):
+    """
+    Mark an existing peer as deleted. All entities (e.g., slices,
+    keys, nodes, etc.) for which this peer is authoritative will also
+    be deleted or marked as deleted.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Peer.fields['peer_id'],
+              Peer.fields['peername'])
+        ]
+
+    returns = Parameter(int, "1 if successful")
+
+    def call(self, auth, peer_id_or_name):
+        # Get account information
+        peers = Peers(self.api, [peer_id_or_name])
+        if not peers:
+            raise PLCInvalidArgument, "No such peer"
+
+        peer = peers[0]
+        peer.delete()
+
+        # Log affected objects
+       self.event_objects = {'Peer': [peer['peer_id']]}
+
+       return 1
diff --git a/PLC/Methods/DeletePerson.py b/PLC/Methods/DeletePerson.py
new file mode 100644 (file)
index 0000000..448f808
--- /dev/null
@@ -0,0 +1,51 @@
+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 Auth
+
+class DeletePerson(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. ins can delete
+    anyone.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech']
+
+    accepts = [
+        Auth(),
+        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[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        # 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()
+       
+       # Logging variables
+       self.event_objects = {'Person': [person['person_id']]}
+       self.message = 'Person %d deleted' % person['person_id']
+
+        return 1
diff --git a/PLC/Methods/DeletePersonFromSite.py b/PLC/Methods/DeletePersonFromSite.py
new file mode 100644 (file)
index 0000000..db2af2d
--- /dev/null
@@ -0,0 +1,56 @@
+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 Auth
+
+class DeletePersonFromSite(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 = [
+        Auth(),
+        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[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        # Get site information
+        sites = Sites(self.api, [site_id_or_login_base])
+        if not sites:
+            raise PLCInvalidArgument, "No such site"
+        site = sites[0]
+
+        if site['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local site"
+
+        if site['site_id'] in person['site_ids']:
+            site.remove_person(person)
+
+       # Logging variables
+       self.event_objects = {'Site': [site['site_id']],
+                             'Person': [person['person_id']]}  
+       self.message = 'Person %d deleted from site %d  ' % \
+               (person['person_id'], site['site_id'])
+        return 1
diff --git a/PLC/Methods/DeletePersonFromSlice.py b/PLC/Methods/DeletePersonFromSlice.py
new file mode 100644 (file)
index 0000000..c990ccd
--- /dev/null
@@ -0,0 +1,59 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+
+class DeletePersonFromSlice(Method):
+    """
+    Deletes the specified person from the specified slice. If the person is
+    not a member of the slice, no errors are returned. 
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, person_id_or_email, slice_id_or_name):
+        # Get account information
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        # Get slice information
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        # N.B. Allow foreign users to be added to local slices and
+        # local users to be added to foreign slices (and, of course,
+        # local users to be added to local slices).
+        if person['peer_id'] is not None and slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Cannot delete foreign users from foreign slices"
+
+        # If we are not admin, make sure the caller is a pi
+        # of the site associated with the slice
+       if 'admin' not in self.caller['roles']:
+               if slice['site_id'] not in self.caller['site_ids']:
+                       raise PLCPermissionDenied, "Not allowed to delete users from this slice"
+
+       if slice['slice_id'] in person['slice_ids']:
+            slice.remove_person(person)
+       
+       self.event_objects = {'Slice': [slice['slice_id']],
+                             'Person': [person['person_id']]}  
+
+        return 1
diff --git a/PLC/Methods/DeleteRole.py b/PLC/Methods/DeleteRole.py
new file mode 100644 (file)
index 0000000..f707280
--- /dev/null
@@ -0,0 +1,38 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Roles import Role, Roles
+from PLC.Auth import Auth
+
+class DeleteRole(Method):
+    """
+    Deletes a role.
+
+    WARNING: This will remove the specified role from all accounts
+    that possess it, and from all node and slice attributes that refer
+    to it.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Role.fields['role_id'],
+              Role.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    
+    def call(self, auth, role_id_or_name):
+        roles = Roles(self.api, [role_id_or_name])
+        if not roles:
+            raise PLCInvalidArgument, "No such role"
+        role = roles[0]
+
+        role.delete()
+       self.event_objects = {'Role': [role['role_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteRoleFromPerson.py b/PLC/Methods/DeleteRoleFromPerson.py
new file mode 100644 (file)
index 0000000..151ba25
--- /dev/null
@@ -0,0 +1,67 @@
+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 Auth
+from PLC.Roles import Role, Roles
+
+class DeleteRoleFromPerson(Method):
+    """
+    Deletes the specified role from the person.
+    
+    PIs can only revoke the tech and user roles from users and techs
+    at their sites. ins can revoke any role from any user.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Mixed(Role.fields['role_id'],
+              Role.fields['name']),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, role_id_or_name, person_id_or_email):
+        # Get role
+        roles = Roles(self.api, [role_id_or_name])
+        if not roles:
+            raise PLCInvalidArgument, "Invalid role '%s'" % unicode(role_id_or_name)
+        role = roles[0]
+
+        # Get account information
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        # 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['role_id'] <= min(self.caller['role_ids']):
+            raise PLCPermissionDenied, "Not allowed to revoke that role"
+
+        if role['role_id'] in person['role_ids']:
+            person.remove_role(role)
+       
+       # Logging variables
+       self.event_objects = {'Person': [person['person_id']],
+                             'Role': [role['role_id']]}        
+       self.message = "Role %d revoked from person %d" % \
+                       (role['role_id'], person['person_id'])
+
+        return 1
diff --git a/PLC/Methods/DeleteSession.py b/PLC/Methods/DeleteSession.py
new file mode 100644 (file)
index 0000000..3898f51
--- /dev/null
@@ -0,0 +1,30 @@
+import time
+
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import SessionAuth
+from PLC.Sessions import Session, Sessions
+
+class DeleteSession(Method):
+    """
+    Invalidates the current session.
+
+    Returns 1 if successful.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+    accepts = [SessionAuth()]
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth):
+        assert auth.has_key('session')
+
+        sessions = Sessions(self.api, [auth['session']])
+        if not sessions:
+            raise PLCAPIError, "No such session"
+        session = sessions[0]
+
+        session.delete()
+
+        return 1
diff --git a/PLC/Methods/DeleteSite.py b/PLC/Methods/DeleteSite.py
new file mode 100644 (file)
index 0000000..c23fff4
--- /dev/null
@@ -0,0 +1,46 @@
+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 Auth
+
+class DeleteSite(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 = [
+        Auth(),
+        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[0]
+
+        if site['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local site"
+
+        site.delete()
+       
+       # Logging variables
+       self.event_objects = {'Site': [site['site_id']]}
+       self.message = 'Site %d deleted' % site['site_id']      
+
+        return 1
diff --git a/PLC/Methods/DeleteSlice.py b/PLC/Methods/DeleteSlice.py
new file mode 100644 (file)
index 0000000..297f8a9
--- /dev/null
@@ -0,0 +1,48 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+
+class DeleteSlice(Method):
+    """
+    Deletes the specified slice.
+
+    Users may only delete slices of which they are members. PIs may
+    delete any of the slices at their sites, or any slices of which
+    they are members. Admins may delete any slice.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_id_or_name):
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        if slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local slice"
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+
+        slice.delete()
+        self.event_objects = {'Slice': [slice['slice_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteSliceAttribute.py b/PLC/Methods/DeleteSliceAttribute.py
new file mode 100644 (file)
index 0000000..06a99f3
--- /dev/null
@@ -0,0 +1,59 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+from PLC.Slices import Slice, Slices
+from PLC.Nodes import Node, Nodes
+from PLC.Auth import Auth
+
+class DeleteSliceAttribute(Method):
+    """
+    Deletes the specified slice or sliver attribute.
+
+    Attributes may require the caller to have a particular role in
+    order to be deleted. Users may only delete attributes of
+    slices or slivers of which they are members. PIs may only delete
+    attributes of slices or slivers at their sites, or of which they
+    are members. Admins may delete attributes of any slice or sliver.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        SliceAttribute.fields['slice_attribute_id']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_attribute_id):
+        slice_attributes = SliceAttributes(self.api, [slice_attribute_id])
+        if not slice_attributes:
+            raise PLCInvalidArgument, "No such slice attribute"
+        slice_attribute = slice_attributes[0]
+
+        slices = Slices(self.api, [slice_attribute['slice_id']])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        assert slice_attribute['slice_attribute_id'] in slice['slice_attribute_ids']
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+
+            if slice_attribute['min_role_id'] is not None and \
+               min(self.caller['role_ids']) > slice_attribute['min_role_id']:
+                raise PLCPermissioinDenied, "Not allowed to delete the specified attribute"
+
+        slice_attribute.delete()
+       self.event_objects = {'SliceAttribute': [slice_attribute['slice_attribute_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteSliceAttributeType.py b/PLC/Methods/DeleteSliceAttributeType.py
new file mode 100644 (file)
index 0000000..e6c1a8a
--- /dev/null
@@ -0,0 +1,34 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceAttributeTypes import SliceAttributeType, SliceAttributeTypes
+from PLC.Auth import Auth
+
+class DeleteSliceAttributeType(Method):
+    """
+    Deletes the specified slice attribute.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(SliceAttributeType.fields['attribute_type_id'],
+              SliceAttributeType.fields['name']),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, attribute_type_id_or_name):
+        attribute_types = SliceAttributeTypes(self.api, [attribute_type_id_or_name])
+        if not attribute_types:
+            raise PLCInvalidArgument, "No such slice attribute type"
+        attribute_type = attribute_types[0]
+
+        attribute_type.delete()
+       self.event_objects = {'AttributeType': [attribute_type['attribute_type_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteSliceFromNodes.py b/PLC/Methods/DeleteSliceFromNodes.py
new file mode 100644 (file)
index 0000000..2390be5
--- /dev/null
@@ -0,0 +1,58 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+
+class DeleteSliceFromNodes(Method):
+    """
+    Deletes the specified slice from the specified nodes. If the slice is
+    not associated with a node, no errors are returned. 
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+       Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+       [Mixed(Node.fields['node_id'],
+               Node.fields['hostname'])]
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_id_or_name, node_id_or_hostname_list):
+        # Get slice information
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+       
+       # Remove slice from all nodes found
+
+       # Get specified nodes
+        nodes = Nodes(self.api, node_id_or_hostname_list)
+       for node in nodes:
+           if slice['peer_id'] is not None and node['peer_id'] is not None:
+               raise PLCPermissionDenied, "Not allowed to remove peer slice from peer node"
+            if slice['slice_id'] in node['slice_ids']:
+                slice.remove_node(node, commit = False)
+
+        slice.sync()
+       
+       self.event_objects = {'Node': [node['node_id'] for node in nodes],
+                             'Slice': [slice['slice_id']]}      
+
+        return 1
diff --git a/PLC/Methods/DeleteSliceFromNodesWhitelist.py b/PLC/Methods/DeleteSliceFromNodesWhitelist.py
new file mode 100644 (file)
index 0000000..8899d88
--- /dev/null
@@ -0,0 +1,54 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+
+class DeleteSliceFromNodesWhitelist(Method):
+    """
+    Deletes the specified slice from the whitelist on the specified nodes. Nodes may be
+    either local or foreign nodes.
+
+    If the slice is already associated with a node, no errors are
+    returned.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+       [Mixed(Node.fields['node_id'],
+               Node.fields['hostname'])]
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_id_or_name, node_id_or_hostname_list):
+        # Get slice information
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        if slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local slice"
+
+        # Get specified nodes, add them to the slice         
+        nodes = Nodes(self.api, node_id_or_hostname_list)
+       for node in nodes:
+            if node['peer_id'] is not None:
+                raise PLCInvalidArgument, "%s not a local node" % node['hostname']
+           if slice['slice_id'] in node['slice_ids_whitelist']:
+                slice.delete_from_node_whitelist(node, commit = False)
+
+        slice.sync()
+
+       self.event_objects = {'Node': [node['node_id'] for node in nodes],
+                             'Slice': [slice['slice_id']]}
+
+        return 1
diff --git a/PLC/Methods/DeleteSliceInstantiation.py b/PLC/Methods/DeleteSliceInstantiation.py
new file mode 100644 (file)
index 0000000..5098a9d
--- /dev/null
@@ -0,0 +1,34 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceInstantiations import SliceInstantiation, SliceInstantiations
+from PLC.Auth import Auth
+
+class DeleteSliceInstantiation(Method):
+    """
+    Deletes a slice instantiation state.
+
+    WARNING: This will cause the deletion of all slices of this instantiation.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        SliceInstantiation.fields['instantiation']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, instantiation):
+        slice_instantiations = SliceInstantiations(self.api, [instantiation])
+        if not slice_instantiations:
+            raise PLCInvalidArgument, "No such slice instantiation state"
+        slice_instantiation = slice_instantiations[0]
+
+        slice_instantiation.delete()
+
+        return 1
diff --git a/PLC/Methods/GenerateNodeConfFile.py b/PLC/Methods/GenerateNodeConfFile.py
new file mode 100644 (file)
index 0000000..0b5cf8e
--- /dev/null
@@ -0,0 +1,107 @@
+import random
+import base64
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+
+class GenerateNodeConfFile(Method):
+    """
+    Creates a new node configuration file if all network settings are
+    present. This function will generate a new node key for the
+    specified node, effectively invalidating any old configuration
+    files.
+
+    Non-admins can only generate files for nodes at their sites.
+
+    Returns the contents of the file if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+       Parameter(bool, "True if you want to regenerate node key")
+        ]
+
+    returns = Parameter(str, "Node configuration file")
+
+    def call(self, auth, node_id_or_hostname, regenerate_node_key = True):
+        # Get node information
+        nodes = Nodes(self.api, [node_id_or_hostname])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node"
+        node = nodes[0]
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+
+        # 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']:
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to generate a configuration file for that node"
+
+       # Get node networks for this node
+        primary = None
+        nodenetworks = NodeNetworks(self.api, node['nodenetwork_ids'])
+        for nodenetwork in nodenetworks:
+            if nodenetwork['is_primary']:
+                primary = nodenetwork
+                break
+        if primary is None:
+            raise PLCInvalidArgument, "No primary network configured"
+
+        # Split hostname into host and domain parts
+        parts = node['hostname'].split(".", 1)
+        if len(parts) < 2:
+            raise PLCInvalidArgument, "Node hostname is invalid"
+        host = parts[0]
+        domain = parts[1]
+
+       if regenerate_node_key:
+            # Generate 32 random bytes
+            bytes = random.sample(xrange(0, 256), 32)
+            # Base64 encode their string representation
+            node['key'] = base64.b64encode("".join(map(chr, bytes)))
+            # XXX Boot Manager cannot handle = in the key
+            node['key'] = node['key'].replace("=", "")
+            # Save it
+            node.sync()
+
+        # Generate node configuration file suitable for BootCD
+        file = ""
+
+        file += 'NODE_ID="%d"\n' % node['node_id']
+        file += 'NODE_KEY="%s"\n' % node['key']
+
+        if primary['mac']:
+            file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
+
+        file += 'IP_METHOD="%s"\n' % primary['method']
+
+        if primary['method'] == 'static':
+            file += 'IP_ADDRESS="%s"\n' % primary['ip']
+            file += 'IP_GATEWAY="%s"\n' % primary['gateway']
+            file += 'IP_NETMASK="%s"\n' % primary['netmask']
+            file += 'IP_NETADDR="%s"\n' % primary['network']
+            file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
+            file += 'IP_DNS1="%s"\n' % primary['dns1']
+            file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
+
+        file += 'HOST_NAME="%s"\n' % host
+        file += 'DOMAIN_NAME="%s"\n' % domain
+
+        for nodenetwork in nodenetworks:
+            if nodenetwork['method'] == 'ipmi':
+                file += 'IPMI_ADDRESS="%s"\n' % nodenetwork['ip']
+                if nodenetwork['mac']:
+                    file += 'IPMI_MAC="%s"\n' % nodenetwork['mac'].lower()
+                break
+
+        return file
diff --git a/PLC/Methods/GetAddressTypes.py b/PLC/Methods/GetAddressTypes.py
new file mode 100644 (file)
index 0000000..d10be73
--- /dev/null
@@ -0,0 +1,32 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.AddressTypes import AddressType, AddressTypes
+from PLC.Auth import Auth
+
+class GetAddressTypes(Method):
+    """
+    Returns an array of structs containing details about address
+    types. If address_type_filter is specified and is an array of
+    address type identifiers, or a struct of address type attributes,
+    only address types matching the filter will be returned. If
+    return_fields is specified, only the specified details will be
+    returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(AddressType.fields['address_type_id'],
+                     AddressType.fields['name'])],
+              Filter(AddressType.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [AddressType.fields]
+
+
+    def call(self, auth, address_type_filter = None, return_fields = None):
+        return AddressTypes(self.api, address_type_filter, return_fields)
diff --git a/PLC/Methods/GetAddresses.py b/PLC/Methods/GetAddresses.py
new file mode 100644 (file)
index 0000000..b299295
--- /dev/null
@@ -0,0 +1,30 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Addresses import Address, Addresses
+from PLC.Auth import Auth
+
+class GetAddresses(Method):
+    """
+    Returns an array of structs containing details about addresses. If
+    address_filter is specified and is an array of address
+    identifiers, or a struct of address attributes, only addresses
+    matching the filter will be returned. If return_fields is
+    specified, only the specified details will be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Address.fields['address_id']],
+              Filter(Address.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [Address.fields]
+    
+
+    def call(self, auth, address_filter = None, return_fields = None):
+        return Addresses(self.api, address_filter, return_fields)
diff --git a/PLC/Methods/GetBootMedium.py b/PLC/Methods/GetBootMedium.py
new file mode 100644 (file)
index 0000000..fcd0957
--- /dev/null
@@ -0,0 +1,470 @@
+# $Id: GetBootMedium.py 9562 2008-06-13 14:00:10Z thierry $
+import random
+import base64
+import os
+import os.path
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.NodeNetworkSettings import NodeNetworkSetting, NodeNetworkSettings
+from PLC.NodeGroups import NodeGroup, NodeGroups
+
+# could not define this in the class..
+boot_medium_actions = [ 'node-preview',
+                        'node-floppy',
+                        'node-iso',
+                        'node-usb',
+                        'generic-iso',
+                        'generic-usb',
+                        ]
+
+# compute a new key
+# xxx used by GetDummyBoxMedium
+def compute_key():
+    # Generate 32 random bytes
+    bytes = random.sample(xrange(0, 256), 32)
+    # Base64 encode their string representation
+    key = base64.b64encode("".join(map(chr, bytes)))
+    # Boot Manager cannot handle = in the key
+    # XXX this sounds wrong, as it might prevent proper decoding
+    key = key.replace("=", "")
+    return key
+
+class GetBootMedium(Method):
+    """
+    This method is a redesign based on former, supposedly dedicated, 
+    AdmGenerateNodeConfFile
+
+    As compared with its ancestor, this method provides a much more detailed
+    detailed interface, that allows to
+    (*) either just preview the node config file -- in which case 
+        the node key is NOT recomputed, and NOT provided in the output
+    (*) or regenerate the node config file for storage on a floppy 
+        that is, exactly what the ancestor method used todo, 
+        including renewing the node's key
+    (*) or regenerate the config file and bundle it inside an ISO or USB image
+    (*) or just provide the generic ISO or USB boot images 
+        in which case of course the node_id_or_hostname parameter is not used
+
+    action is expected among the following string constants
+    (*) node-preview
+    (*) node-floppy
+    (*) node-iso
+    (*) node-usb
+    (*) generic-iso
+    (*) generic-usb
+
+    Apart for the preview mode, this method generates a new node key for the
+    specified node, effectively invalidating any old boot medium.
+
+    In addition, two return mechanisms are supported.
+    (*) The default behaviour is that the file's content is returned as a 
+        base64-encoded string. This is how the ancestor method used to work.
+        To use this method, pass an empty string as the file parameter.
+
+    (*) Or, for efficiency -- this makes sense only when the API is used 
+        by the web pages that run on the same host -- the caller may provide 
+        a filename, in which case the resulting file is stored in that location instead. 
+        The filename argument can use the following markers, that are expanded 
+        within the method
+        - %d : default root dir (some builtin dedicated area under /var/tmp/)
+               Using this is recommended, and enforced for non-admin users
+        - %n : the node's name when this makes sense, or a mktemp-like name when 
+               generic media is requested
+        - %s : a file suffix appropriate in the context (.txt, .iso or the like)
+        - %v : the bootcd version string (e.g. 4.0)
+        - %p : the PLC name
+        - %f : the nodefamily
+        - %a : arch
+        With the file-based return mechanism, the method returns the full pathname 
+        of the result file; 
+        ** WARNING **
+        It is the caller's responsability to remove this file after use.
+
+    Options: an optional array of keywords. 
+        options are not supported for generic images
+    Currently supported are
+        - 'partition' - for USB actions only
+        - 'cramfs'
+        - 'serial' or 'serial:<console_spec>'
+        console_spec (or 'default') is passed as-is to bootcd/build.sh
+        it is expected to be a colon separated string denoting
+        tty - baudrate - parity - bits
+        e.g. ttyS0:115200:n:8
+
+    Security:
+        - Non-admins can only generate files for nodes at their sites.
+        - Non-admins, when they provide a filename, *must* specify it in the %d area
+
+   Housekeeping: 
+        Whenever needed, the method stores intermediate files in a
+        private area, typically not located under the web server's
+        accessible area, and are cleaned up by the method.
+
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+        Parameter (str, "Action mode, expected in " + "|".join(boot_medium_actions)),
+        Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
+        Parameter ([str], "Options"),
+        ]
+
+    returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
+
+    BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
+    BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
+    GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
+    WORKDIR = "/var/tmp/bootmedium"
+    DEBUG = False
+    # uncomment this to preserve temporary area and bootcustom logs
+    #DEBUG = True
+
+    ### returns (host, domain) :
+    # 'host' : host part of the hostname
+    # 'domain' : domain part of the hostname
+    def split_hostname (self, node):
+        # Split hostname into host and domain parts
+        parts = node['hostname'].split(".", 1)
+        if len(parts) < 2:
+            raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
+        return parts
+        
+    # plnode.txt content
+    def floppy_contents (self, node, renew_key):
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+
+        # 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']:
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
+
+        # Get node networks for this node
+        primary = None
+        nodenetworks = NodeNetworks(self.api, node['nodenetwork_ids'])
+        for nodenetwork in nodenetworks:
+            if nodenetwork['is_primary']:
+                primary = nodenetwork
+                break
+        if primary is None:
+            raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
+
+        ( host, domain ) = self.split_hostname (node)
+
+        if renew_key:
+            node['key'] = compute_key()
+            # Save it
+            node.sync()
+
+        # Generate node configuration file suitable for BootCD
+        file = ""
+
+        if renew_key:
+            file += 'NODE_ID="%d"\n' % node['node_id']
+            file += 'NODE_KEY="%s"\n' % node['key']
+
+        if primary['mac']:
+            file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
+
+        file += 'IP_METHOD="%s"\n' % primary['method']
+
+        if primary['method'] == 'static':
+            file += 'IP_ADDRESS="%s"\n' % primary['ip']
+            file += 'IP_GATEWAY="%s"\n' % primary['gateway']
+            file += 'IP_NETMASK="%s"\n' % primary['netmask']
+            file += 'IP_NETADDR="%s"\n' % primary['network']
+            file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
+            file += 'IP_DNS1="%s"\n' % primary['dns1']
+            file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
+
+        file += 'HOST_NAME="%s"\n' % host
+        file += 'DOMAIN_NAME="%s"\n' % domain
+
+        # define various nodenetwork settings attached to the primary nodenetwork
+        settings = NodeNetworkSettings (self.api, {'nodenetwork_id':nodenetwork['nodenetwork_id']})
+
+        categories = set()
+        for setting in settings:
+            if setting['category'] is not None:
+                categories.add(setting['category'])
+        
+        for category in categories:
+            category_settings = NodeNetworkSettings(self.api,{'nodenetwork_id':nodenetwork['nodenetwork_id'],
+                                                              'category':category})
+            if category_settings:
+                file += '### Category : %s\n'%category
+                for setting in category_settings:
+                    file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
+
+        for nodenetwork in nodenetworks:
+            if nodenetwork['method'] == 'ipmi':
+                file += 'IPMI_ADDRESS="%s"\n' % nodenetwork['ip']
+                if nodenetwork['mac']:
+                    file += 'IPMI_MAC="%s"\n' % nodenetwork['mac'].lower()
+                break
+
+        return file
+
+    # see also InstallBootstrapFS in bootmanager that does similar things
+    def get_nodefamily (self, node):
+        try:
+            (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
+        except:
+            (pldistro,arch) = ("planetlab","i386")
+            
+        if not node:
+            return (pldistro,arch)
+
+        known_archs = [ 'i386', 'x86_64' ]
+        nodegroupnames = [ ng['name'] for ng in NodeGroups (self.api, node['nodegroup_ids'],['name'])]
+        # (1) if groupname == arch, nodefamily becomes pldistro-groupname
+        # (2) else if groupname looks like pldistro-arch, it is taken as a nodefamily
+        # (3) otherwise groupname is taken as an extension
+        for nodegroupname in nodegroupnames:
+            if nodegroupname in known_archs:
+                arch = nodegroupname
+            else:
+                for known_arch in known_archs:
+                    try:
+                        (api_pldistro,api_arch)=nodegroupname.split("-")
+                        # sanity check
+                        if api_arch != known_arch: raise Exception,"mismatch"
+                        (pldistro,arch) = (api_pldistro, api_arch)
+                        break
+                    except:
+                        pass
+        return (pldistro,arch)
+
+    def bootcd_version (self):
+        try:
+            return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
+        except:
+            raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
+    
+    def cleantrash (self):
+        for file in self.trash:
+            if self.DEBUG:
+                print 'DEBUG -- preserving',file
+            else:
+                os.unlink(file)
+
+    def call(self, auth, node_id_or_hostname, action, filename, options = []):
+
+        self.trash=[]
+        ### check action
+        if action not in boot_medium_actions:
+            raise PLCInvalidArgument, "Unknown action %s"%action
+
+        ### compute file suffix and type
+        if action.find("-iso") >= 0 :
+            suffix=".iso"
+            type = "iso"
+        elif action.find("-usb") >= 0:
+            suffix=".usb"
+            type = "usb"
+        else:
+            suffix=".txt"
+            type = "txt"
+
+        # handle / caconicalize options
+        if type == "txt":
+            if options:
+                raise PLCInvalidArgument, "Options are not supported for node configs"
+        else:
+            # create a dict for build.sh 
+            optdict={}
+            for option in options:
+                if option == "cramfs":
+                    optdict['cramfs']=True
+                elif option == 'partition':
+                    if type != "usb":
+                        raise PLCInvalidArgument, "option 'partition' is for USB images only"
+                    else:
+                        type="usb_partition"
+                elif option == "serial":
+                    optdict['serial']='default'
+                elif option.find("serial:") == 0:
+                    optdict['serial']=option.replace("serial:","")
+                else:
+                    raise PLCInvalidArgument, "unknown option %s"%option
+
+        ### check node if needed
+        if action.find("node-") == 0:
+            nodes = Nodes(self.api, [node_id_or_hostname])
+            if not nodes:
+                raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
+            node = nodes[0]
+            nodename = node['hostname']
+
+        else:
+            node = None
+            # compute a 8 bytes random number
+            tempbytes = random.sample (xrange(0,256), 8);
+            def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
+            nodename = "".join(map(hexa2,tempbytes))
+
+        # get nodefamily
+        (pldistro,arch) = self.get_nodefamily(node)
+        self.nodefamily="%s-%s"%(pldistro,arch)
+        # apply on globals
+        for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
+            setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
+            
+        ### handle filename
+        # allow to set filename to None or any other empty value
+        if not filename: filename=''
+        filename = filename.replace ("%d",self.WORKDIR)
+        filename = filename.replace ("%n",nodename)
+        filename = filename.replace ("%s",suffix)
+        filename = filename.replace ("%p",self.api.config.PLC_NAME)
+        # let's be cautious
+        try: filename = filename.replace ("%f", self.nodefamily)
+        except: pass
+        try: filename = filename.replace ("%a", arch)
+        except: pass
+        try: filename = filename.replace ("%v",self.bootcd_version())
+        except: pass
+
+        ### Check filename location
+        if filename != '':
+            if 'admin' not in self.caller['roles']:
+                if ( filename.index(self.WORKDIR) != 0):
+                    raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
+
+            ### output should not exist (concurrent runs ..)
+            if os.path.exists(filename):
+                raise PLCInvalidArgument, "Resulting file %s already exists"%filename
+
+            ### we can now safely create the file, 
+            ### either we are admin or under a controlled location
+            filedir=os.path.dirname(filename)
+            # dirname does not return "." for a local filename like its shell counterpart
+            if filedir:
+                if not os.path.exists(filedir):
+                    try:
+                        os.makedirs (filedir,0777)
+                    except:
+                        raise PLCPermissionDenied, "Could not create dir %s"%filedir
+
+        
+        ### generic media
+        if action == 'generic-iso' or action == 'generic-usb':
+            if options:
+                raise PLCInvalidArgument, "Options are not supported for generic images"
+            # this raises an exception if bootcd is missing
+            version = self.bootcd_version()
+            generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
+                                             version,
+                                             suffix)
+            generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
+
+            if filename:
+                ret=os.system ("cp %s %s"%(generic_path,filename))
+                if ret==0:
+                    return filename
+                else:
+                    raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
+            else:
+                ### return the generic medium content as-is, just base64 encoded
+                return base64.b64encode(file(generic_path).read())
+
+       ### config file preview or regenerated
+       if action == 'node-preview' or action == 'node-floppy':
+            renew_key = (action == 'node-floppy')
+            floppy = self.floppy_contents (node,renew_key)
+           if filename:
+               try:
+                   file(filename,'w').write(floppy)
+               except:
+                   raise PLCPermissionDenied, "Could not write into %s"%filename
+               return filename
+           else:
+               return floppy
+
+        ### we're left with node-iso and node-usb
+        if action == 'node-iso' or action == 'node-usb':
+
+            ### check we've got required material
+            version = self.bootcd_version()
+            
+            if not os.path.isfile(self.BOOTCDBUILD):
+                raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
+
+            # create the workdir if needed
+            if not os.path.isdir(self.WORKDIR):
+                try:
+                    os.makedirs(self.WORKDIR,0777)
+                    os.chmod(self.WORKDIR,0777)
+                except:
+                    raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
+            
+            try:
+                # generate floppy config
+                floppy_text = self.floppy_contents(node,True)
+                # store it
+                floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
+                try:
+                    file(floppy_file,"w").write(floppy_text)
+                except:
+                    raise PLCPermissionDenied, "Could not write into %s"%floppy_file
+
+                self.trash.append(floppy_file)
+
+                node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
+
+                # make build's arguments
+                serial_arg=""
+                if "cramfs" in optdict: type += "_cramfs"
+                if "serial" in optdict: serial_arg = "-s %s"%optdict['serial']
+                log_file="%s.log"%node_image
+                # invoke build.sh
+                build_command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
+                                                                         floppy_file,
+                                                                         node_image,
+                                                                         type,
+                                                                         serial_arg,
+                                                                         log_file)
+                if self.DEBUG:
+                    print 'build command:',build_command
+                ret=os.system(build_command)
+                if ret != 0:
+                    raise PLCAPIError,"bootcd/build.sh failed\n%s\n%s"%(
+                        build_command,file(log_file).read())
+
+                self.trash.append(log_file)
+                if not os.path.isfile (node_image):
+                    raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
+            
+                # handle result
+                if filename:
+                    ret=os.system("mv %s %s"%(node_image,filename))
+                    if ret != 0:
+                        self.trash.append(node_image)
+                        self.cleantrash()
+                        raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
+                    self.cleantrash()
+                    return filename
+                else:
+                    result = file(node_image).read()
+                    self.trash.append(node_image)
+                    self.cleantrash()
+                    return base64.b64encode(result)
+            except:
+                self.cleantrash()
+                raise
+                
+        # we're done here, or we missed something
+        raise PLCAPIError,'Unhandled action %s'%action
+
diff --git a/PLC/Methods/GetBootStates.py b/PLC/Methods/GetBootStates.py
new file mode 100644 (file)
index 0000000..4cd31be
--- /dev/null
@@ -0,0 +1,22 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.BootStates import BootState, BootStates
+from PLC.Auth import Auth
+
+class GetBootStates(Method):
+    """
+    Returns an array of all valid node boot states.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth()
+        ]
+
+    returns = [BootState.fields['boot_state']]
+    
+
+    def call(self, auth):
+        return [boot_state['boot_state'] for boot_state in BootStates(self.api)]
diff --git a/PLC/Methods/GetConfFiles.py b/PLC/Methods/GetConfFiles.py
new file mode 100644 (file)
index 0000000..89d5250
--- /dev/null
@@ -0,0 +1,31 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Auth import Auth
+
+class GetConfFiles(Method):
+    """
+    Returns an array of structs containing details about configuration
+    files. If conf_file_filter is specified and is an array of
+    configuration file identifiers, or a struct of configuration file
+    attributes, only configuration files matching the filter will be
+    returned. If return_fields is specified, only the specified
+    details will be returned.
+    """
+
+    roles = ['admin', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([ConfFile.fields['conf_file_id']],
+              Filter(ConfFile.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [ConfFile.fields]
+
+
+    def call(self, auth, conf_file_filter = None, return_fields = None):
+        return ConfFiles(self.api, conf_file_filter, return_fields)
diff --git a/PLC/Methods/GetEventObjects.py b/PLC/Methods/GetEventObjects.py
new file mode 100644 (file)
index 0000000..02bcd68
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.EventObjects import EventObject, EventObjects
+from PLC.Auth import Auth
+
+class GetEventObjects(Method):
+    """
+    Returns an array of structs containing details about events and
+    faults. If event_filter is specified and is an array of event
+    identifiers, or a struct of event attributes, only events matching
+    the filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Filter(EventObject.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [EventObject.fields]
+
+    def call(self, auth, event_filter = None, return_fields = None):
+        return EventObjects(self.api, event_filter, return_fields)
+
diff --git a/PLC/Methods/GetEvents.py b/PLC/Methods/GetEvents.py
new file mode 100644 (file)
index 0000000..2bc989c
--- /dev/null
@@ -0,0 +1,30 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Events import Event, Events
+from PLC.Auth import Auth
+
+class GetEvents(Method):
+    """
+    Returns an array of structs containing details about events and
+    faults. If event_filter is specified and is an array of event
+    identifiers, or a struct of event attributes, only events matching
+    the filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed([Event.fields['event_id']],
+              Filter(Event.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [Event.fields]
+
+    def call(self, auth, event_filter = None, return_fields = None):
+        return Events(self.api, event_filter, return_fields)
+
diff --git a/PLC/Methods/GetInitScripts.py b/PLC/Methods/GetInitScripts.py
new file mode 100644 (file)
index 0000000..d8bb0f5
--- /dev/null
@@ -0,0 +1,31 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.InitScripts import InitScript, InitScripts
+from PLC.Auth import Auth
+
+class GetInitScripts(Method):
+    """
+    Returns an array of structs containing details about initscripts. 
+    If initscript_filter is specified and is an array of initscript 
+    identifiers, or a struct of initscript attributes, only initscripts 
+    matching the filter will be returned. If return_fields is specified, 
+    only the specified details will be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(InitScript.fields['initscript_id'],
+                    InitScript.fields['name'])],
+              Filter(InitScript.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [InitScript.fields]
+
+
+    def call(self, auth, initscript_filter = None, return_fields = None):
+        return InitScripts(self.api, initscript_filter, return_fields)
diff --git a/PLC/Methods/GetKeyTypes.py b/PLC/Methods/GetKeyTypes.py
new file mode 100644 (file)
index 0000000..32bb658
--- /dev/null
@@ -0,0 +1,22 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.KeyTypes import KeyType, KeyTypes
+from PLC.Auth import Auth
+
+class GetKeyTypes(Method):
+    """
+    Returns an array of all valid key types.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth()
+        ]
+
+    returns = [KeyType.fields['key_type']]
+
+
+    def call(self, auth):
+        return [key_type['key_type'] for key_type in KeyTypes(self.api)]
diff --git a/PLC/Methods/GetKeys.py b/PLC/Methods/GetKeys.py
new file mode 100644 (file)
index 0000000..2d7550c
--- /dev/null
@@ -0,0 +1,41 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Persons import Person, Persons
+from PLC.Keys import Key, Keys
+from PLC.Auth import Auth
+
+class GetKeys(Method):
+    """
+    Returns an array of structs containing details about keys. If
+    key_filter is specified and is an array of key identifiers, or a
+    struct of key attributes, only keys matching the filter will be
+    returned. If return_fields is specified, only the specified
+    details will be returned.
+
+    Admin may query all keys. Non-admins may only query their own
+    keys.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Key.fields['key_id'])],
+              Filter(Key.fields)),
+        Parameter([str], "List of fields to return", nullok = True)        
+        ]
+
+    returns = [Key.fields]
+
+
+    def call(self, auth, key_filter = None, return_fields = None):
+       keys = Keys(self.api, key_filter, return_fields)
+
+       # If we are not admin, make sure to only return our own keys       
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            keys = filter(lambda key: key['key_id'] in self.caller['key_ids'], keys)
+
+        return keys
diff --git a/PLC/Methods/GetMessages.py b/PLC/Methods/GetMessages.py
new file mode 100644 (file)
index 0000000..b0eb44e
--- /dev/null
@@ -0,0 +1,31 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Messages import Message, Messages
+from PLC.Auth import Auth
+
+class GetMessages(Method):
+    """
+    Returns an array of structs containing details about message
+    templates. If message template_filter is specified and is an array
+    of message template identifiers, or a struct of message template
+    attributes, only message templates matching the filter will be
+    returned. If return_fields is specified, only the specified
+    details will be returned.
+    """
+
+    roles = ['admin', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Message.fields['message_id']],
+              Filter(Message.fields)),
+        Parameter([str], "List of fields to return", nullok = True),
+        ]
+
+    returns = [Message.fields]
+
+
+    def call(self, auth, message_filter = None, return_fields = None):
+        return Messages(self.api, message_filter, return_fields)
diff --git a/PLC/Methods/GetNetworkMethods.py b/PLC/Methods/GetNetworkMethods.py
new file mode 100644 (file)
index 0000000..cee914a
--- /dev/null
@@ -0,0 +1,22 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NetworkMethods import NetworkMethod, NetworkMethods
+from PLC.Auth import Auth
+
+class GetNetworkMethods(Method):
+    """
+    Returns a list of all valid network methods.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth()
+        ]
+
+    returns = [NetworkMethod.fields['method']]
+
+
+    def call(self, auth):
+        return [network_method['method'] for network_method in NetworkMethods(self.api)]
diff --git a/PLC/Methods/GetNetworkTypes.py b/PLC/Methods/GetNetworkTypes.py
new file mode 100644 (file)
index 0000000..dbddd9f
--- /dev/null
@@ -0,0 +1,22 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NetworkTypes import NetworkType, NetworkTypes
+from PLC.Auth import Auth
+
+class GetNetworkTypes(Method):
+    """
+    Returns a list of all valid network types.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth()
+        ]
+
+    returns = [NetworkType.fields['type']]
+
+
+    def call(self, auth):
+        return [network_type['type'] for network_type in NetworkTypes(self.api)]
diff --git a/PLC/Methods/GetNodeGroups.py b/PLC/Methods/GetNodeGroups.py
new file mode 100644 (file)
index 0000000..f4927ef
--- /dev/null
@@ -0,0 +1,30 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.NodeGroups import NodeGroup, NodeGroups
+
+class GetNodeGroups(Method):
+    """
+    Returns an array of structs containing details about node groups.
+    If nodegroup_filter is specified and is an array of node group
+    identifiers or names, or a struct of node group attributes, only
+    node groups matching the filter will be returned. If return_fields
+    is specified, only the specified details will be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(NodeGroup.fields['nodegroup_id'],
+                     NodeGroup.fields['name'])],
+              Filter(NodeGroup.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [NodeGroup.fields]
+  
+    def call(self, auth, nodegroup_filter = None, return_fields = None):
+       return NodeGroups(self.api, nodegroup_filter, return_fields)
diff --git a/PLC/Methods/GetNodeNetworkSettingTypes.py b/PLC/Methods/GetNodeNetworkSettingTypes.py
new file mode 100644 (file)
index 0000000..462cfbc
--- /dev/null
@@ -0,0 +1,33 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.NodeNetworkSettingTypes import NodeNetworkSettingType, NodeNetworkSettingTypes
+
+class GetNodeNetworkSettingTypes(Method):
+    """
+    Returns an array of structs containing details about
+    nodenetwork setting types.
+
+    The usual filtering scheme applies on this method.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(NodeNetworkSettingType.fields['nodenetwork_setting_type_id'],
+                     NodeNetworkSettingType.fields['name'])],
+              Filter(NodeNetworkSettingType.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [NodeNetworkSettingType.fields]
+
+    def call(self, auth, nodenetwork_setting_type_filter = None, return_fields = None):
+        return NodeNetworkSettingTypes(self.api, nodenetwork_setting_type_filter, return_fields)
diff --git a/PLC/Methods/GetNodeNetworkSettings.py b/PLC/Methods/GetNodeNetworkSettings.py
new file mode 100644 (file)
index 0000000..0003be9
--- /dev/null
@@ -0,0 +1,45 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Persons import Person, Persons
+from PLC.Auth import Auth
+
+from PLC.NodeNetworkSettings import NodeNetworkSetting, NodeNetworkSettings
+from PLC.Sites import Site, Sites
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+
+class GetNodeNetworkSettings(Method):
+    """
+    Returns an array of structs containing details about
+    nodenetworks and related settings.
+
+    If nodenetwork_setting_filter is specified and is an array of
+    nodenetwork setting identifiers, only nodenetwork settings matching
+    the filter will be returned. If return_fields is specified, only
+    the specified details will be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([NodeNetworkSetting.fields['nodenetwork_setting_id']],
+              Parameter(int,"Nodenetwork setting id"),
+              Filter(NodeNetworkSetting.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [NodeNetworkSetting.fields]
+    
+
+    def call(self, auth, nodenetwork_setting_filter = None, return_fields = None):
+
+        nodenetwork_settings = NodeNetworkSettings(self.api, nodenetwork_setting_filter, return_fields)
+
+        return nodenetwork_settings
diff --git a/PLC/Methods/GetNodeNetworks.py b/PLC/Methods/GetNodeNetworks.py
new file mode 100644 (file)
index 0000000..150f87d
--- /dev/null
@@ -0,0 +1,32 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+
+class GetNodeNetworks(Method):
+    """
+    Returns an array of structs containing details about node network
+    interfacess. If nodenetworks_filter is specified and is an array
+    of node network identifiers, or a struct of node network
+    fields and values, only node network interfaces matching the filter
+    will be returned.
+
+    If return_fields is given, only the specified details will be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous']
+
+    accepts = [
+        Auth(),
+        Mixed([NodeNetwork.fields['nodenetwork_id']],
+              Parameter (int, "nodenetwork id"),
+              Filter(NodeNetwork.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [NodeNetwork.fields]
+    
+    def call(self, auth, nodenetwork_filter = None, return_fields = None):
+        return NodeNetworks(self.api, nodenetwork_filter, return_fields)
diff --git a/PLC/Methods/GetNodes.py b/PLC/Methods/GetNodes.py
new file mode 100644 (file)
index 0000000..f4b577d
--- /dev/null
@@ -0,0 +1,83 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Nodes import Node, Nodes
+from PLC.Persons import Person, Persons
+from PLC.Auth import Auth
+
+class GetNodes(Method):
+    """
+    Returns an array of structs containing details about nodes. If
+    node_filter is specified and is an array of node identifiers or
+    hostnames, or a struct of node attributes, only nodes matching the
+    filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    Some fields may only be viewed by admins.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Node.fields['node_id'],
+                     Node.fields['hostname'])],
+             Parameter(str,"hostname"),
+              Parameter(int,"node_id"),
+              Filter(Node.fields)),
+        Parameter([str], "List of fields to return", nullok = True),
+        ]
+
+    returns = [Node.fields]
+
+
+    def call(self, auth, node_filter = None, return_fields = None):
+        
+       # Must query at least slice_ids_whitelist
+       if return_fields is not None:
+           added_fields = set(['slice_ids_whitelist', 'site_id']).difference(return_fields)
+           return_fields += added_fields
+       else:
+           added_fields =[]    
+
+       # Get node information
+        nodes = Nodes(self.api, node_filter, return_fields)
+
+        # Remove admin only fields
+        if not isinstance(self.caller, Person) or \
+           'admin' not in self.caller['roles']:
+           slice_ids = set()
+           site_ids = set()
+           
+           if self.caller:
+               slice_ids.update(self.caller['slice_ids'])
+               if isinstance(self.caller, Node):
+                   site_ids.update([self.caller['site_id']])
+               else:  
+                   site_ids.update(self.caller['site_ids'])
+
+           # if node has whitelist, only return it if users is at
+           # the same site or user has a slice on the whitelist 
+            for node in nodes[:]:
+               if 'site_id' in node and \
+                  site_ids.intersection([node['site_id']]):
+                   continue    
+               if 'slice_ids_whitelist' in node and \
+                  node['slice_ids_whitelist'] and \
+                  not slice_ids.intersection(node['slice_ids_whitelist']):
+                   nodes.remove(node)
+
+           # remove remaining admin only fields
+            for node in nodes:    
+               for field in ['boot_nonce', 'key', 'session', 'root_person_ids']:
+                    if field in node:
+                        del node[field]
+       
+       # remove added fields if not specified
+       if added_fields:
+           for node in nodes:
+               for field in added_fields:
+                   del node[field]     
+
+        return nodes
diff --git a/PLC/Methods/GetPCUProtocolTypes.py b/PLC/Methods/GetPCUProtocolTypes.py
new file mode 100644 (file)
index 0000000..44f9380
--- /dev/null
@@ -0,0 +1,40 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUProtocolTypes import PCUProtocolType, PCUProtocolTypes
+from PLC.Auth import Auth
+from PLC.Filter import Filter
+
+class GetPCUProtocolTypes(Method):
+    """
+    Returns an array of PCU Types.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+       Mixed([PCUProtocolType.fields['pcu_type_id']],
+               Filter(PCUProtocolType.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [PCUProtocolType.fields]
+    
+
+    def call(self, auth, protocol_type_filter = None, return_fields = None):
+
+       #Must query at least pcu_type_id
+       if return_fields is not None and 'pcu_protocol_type_id' not in return_fields:
+           return_fields.append('pcu_protocol_type_id')
+           added_fields = ['pcu_protocol_type_id']
+       else:
+           added_fields = []
+
+       protocol_types = PCUProtocolTypes(self.api, protocol_type_filter, return_fields)
+
+       for added_field in added_fields:
+           for protocol_type in protocol_types:
+               del protocol_type[added_field]
+               
+        return protocol_types 
diff --git a/PLC/Methods/GetPCUTypes.py b/PLC/Methods/GetPCUTypes.py
new file mode 100644 (file)
index 0000000..2a81508
--- /dev/null
@@ -0,0 +1,50 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUTypes import PCUType, PCUTypes
+from PLC.Auth import Auth
+from PLC.Filter import Filter
+
+class GetPCUTypes(Method):
+    """
+    Returns an array of PCU Types.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+       Mixed([Mixed(PCUType.fields['pcu_type_id'],
+                     PCUType.fields['model'])],
+              Parameter(str, 'model'),
+              Parameter(int, 'node_id'),               
+               Filter(PCUType.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [PCUType.fields]
+    
+
+    def call(self, auth, pcu_type_filter = None, return_fields = None):
+
+       #Must query at least pcu_type_id
+       if return_fields is not None:
+           added_fields = []
+           if 'pcu_type_id' not in return_fields:
+               return_fields.append('pcu_type_id')
+               added_fields.append('pcu_type_id')
+           if 'pcu_protocol_types' in return_fields and \
+              'pcu_protocol_type_ids' not in return_fields:
+               return_fields.append('pcu_protocol_type_ids')
+               added_fields.append('pcu_protocol_type_ids') 
+       else:
+           added_fields = []
+
+       pcu_types = PCUTypes(self.api, pcu_type_filter, return_fields)
+
+       # remove added fields and protocol_types
+       for added_field in added_fields:
+           for pcu_type in pcu_types:
+               del pcu_type[added_field]
+               
+        return pcu_types 
diff --git a/PLC/Methods/GetPCUs.py b/PLC/Methods/GetPCUs.py
new file mode 100644 (file)
index 0000000..ee9ab4d
--- /dev/null
@@ -0,0 +1,73 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+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 Auth
+
+class GetPCUs(Method):
+    """
+    Returns an array of structs containing details about power control
+    units (PCUs). If pcu_filter is specified and is an array of PCU
+    identifiers, or a struct of PCU attributes, only PCUs matching the
+    filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    Admin may query all PCUs. Non-admins may only query the PCUs at
+    their sites.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([PCU.fields['pcu_id']],
+              Filter(PCU.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [PCU.fields]
+
+    def call(self, auth, pcu_filter = None, return_fields = None):
+       # If we are not admin
+        if not (isinstance(self.caller, Person) and 'admin' in self.caller['roles']):
+            # Return only the PCUs at our site
+            valid_pcu_ids = []
+
+            if isinstance(self.caller, Person):
+                site_ids = self.caller['site_ids']
+            elif isinstance(self.caller, Node):
+                site_ids = [self.caller['site_id']]
+
+            for site in Sites(self.api, site_ids):
+                valid_pcu_ids += site['pcu_ids']
+
+            if not valid_pcu_ids:
+                return []
+
+            if pcu_filter is None:
+                pcu_filter = valid_pcu_ids
+
+        # Must query at least slice_id (see below)
+        if return_fields is not None and 'pcu_id' not in return_fields:
+            return_fields.append('pcu_id')
+            added_fields = True
+        else:
+            added_fields = False
+
+        pcus = PCUs(self.api, pcu_filter, return_fields)
+
+        # Filter out PCUs that are not viewable
+        if not (isinstance(self.caller, Person) and 'admin' in self.caller['roles']):
+            pcus = filter(lambda pcu: pcu['pcu_id'] in valid_pcu_ids, pcus)
+
+        # Remove pcu_id if not specified
+        if added_fields:
+            for pcu in pcus:
+               if 'pcu_id' in pcu:
+                   del pcu['pcu_id']
+
+        return pcus
diff --git a/PLC/Methods/GetPeerData.py b/PLC/Methods/GetPeerData.py
new file mode 100644 (file)
index 0000000..e68cf55
--- /dev/null
@@ -0,0 +1,87 @@
+#
+# Thierry Parmentelat - INRIA
+# 
+# $Id: GetPeerData.py 5574 2007-10-25 20:33:17Z thierry $
+
+import time
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.Peers import Peer, Peers
+
+from PLC.Sites import Site, Sites
+from PLC.Keys import Key, Keys
+from PLC.Nodes import Node, Nodes
+from PLC.Persons import Person, Persons
+from PLC.Slices import Slice, Slices
+from PLC.SliceAttributes import SliceAttributes
+
+class GetPeerData(Method):
+    """
+    Returns lists of local objects that a peer should cache in its
+    database as foreign objects. Also returns the list of foreign
+    nodes in this database, for which the calling peer is
+    authoritative, to assist in synchronization of slivers.
+    
+    See the implementation of RefreshPeer for how this data is used.
+    """
+
+    roles = ['admin', 'peer']
+
+    accepts = [Auth()]
+
+    returns = {
+        'Sites': Parameter([dict], "List of local sites"),
+        'Keys': Parameter([dict], "List of local keys"),
+        'Nodes': Parameter([dict], "List of local nodes"),
+        'Persons': Parameter([dict], "List of local users"),
+        'Slices': Parameter([dict], "List of local slices"),
+        'db_time': Parameter(float, "(Debug) Database fetch time"),
+        }
+
+    def call (self, auth):
+        start = time.time()
+
+        # Filter out various secrets
+        node_fields = filter(lambda field: field not in \
+                             ['boot_nonce', 'key', 'session', 'root_person_ids'],
+                             Node.fields)
+        nodes = Nodes(self.api, {'peer_id': None}, node_fields);
+        # filter out whitelisted nodes
+        nodes = [ n for n in nodes if not n['slice_ids_whitelist']] 
+        
+
+        person_fields = filter(lambda field: field not in \
+                               ['password', 'verification_key', 'verification_expires'],
+                               Person.fields)
+
+        # XXX Optimize to return only those Persons, Keys, and Slices
+        # necessary for slice creation on the calling peer's nodes.
+
+       # filter out special person
+       persons = Persons(self.api, {'~email':[self.api.config.PLC_API_MAINTENANCE_USER,
+                                              self.api.config.PLC_ROOT_USER],
+                                    'peer_id': None}, person_fields)
+
+       # filter out system slices
+        system_slice_ids = SliceAttributes(self.api, {'name': 'system', 'value': '1'}).dict('slice_id')
+       slices = Slices(self.api, {'peer_id': None,
+                                  '~slice_id':system_slice_ids.keys()})
+       
+        result = {
+            'Sites': Sites(self.api, {'peer_id': None}),
+            'Keys': Keys(self.api, {'peer_id': None}),
+            'Nodes': nodes,
+            'Persons': persons,
+            'Slices': slices,
+            }
+
+        if isinstance(self.caller, Peer):
+            result['PeerNodes'] = Nodes(self.api, {'peer_id': self.caller['peer_id']})
+
+        result['db_time'] = time.time() - start
+
+        return result
diff --git a/PLC/Methods/GetPeerName.py b/PLC/Methods/GetPeerName.py
new file mode 100644 (file)
index 0000000..30fbd94
--- /dev/null
@@ -0,0 +1,19 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter
+from PLC.Auth import Auth
+
+from PLC.Peers import Peer, Peers
+
+class GetPeerName (Method):
+    """
+    Returns this peer's name, as defined in the config as PLC_NAME
+    """
+
+    roles = ['admin', 'peer', 'node']
+
+    accepts = [Auth()]
+
+    returns = Peer.fields['peername']
+
+    def call (self, auth):
+        return self.api.config.PLC_NAME
diff --git a/PLC/Methods/GetPeers.py b/PLC/Methods/GetPeers.py
new file mode 100644 (file)
index 0000000..e93fe36
--- /dev/null
@@ -0,0 +1,47 @@
+#
+# Thierry Parmentelat - INRIA
+# 
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+
+from PLC.Persons import Person
+from PLC.Peers import Peer, Peers
+
+class GetPeers (Method):
+    """
+    Returns an array of structs containing details about peers. If
+    person_filter is specified and is an array of peer identifiers or
+    peer names, or a struct of peer attributes, only peers matching
+    the filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+    """
+
+    roles = ['admin', 'node','pi','user']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Peer.fields['peer_id'],
+                     Peer.fields['peername'])],
+              Filter(Peer.fields)),
+        Parameter([str], "List of fields to return", nullok = True)        
+        ]
+
+    returns = [Peer.fields]
+
+    def call (self, auth, peer_filter = None, return_fields = None):
+        
+        peers = Peers(self.api, peer_filter, return_fields)
+
+        # Remove admin only fields
+        if not isinstance(self.caller, Person) or \
+                'admin' not in self.caller['roles']:
+            for peer in peers:    
+               for field in ['key', 'cacert']:
+                    if field in peer:
+                        del peer[field]
+
+        return peers
diff --git a/PLC/Methods/GetPersons.py b/PLC/Methods/GetPersons.py
new file mode 100644 (file)
index 0000000..5228933
--- /dev/null
@@ -0,0 +1,87 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Auth import Auth
+
+hidden_fields = ['password', 'verification_key', 'verification_expires']
+
+class GetPersons(Method):
+    """
+    Returns an array of structs containing details about users. If
+    person_filter is specified and is an array of user identifiers or
+    usernames, or a struct of user attributes, only users matching the
+    filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    Users and techs may only retrieve details about themselves. PIs
+    may retrieve details about themselves and others at their
+    sites. Admins and nodes may retrieve details about all accounts.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Person.fields['person_id'],
+                     Person.fields['email'])],
+             Parameter(str,"email"),
+              Parameter(int,"person_id"), 
+              Filter(Person.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    # Filter out password field
+    return_fields = dict(filter(lambda (field, value): field not in hidden_fields,
+                                Person.fields.items()))
+    returns = [return_fields]
+    
+    def call(self, auth, person_filter = None, return_fields = None):
+       # If we are not admin, make sure to only return viewable accounts
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            # Get accounts that we are able to view
+            valid_person_ids = [self.caller['person_id']]
+            if 'pi' in self.caller['roles'] and self.caller['site_ids']:
+                sites = Sites(self.api, self.caller['site_ids'])
+                for site in sites:
+                    valid_person_ids += site['person_ids']
+
+            if not valid_person_ids:
+                return []
+
+            if person_filter is None:
+                person_filter = valid_person_ids
+
+        # Filter out password field
+        if return_fields:
+            return_fields = filter(lambda field: field not in hidden_fields,
+                                   return_fields)
+       else:
+           return_fields = self.return_fields.keys()
+
+        # Must query at least person_id, site_ids, and role_ids (see
+        # Person.can_view() and below).
+        if return_fields is not None:
+            added_fields = set(['person_id', 'site_ids', 'role_ids']).difference(return_fields)
+            return_fields += added_fields
+        else:
+            added_fields = []
+
+        persons = Persons(self.api, person_filter, return_fields)
+
+        # Filter out accounts that are not viewable
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            persons = filter(self.caller.can_view, persons)
+
+        # Remove added fields if not specified
+        if added_fields:
+            for person in persons:
+                for field in added_fields:
+                   if field in person:
+                       del person[field]
+
+        return persons
diff --git a/PLC/Methods/GetPlcRelease.py b/PLC/Methods/GetPlcRelease.py
new file mode 100644 (file)
index 0000000..d35df13
--- /dev/null
@@ -0,0 +1,58 @@
+from PLC.Method import Method
+from PLC.Auth import Auth
+from PLC.Faults import *
+
+import re
+
+comment_regexp = '\A\s*#.|\A\s*\Z|\Axxxxx' 
+
+regexps = { 'build'   : '\A[bB]uild\s+(?P<key>[^:]+)\s*:\s*(?P<value>.*)\Z',
+           'tags'    : '\A(?P<key>[^:]+)\s*:=\s*(?P<value>.*)\Z',
+            'rpms'    : '\A(?P<key>[^:]+)\s*::\s*(?P<value>.*)\Z',
+}
+
+class GetPlcRelease(Method):
+    """
+    Returns various information about the current myplc installation.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous']
+
+    accepts = [
+        Auth(),
+        ]
+
+    # for now only return /etc/myplc-release verbatim
+    returns = { 'build' : 'information about the build',
+               'tags' : 'describes the codebase location and tags used for building',
+               'rpms' : 'details the rpm installed in the myplc chroot jail' }
+
+    def call(self, auth):
+
+       comment_matcher = re.compile(comment_regexp)
+
+       matchers = {}
+       result = {} 
+       for field in regexps.keys():
+           matchers[field] = re.compile(regexps[field])
+           result[field]={}
+
+       try:
+           release = open('/etc/myplc-release')
+           for line in release.readlines():
+               line=line.strip()
+               if comment_matcher.match(line):
+                   continue
+               for field in regexps.keys():
+                   m=matchers[field].match(line)
+                   if m:
+                       (key,value)=m.groups(['key','value'])
+                       result[field][key]=value
+                       break
+               else:
+                   if not result.has_key('unexpected'):
+                       result['unexpected']=""
+                   result['unexpected'] += (line+"\n")
+       except:
+           raise PLCNotImplemented, 'Cannot open /etc/myplc-release'
+       return result
diff --git a/PLC/Methods/GetRoles.py b/PLC/Methods/GetRoles.py
new file mode 100644 (file)
index 0000000..0456662
--- /dev/null
@@ -0,0 +1,21 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Roles import Role, Roles
+from PLC.Auth import Auth
+
+class GetRoles(Method):
+    """
+    Get an array of structs containing details about all roles.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth()
+        ]
+
+    returns = [Role.fields]
+    
+    def call(self, auth):
+       return Roles(self.api)
diff --git a/PLC/Methods/GetSession.py b/PLC/Methods/GetSession.py
new file mode 100644 (file)
index 0000000..ae75219
--- /dev/null
@@ -0,0 +1,39 @@
+import time
+
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Sessions import Session, Sessions
+from PLC.Nodes import Node, Nodes
+from PLC.Persons import Person, Persons
+
+class GetSession(Method):
+    """
+    Returns a new session key if a user or node authenticated
+    successfully, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+    accepts = [Auth()]
+    returns = Session.fields['session_id']
+    
+
+    def call(self, auth):
+        # Authenticated with a session key, just return it
+        if auth.has_key('session'):
+            return auth['session']
+
+        session = Session(self.api)
+
+        if isinstance(self.caller, Person):
+            # XXX Make this configurable
+            session['expires'] = int(time.time()) + (24 * 60 * 60)
+
+        session.sync(commit = False)
+
+        if isinstance(self.caller, Node):
+            session.add_node(self.caller, commit = True)
+        elif isinstance(self.caller, Person):
+            session.add_person(self.caller, commit = True)
+
+        return session['session_id']
diff --git a/PLC/Methods/GetSessions.py b/PLC/Methods/GetSessions.py
new file mode 100644 (file)
index 0000000..a72553c
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Sessions import Session, Sessions
+from PLC.Persons import Person, Persons
+from PLC.Auth import Auth
+
+class GetSessions(Method):
+    """
+    Returns an array of structs containing details about users sessions. If
+    session_filter is specified and is an array of user identifiers or
+    session_keys, or a struct of session attributes, only sessions matching the
+    filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Session.fields['person_id'],
+                     Session.fields['session_id'])],
+              Filter(Session.fields))
+        ]
+
+    returns = [Session.fields]
+    
+    def call(self, auth, session_filter = None):
+
+        sessions = Sessions(self.api, session_filter)
+       
+       return sessions
diff --git a/PLC/Methods/GetSites.py b/PLC/Methods/GetSites.py
new file mode 100644 (file)
index 0000000..c0f198e
--- /dev/null
@@ -0,0 +1,31 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Sites import Site, Sites
+
+class GetSites(Method):
+    """
+    Returns an array of structs containing details about sites. If
+    site_filter is specified and is an array of site identifiers or
+    hostnames, or a struct of site attributes, only sites matching the
+    filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Site.fields['site_id'],
+                     Site.fields['login_base'])],
+             Parameter(str,"login_base"),
+              Parameter(int,"site_id"),
+              Filter(Site.fields)),
+        Parameter([str], "List of fields to return", nullok = True)        
+        ]
+
+    returns = [Site.fields]
+
+    def call(self, auth, site_filter = None, return_fields = None):
+        return Sites(self.api, site_filter, return_fields)
diff --git a/PLC/Methods/GetSliceAttributeTypes.py b/PLC/Methods/GetSliceAttributeTypes.py
new file mode 100644 (file)
index 0000000..bc8f1ed
--- /dev/null
@@ -0,0 +1,30 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.SliceAttributeTypes import SliceAttributeType, SliceAttributeTypes
+
+class GetSliceAttributeTypes(Method):
+    """
+    Returns an array of structs containing details about slice
+    attribute types. If attribute_type_filter is specified and
+    is an array of slice attribute type identifiers, or a
+    struct of slice attribute type attributes, only slice attribute
+    types matching the filter will be returned. If return_fields is
+    specified, only the specified details will be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(SliceAttributeType.fields['attribute_type_id'],
+                     SliceAttributeType.fields['name'])],
+              Filter(SliceAttributeType.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [SliceAttributeType.fields]
+
+    def call(self, auth, attribute_type_filter = None, return_fields = None):
+        return SliceAttributeTypes(self.api, attribute_type_filter, return_fields)
diff --git a/PLC/Methods/GetSliceAttributes.py b/PLC/Methods/GetSliceAttributes.py
new file mode 100644 (file)
index 0000000..b8a0a11
--- /dev/null
@@ -0,0 +1,88 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+
+class GetSliceAttributes(Method):
+    """
+    Returns an array of structs containing details about slice and
+    sliver attributes. An attribute is a sliver attribute if the
+    node_id field is set. If slice_attribute_filter is specified and
+    is an array of slice attribute identifiers, or a struct of slice
+    attribute attributes, only slice attributes matching the filter
+    will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    Users may only query attributes of slices or slivers of which they
+    are members. PIs may only query attributes of slices or slivers at
+    their sites, or of which they are members. Admins may query
+    attributes of any slice or sliver.
+    """
+
+    roles = ['admin', 'pi', 'user', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([SliceAttribute.fields['slice_attribute_id']],
+              Filter(SliceAttribute.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [SliceAttribute.fields]
+    
+
+    def call(self, auth, slice_attribute_filter = None, return_fields = None):
+       # If we are not admin, make sure to only return our own slice
+       # and sliver attributes.
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            # Get slices that we are able to view
+            valid_slice_ids = self.caller['slice_ids']
+            if 'pi' in self.caller['roles'] and self.caller['site_ids']:
+                sites = Sites(self.api, self.caller['site_ids'])
+                for site in sites:
+                    valid_slice_ids += site['slice_ids']
+
+            if not valid_slice_ids:
+                return []
+
+            # Get slice attributes that we are able to view
+            valid_slice_attribute_ids = []
+            slices = Slices(self.api, valid_slice_ids)
+            for slice in slices:
+                valid_slice_attribute_ids += slice['slice_attribute_ids']
+
+            if not valid_slice_attribute_ids:
+                return []
+
+            if slice_attribute_filter is None:
+                slice_attribute_filter = valid_slice_attribute_ids
+
+        # Must query at least slice_attribute_id (see below)
+        if return_fields is not None and 'slice_attribute_id' not in return_fields:
+            return_fields.append('slice_attribute_id')
+            added_fields = True
+        else:
+            added_fields = False
+
+        slice_attributes = SliceAttributes(self.api, slice_attribute_filter, return_fields)
+
+        # Filter out slice attributes that are not viewable
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            slice_attributes = filter(lambda slice_attribute: \
+                                      slice_attribute['slice_attribute_id'] in valid_slice_attribute_ids,
+                                      slice_attributes)
+
+        # Remove slice_attribute_id if not specified
+        if added_fields:
+            for slice_attribute in slice_attributes:
+               if 'slice_attribute_id' in slice_attribute:
+                   del slice_attribute['slice_attribute_id']
+
+        return slice_attributes
diff --git a/PLC/Methods/GetSliceInstantiations.py b/PLC/Methods/GetSliceInstantiations.py
new file mode 100644 (file)
index 0000000..174c209
--- /dev/null
@@ -0,0 +1,21 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceInstantiations import SliceInstantiation, SliceInstantiations
+from PLC.Auth import Auth
+
+class GetSliceInstantiations(Method):
+    """
+    Returns an array of all valid slice instantiation states.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth()
+        ]
+
+    returns = [SliceInstantiation.fields['instantiation']]
+
+    def call(self, auth):
+        return [slice_instantiation['instantiation'] for slice_instantiation in SliceInstantiations(self.api)]
diff --git a/PLC/Methods/GetSliceKeys.py b/PLC/Methods/GetSliceKeys.py
new file mode 100644 (file)
index 0000000..4029c83
--- /dev/null
@@ -0,0 +1,134 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Slices import Slice, Slices
+from PLC.Keys import Key, Keys
+
+class GetSliceKeys(Method):
+    """
+    Returns an array of structs containing public key info for users in 
+    the specified slices. If slice_filter is specified and is an array 
+    of slice identifiers or slice names, or a struct of slice 
+    attributes, only slices matching the filter will be returned. If 
+    return_fields is specified, only the specified details will be 
+    returned.
+
+    Users may only query slices of which they are members. PIs may
+    query any of the slices at their sites. Admins and nodes may query
+    any slice. If a slice that cannot be queried is specified in
+    slice_filter, details about that slice will not be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Slice.fields['slice_id'],
+                     Slice.fields['name'])],
+              Filter(Slice.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [
+       {
+       'slice_id': Slice.fields['slice_id'],
+       'name': Slice.fields['name'], 
+       'person_id': Person.fields['person_id'], 
+       'email': Person.fields['email'],
+       'key': Key.fields['key']
+       }]
+
+    def call(self, auth, slice_filter = None, return_fields = None):
+       slice_fields = ['slice_id', 'name']
+       person_fields = ['person_id', 'email']
+       key_fields = ['key']
+
+       # If we are not admin, make sure to return only viewable
+       # slices.
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            # Get slices that we are able to view
+            valid_slice_ids = self.caller['slice_ids']
+            if 'pi' in self.caller['roles'] and self.caller['site_ids']:
+                sites = Sites(self.api, self.caller['site_ids'])
+                for site in sites:
+                    valid_slice_ids += site['slice_ids']
+
+            if not valid_slice_ids:
+                return []
+
+            if slice_filter is None:
+                slice_filter = valid_slice_ids
+
+       if return_fields:
+           slice_return_fields = filter(lambda field: field in slice_fields, return_fields)
+           person_return_fields = filter(lambda field: field in person_fields, return_fields)
+           key_return_fields = filter(lambda field: field in key_fields, return_fields)
+       else:
+           slice_return_fields = slice_fields
+           person_return_fields = person_fields
+           key_return_fields = key_fields
+
+       # Must query at least Slice.slice_id, Slice.person_ids, 
+       # and Person.person_id and Person.key_ids so we can join data correctly
+        slice_added_fields = set(['slice_id', 'person_ids']).difference(slice_return_fields)
+        slice_return_fields += slice_added_fields
+       person_added_fields = set(['person_id', 'key_ids']).difference(person_return_fields)
+       person_return_fields += person_added_fields
+       key_added_fields = set(['key_id']).difference(key_return_fields)
+       key_return_fields += key_added_fields
+
+       # Get the slices
+        all_slices = Slices(self.api, slice_filter, slice_return_fields).dict('slice_id')
+       slice_ids = all_slices.keys()
+       slices = all_slices.values()
+
+       # Filter out slices that are not viewable
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            slices = filter(lambda slice: slice['slice_id'] in valid_slice_ids, slices)
+       
+       # Get the persons
+       person_ids = set()
+       for slice in slices:
+           person_ids.update(slice['person_ids'])
+
+       all_persons = Persons(self.api, list(person_ids), person_return_fields).dict('person_id')
+       person_ids = all_persons.keys()
+       persons = all_persons.values()
+       
+       # Get the keys
+       key_ids = set()
+       for person in persons:
+           key_ids.update(person['key_ids'])
+       
+       all_keys = Keys(self.api, list(key_ids), key_return_fields).dict('key_id')
+       key_ids = all_keys.keys()
+       keys = all_keys.values()
+
+       # Create slice_keys list
+       slice_keys = []
+       slice_fields = list(set(slice_return_fields).difference(slice_added_fields))
+       person_fields = list(set(person_return_fields).difference(person_added_fields))
+       key_fields = list(set(key_return_fields).difference(key_added_fields))
+
+       for slice in slices:
+            slice_key = dict.fromkeys(slice_fields + person_fields + key_fields)
+           if not slice['person_ids']:
+               continue
+           for person_id in slice['person_ids']:
+               person = all_persons[person_id]
+               if not person['key_ids']:
+                   continue
+               for key_id in person['key_ids']:
+                   key = all_keys[key_id]
+                   slice_key.update(dict(filter(lambda (k, v): k in slice_fields, slice.items())))
+                   slice_key.update(dict(filter(lambda (k, v): k in person_fields, person.items())))
+                   slice_key.update(dict(filter(lambda (k, v): k in key_fields, key.items())))
+                   slice_keys.append(slice_key.copy())
+
+       return slice_keys       
+
diff --git a/PLC/Methods/GetSliceTicket.py b/PLC/Methods/GetSliceTicket.py
new file mode 100644 (file)
index 0000000..cd73f7b
--- /dev/null
@@ -0,0 +1,77 @@
+import time
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.GPG import gpg_sign, gpg_verify
+from PLC.InitScripts import InitScript, InitScripts
+
+from PLC.Methods.GetSlivers import get_slivers
+
+class GetSliceTicket(Method):
+    """
+    Returns a ticket for, or signed representation of, the specified
+    slice. Slice tickets may be used to manually instantiate or update
+    a slice on a node. Present this ticket to the local Node Manager
+    interface to redeem it.
+
+    If the slice has not been added to a node with AddSliceToNodes,
+    and the ticket is redeemed on that node, it will be deleted the
+    next time the Node Manager contacts the API.
+
+    Users may only obtain tickets for slices of which they are
+    members. PIs may obtain tickets for any of the slices at their
+    sites, or any slices of which they are members. Admins may obtain
+    tickets for any slice.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user', 'peer']
+
+    accepts = [
+        Auth(),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+        ]
+
+    returns = Parameter(str, 'Signed slice ticket')
+
+    def call(self, auth, slice_id_or_name):
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        # Allow peers to obtain tickets for their own slices
+        if slice['peer_id'] is not None:
+            if not isinstance(self.caller, Peer):
+                raise PLCInvalidArgument, "Not a local slice"
+            elif slice['peer_id'] != self.caller['peer_id']:
+                raise PLCInvalidArgument, "Only the authoritative peer may obtain tickets for that slice"
+
+        # Tickets are the canonicalized XML-RPC methodResponse
+        # representation of a partial GetSlivers() response, i.e.,
+       
+       initscripts = InitScripts(self.api, {'enabled': True})
+
+        data = {
+            'timestamp': int(time.time()),
+           'initscripts': initscripts,
+            'slivers': get_slivers(self.api, [slice['slice_id']]),
+            }
+
+        # Sign ticket
+        signed_ticket = gpg_sign((data,),
+                                 self.api.config.PLC_ROOT_GPG_KEY,
+                                 self.api.config.PLC_ROOT_GPG_KEY_PUB,
+                                 methodresponse = True,
+                                 detach_sign = False)
+
+        # Verify ticket
+        gpg_verify(signed_ticket,
+                   self.api.config.PLC_ROOT_GPG_KEY_PUB)
+
+        return signed_ticket
diff --git a/PLC/Methods/GetSlices.py b/PLC/Methods/GetSlices.py
new file mode 100644 (file)
index 0000000..63dc0b4
--- /dev/null
@@ -0,0 +1,75 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Persons import Person, Persons
+from PLC.Sites import Site, Sites
+from PLC.Slices import Slice, Slices
+
+class GetSlices(Method):
+    """
+    Returns an array of structs containing details about slices. If
+    slice_filter is specified and is an array of slice identifiers or
+    slice names, or a struct of slice attributes, only slices matching
+    the filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    Users may only query slices of which they are members. PIs may
+    query any of the slices at their sites. Admins and nodes may query
+    any slice. If a slice that cannot be queried is specified in
+    slice_filter, details about that slice will not be returned.
+    """
+
+    roles = ['admin', 'pi', 'user', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Slice.fields['slice_id'],
+                     Slice.fields['name'])],
+              Parameter(str,"name"),
+              Parameter(int,"slice_id"),
+              Filter(Slice.fields)),
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [Slice.fields]
+
+    def call(self, auth, slice_filter = None, return_fields = None):
+       # If we are not admin, make sure to return only viewable
+       # slices.
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            # Get slices that we are able to view
+            valid_slice_ids = self.caller['slice_ids']
+            if 'pi' in self.caller['roles'] and self.caller['site_ids']:
+                sites = Sites(self.api, self.caller['site_ids'])
+                for site in sites:
+                    valid_slice_ids += site['slice_ids']
+
+            if not valid_slice_ids:
+                return []
+
+            if slice_filter is None:
+                slice_filter = valid_slice_ids
+
+        # Must query at least slice_id (see below)
+        if return_fields is not None and 'slice_id' not in return_fields:
+            return_fields.append('slice_id')
+            added_fields = True
+        else:
+            added_fields = False
+
+        slices = Slices(self.api, slice_filter, return_fields)
+
+        # Filter out slices that are not viewable
+        if isinstance(self.caller, Person) and \
+           'admin' not in self.caller['roles']:
+            slices = filter(lambda slice: slice['slice_id'] in valid_slice_ids, slices)
+
+        # Remove slice_id if not specified
+        if added_fields:
+            for slice in slices:
+               if 'slice_id' in slice:
+                   del slice['slice_id']
+
+        return slices
diff --git a/PLC/Methods/GetSlicesMD5.py b/PLC/Methods/GetSlicesMD5.py
new file mode 100644 (file)
index 0000000..b7e4cde
--- /dev/null
@@ -0,0 +1,30 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+class GetSlicesMD5(Method):
+    """
+    Returns the current md5 hash of slices.xml file
+    (slices-0.5.xml.md5)
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node']
+
+    accepts = [
+        Auth(),
+        ]
+
+    returns = Parameter(str, "MD5 hash of slices.xml")
+    
+
+    def call(self, auth):
+       try:
+           file_path = '/var/www/html/xml/slices-0.5.xml.md5'
+           slices_md5 = file(file_path).readline().strip()
+           if slices_md5 <> "":                    
+               return slices_md5
+           raise PLCInvalidArgument, "File is empty"
+       except IOError:
+           raise PLCInvalidArgument, "No such file"
+       
diff --git a/PLC/Methods/GetSlivers.py b/PLC/Methods/GetSlivers.py
new file mode 100644 (file)
index 0000000..e23e5fc
--- /dev/null
@@ -0,0 +1,227 @@
+import time
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Slices import Slice, Slices
+from PLC.Persons import Person, Persons
+from PLC.Keys import Key, Keys
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+from PLC.InitScripts import InitScript, InitScripts
+
+def get_slivers(api, slice_filter, node = None):
+    # Get slice information
+    slices = Slices(api, slice_filter, ['slice_id', 'name', 'instantiation', 'expires', 'person_ids', 'slice_attribute_ids'])
+
+    # Build up list of users and slice attributes
+    person_ids = set()
+    slice_attribute_ids = set()
+    for slice in slices:
+        person_ids.update(slice['person_ids'])
+        slice_attribute_ids.update(slice['slice_attribute_ids'])
+
+    # Get user information
+    all_persons = Persons(api, {'person_id':person_ids,'enabled':True}, ['person_id', 'enabled', 'key_ids']).dict()
+
+    # Build up list of keys
+    key_ids = set()
+    for person in all_persons.values():
+        key_ids.update(person['key_ids'])
+
+    # Get user account keys
+    all_keys = Keys(api, key_ids, ['key_id', 'key', 'key_type']).dict()
+
+    # Get slice attributes
+    all_slice_attributes = SliceAttributes(api, slice_attribute_ids).dict()
+
+    slivers = []
+    for slice in slices:
+        keys = []
+        for person_id in slice['person_ids']:
+            if person_id in all_persons:
+                person = all_persons[person_id]
+                if not person['enabled']:
+                    continue
+                for key_id in person['key_ids']:
+                    if key_id in all_keys:
+                        key = all_keys[key_id]
+                        keys += [{'key_type': key['key_type'],
+                                  'key': key['key']}]
+
+        attributes = []
+
+        # All (per-node and global) attributes for this slice
+        slice_attributes = []
+        for slice_attribute_id in slice['slice_attribute_ids']:
+            if slice_attribute_id in all_slice_attributes:
+                slice_attributes.append(all_slice_attributes[slice_attribute_id])
+
+        # Per-node sliver attributes take precedence over global
+        # slice attributes, so set them first.
+        # Then comes nodegroup slice attributes
+       # Followed by global slice attributes
+        sliver_attributes = []
+
+        if node is not None:
+            for sliver_attribute in filter(lambda a: a['node_id'] == node['node_id'], slice_attributes):
+                sliver_attributes.append(sliver_attribute['name'])
+                attributes.append({'name': sliver_attribute['name'],
+                                   'value': sliver_attribute['value']})
+
+           # set nodegroup slice attributes
+           for slice_attribute in filter(lambda a: a['nodegroup_id'] in node['nodegroup_ids'], slice_attributes):
+               # Do not set any nodegroup slice attributes for
+                # which there is at least one sliver attribute
+                # already set.
+               if slice_attribute['name'] not in slice_attributes:
+                   attributes.append({'name': slice_attribute['name'],
+                                  'value': slice_attribute['value']})
+
+        for slice_attribute in filter(lambda a: a['node_id'] is None, slice_attributes):
+            # Do not set any global slice attributes for
+            # which there is at least one sliver attribute
+            # already set.
+            if slice_attribute['name'] not in sliver_attributes:
+                attributes.append({'name': slice_attribute['name'],
+                                   'value': slice_attribute['value']})
+
+        slivers.append({
+            'name': slice['name'],
+            'slice_id': slice['slice_id'],
+            'instantiation': slice['instantiation'],
+            'expires': slice['expires'],
+            'keys': keys,
+            'attributes': attributes
+            })
+
+    return slivers
+
+class GetSlivers(Method):
+    """
+    Returns a struct containing information about the specified node
+    (or calling node, if called by a node and node_id_or_hostname is
+    not specified), including the current set of slivers bound to the
+    node.
+
+    All of the information returned by this call can be gathered from
+    other calls, e.g. GetNodes, GetNodeNetworks, GetSlices, etc. This
+    function exists almost solely for the benefit of Node Manager.
+    """
+
+    roles = ['admin', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+        ]
+
+    returns = {
+        'timestamp': Parameter(int, "Timestamp of this call, in seconds since UNIX epoch"),
+        'node_id': Node.fields['node_id'],
+        'hostname': Node.fields['hostname'],
+        'networks': [NodeNetwork.fields],
+        'groups': [NodeGroup.fields['name']],
+        'conf_files': [ConfFile.fields],
+       'initscripts': [InitScript.fields],
+        'slivers': [{
+            'name': Slice.fields['name'],
+            'slice_id': Slice.fields['slice_id'],
+            'instantiation': Slice.fields['instantiation'],
+            'expires': Slice.fields['expires'],
+            'keys': [{
+                'key_type': Key.fields['key_type'],
+                'key': Key.fields['key']
+            }],
+            'attributes': [{
+                'name': SliceAttribute.fields['name'],
+                'value': SliceAttribute.fields['value']
+            }]
+        }]
+    }
+
+    def call(self, auth, node_id_or_hostname = None):
+        timestamp = int(time.time())
+
+        # Get node
+        if node_id_or_hostname is None:
+            if isinstance(self.caller, Node):
+                node = self.caller
+            else:
+                raise PLCInvalidArgument, "'node_id_or_hostname' not specified"
+        else:
+            nodes = Nodes(self.api, [node_id_or_hostname])
+            if not nodes:
+                raise PLCInvalidArgument, "No such node"
+            node = nodes[0]
+
+            if node['peer_id'] is not None:
+                raise PLCInvalidArgument, "Not a local node"
+
+        # Get nodenetwork information
+        networks = NodeNetworks(self.api, node['nodenetwork_ids'])
+
+        # Get node group information
+        nodegroups = NodeGroups(self.api, node['nodegroup_ids']).dict('name')
+        groups = nodegroups.keys()
+
+        # Get all (enabled) configuration files
+        all_conf_files = ConfFiles(self.api, {'enabled': True}).dict()
+        conf_files = {}
+
+        # Global configuration files are the default. If multiple
+        # entries for the same global configuration file exist, it is
+        # undefined which one takes precedence.
+        for conf_file in all_conf_files.values():
+            if not conf_file['node_ids'] and not conf_file['nodegroup_ids']:
+                conf_files[conf_file['dest']] = conf_file
+        
+        # Node group configuration files take precedence over global
+        # ones. If a node belongs to multiple node groups for which
+        # the same configuration file is defined, it is undefined
+        # which one takes precedence.
+        for nodegroup in nodegroups.values():
+            for conf_file_id in nodegroup['conf_file_ids']:
+                if conf_file_id in all_conf_files:
+                    conf_file = all_conf_files[conf_file_id]
+                    conf_files[conf_file['dest']] = conf_file
+        
+        # Node configuration files take precedence over node group
+        # configuration files.
+        for conf_file_id in node['conf_file_ids']:
+            if conf_file_id in all_conf_files:
+                conf_file = all_conf_files[conf_file_id]
+                conf_files[conf_file['dest']] = conf_file            
+
+       # Get all (enabled) initscripts
+       initscripts = InitScripts(self.api, {'enabled': True})  
+
+        # Get system slices
+        system_slice_attributes = SliceAttributes(self.api, {'name': 'system', 'value': '1'}).dict('slice_id')
+        system_slice_ids = system_slice_attributes.keys()
+       
+       # Get nm-controller slices
+       controller_and_delegated_slices = Slices(self.api, {'instantiation': ['nm-controller', 'delegated']}, ['slice_id']).dict('slice_id')
+       controller_and_delegated_slice_ids = controller_and_delegated_slices.keys()
+       slice_ids = system_slice_ids + controller_and_delegated_slice_ids + node['slice_ids']
+
+       slivers = get_slivers(self.api, slice_ids, node)
+
+       node.update_last_contact()
+
+        return {
+            'timestamp': timestamp,
+            'node_id': node['node_id'],
+            'hostname': node['hostname'],
+            'networks': networks,
+            'groups': groups,
+            'conf_files': conf_files.values(),
+           'initscripts': initscripts,
+            'slivers': slivers
+            }
diff --git a/PLC/Methods/GetWhitelist.py b/PLC/Methods/GetWhitelist.py
new file mode 100644 (file)
index 0000000..11251f8
--- /dev/null
@@ -0,0 +1,73 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Nodes import Node, Nodes
+from PLC.Persons import Person, Persons
+from PLC.Auth import Auth
+
+class GetWhitelist(Method):
+    """
+    Returns an array of structs containing details about the specified nodes 
+    whitelists. If node_filter is specified and is an array of node identifiers or
+    hostnames, or a struct of node attributes, only nodes matching the
+    filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    Some fields may only be viewed by admins.
+    """
+
+    roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Node.fields['node_id'],
+                     Node.fields['hostname'])],
+              Filter(Node.fields)),
+        Parameter([str], "List of fields to return", nullok = True),
+        ]
+
+    returns = [Node.fields]
+
+
+    def call(self, auth, node_filter = None, return_fields = None):
+        
+       # Must query at least slice_ids_whitelist
+       if return_fields is not None:
+           added_fields = set(['slice_ids_whitelist']).difference(return_fields)
+           return_fields += added_fields
+       else:
+           added_fields =[]    
+
+       # Get node information
+        nodes = Nodes(self.api, node_filter, return_fields)
+
+       # Remove all nodes without a whitelist
+       for node in nodes[:]:
+           if not node['slice_ids_whitelist']:
+               nodes.remove(node)
+
+        # Remove admin only fields
+        if not isinstance(self.caller, Person) or \
+           'admin' not in self.caller['roles']:
+           slice_ids = set()
+           if self.caller:
+               slice_ids.update(self.caller['slice_ids'])
+           #if node has whitelist, make sure the user has a slice on the whitelist 
+            for node in nodes[:]:
+               if 'slice_ids_whitelist' in node and \
+                  node['slice_ids_whitelist'] and \
+                  not slice_ids.intersection(node['slice_ids_whitelist']):
+                   nodes.remove(node)
+           for node in nodes:   
+                for field in ['boot_nonce', 'key', 'session', 'root_person_ids']:
+                    if field in node:
+                        del node[field]
+
+       # remove added fields if not specified
+       if added_fields:
+           for node in nodes:
+               for field in added_fields:
+                   del node[field]     
+
+        return nodes
diff --git a/PLC/Methods/NotifyPersons.py b/PLC/Methods/NotifyPersons.py
new file mode 100644 (file)
index 0000000..70c273d
--- /dev/null
@@ -0,0 +1,48 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Persons import Person, Persons
+from PLC.sendmail import sendmail
+
+class NotifyPersons(Method):
+    """
+    Sends an e-mail message to the specified users. If person_filter
+    is specified and is an array of user identifiers or usernames, or
+    a struct of user attributes, only users matching the filter will
+    receive the message.
+
+    Returns 1 if successful.
+    """
+
+    roles = ['admin', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed([Mixed(Person.fields['person_id'],
+                     Person.fields['email'])],
+              Filter(Person.fields)),
+        Parameter(str, "E-mail subject"),
+        Parameter(str, "E-mail body")
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, person_filter, subject, body):
+        persons = Persons(self.api, person_filter,
+                          ['person_id', 'first_name', 'last_name', 'email'])
+        if not persons:
+            raise PLCInvalidArgument, "No such user(s)"
+
+        # Send email
+        sendmail(self.api,
+                 To = [("%s %s" % (person['first_name'], person['last_name']),
+                        person['email']) for person in persons],
+                 Subject = subject,
+                 Body = body)
+
+        # Logging variables
+        self.event_objects = {'Person': [person['person_id'] for person in persons]}
+        self.message = subject
+
+        return 1
diff --git a/PLC/Methods/NotifySupport.py b/PLC/Methods/NotifySupport.py
new file mode 100644 (file)
index 0000000..99ec318
--- /dev/null
@@ -0,0 +1,36 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.sendmail import sendmail
+
+class NotifySupport(Method):
+    """
+    Sends an e-mail message to the configured support address. 
+
+    Returns 1 if successful.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Parameter(str, "E-mail subject"),
+        Parameter(str, "E-mail body")
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, subject, body):
+        to_name="%s Support"%self.api.config.PLC_NAME
+        to_address=self.api.config.PLC_MAIL_SUPPORT_ADDRESS
+
+        # Send email
+        sendmail(self.api, To=(to_name,to_address),
+                 Subject = subject,
+                 Body = body)
+
+        # Logging variables
+        #self.event_objects = {'Person': [person['person_id'] for person in persons]}
+        self.message = subject
+
+        return 1
diff --git a/PLC/Methods/RebootNode.py b/PLC/Methods/RebootNode.py
new file mode 100644 (file)
index 0000000..bea6b89
--- /dev/null
@@ -0,0 +1,73 @@
+import socket
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+from PLC.POD import udp_pod
+
+class RebootNode(Method):
+    """
+    Sends the specified node a specially formatted UDP packet which
+    should cause it to reboot immediately.
+
+    Admins can reboot any node. Techs and PIs can only reboot nodes at
+    their site.
+
+    Returns 1 if the packet was successfully sent (which only whether
+    the packet was sent, not whether the reboot was successful).
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    accepts = [
+        Auth(),
+        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[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 at which the node is located.
+        if 'admin' not in self.caller['roles']:
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to delete nodes from specified site"
+
+        session = node['session']
+        if not session:
+            raise PLCInvalidArgument, "No session key on record for that node (i.e., has never successfully booted)"
+        session = session.strip()
+
+        # Only use the hostname as a backup, try to use the primary ID
+        # address instead.
+        host = node['hostname']
+        nodenetworks = NodeNetworks(self.api, node['nodenetwork_ids'])
+        for nodenetwork in nodenetworks:
+            if nodenetwork['is_primary'] == 1:
+                host = nodenetwork['ip']
+                break
+
+        try:
+            udp_pod(host, session)
+        except socket.error, e:
+            # Ignore socket errors
+            pass
+
+       self.event_objects = {'Node': [node['node_id']]}
+       self.message = "RebootNode called"
+               
+        return 1
diff --git a/PLC/Methods/RefreshPeer.py b/PLC/Methods/RefreshPeer.py
new file mode 100644 (file)
index 0000000..a45f8bd
--- /dev/null
@@ -0,0 +1,476 @@
+#
+# Thierry Parmentelat - INRIA
+# 
+# $Id: RefreshPeer.py 5574 2007-10-25 20:33:17Z thierry $
+
+import time
+
+from PLC.Debug import log
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.Peers import Peer, Peers
+from PLC.Sites import Site, Sites
+from PLC.Persons import Person, Persons
+from PLC.KeyTypes import KeyType, KeyTypes
+from PLC.Keys import Key, Keys
+from PLC.BootStates import BootState, BootStates
+from PLC.Nodes import Node, Nodes
+from PLC.SliceInstantiations import SliceInstantiations
+from PLC.Slices import Slice, Slices
+
+verbose=False
+
+class RefreshPeer(Method):
+    """
+    Fetches site, node, slice, person and key data from the specified peer
+    and caches it locally; also deletes stale entries.
+    Upon successful completion, returns a dict reporting various timers.
+    Faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Peer.fields['peer_id'],
+              Peer.fields['peername']),
+        ]
+
+    returns = Parameter(int, "1 if successful")
+
+    def call(self, auth, peer_id_or_peername):
+        # Get peer
+       peers = Peers(self.api, [peer_id_or_peername])
+        if not peers:
+            raise PLCInvalidArgument, "No such peer '%s'" % unicode(peer_id_or_peername)
+        peer = peers[0]
+        peer_id = peer['peer_id']
+
+       # Connect to peer API
+        peer.connect()
+
+        timers = {}
+
+        # Get peer data
+        start = time.time()
+       print >>log, 'Issuing GetPeerData'
+        peer_tables = peer.GetPeerData()
+        timers['transport'] = time.time() - start - peer_tables['db_time']
+        timers['peer_db'] = peer_tables['db_time']
+        if verbose:
+            print >>log, 'GetPeerData returned -> db=%d transport=%d'%(timers['peer_db'],timers['transport'])
+
+        def sync(objects, peer_objects, classobj):
+            """
+            Synchronizes two dictionaries of objects. objects should
+            be a dictionary of local objects keyed on their foreign
+            identifiers. peer_objects should be a dictionary of
+            foreign objects keyed on their local (i.e., foreign to us)
+            identifiers. Returns a final dictionary of local objects
+            keyed on their foreign identifiers.
+            """
+
+            if verbose:
+                print >>log, 'Entering sync on',classobj(self.api).__class__.__name__
+
+            synced = {}
+
+            # Delete stale objects
+            for peer_object_id, object in objects.iteritems():
+                if peer_object_id not in peer_objects:
+                    object.delete(commit = False)
+                    print >> log, peer['peername'],classobj(self.api).__class__.__name__, object[object.primary_key],"deleted" 
+
+            # Add/update new/existing objects
+            for peer_object_id, peer_object in peer_objects.iteritems():
+                if peer_object_id in objects:
+                    # Update existing object
+                    object = objects[peer_object_id]
+
+                    # Replace foreign identifier with existing local
+                    # identifier temporarily for the purposes of
+                    # comparison.
+                    peer_object[object.primary_key] = object[object.primary_key]
+
+                    # Must use __eq__() instead of == since
+                    # peer_object may be a raw dict instead of a Peer
+                    # object.
+                    if not object.__eq__(peer_object):
+                        # Only update intrinsic fields
+                        object.update(object.db_fields(peer_object))
+                        sync = True
+                        dbg = "changed"
+                    else:
+                        sync = False
+                        dbg = None
+
+                    # Restore foreign identifier
+                    peer_object[object.primary_key] = peer_object_id
+                else:
+                    # Add new object
+                    object = classobj(self.api, peer_object)
+                    # Replace foreign identifier with new local identifier
+                    del object[object.primary_key]
+                    sync = True
+                    dbg = "added"
+
+                if sync:
+                    try:
+                        object.sync(commit = False)
+                    except PLCInvalidArgument, err:
+                        # Skip if validation fails
+                        # XXX Log an event instead of printing to logfile
+                        print >> log, "Warning: Skipping invalid", \
+                              peer['peername'], object.__class__.__name__, \
+                              ":", peer_object, ":", err
+                        continue
+
+                synced[peer_object_id] = object
+
+                if dbg:
+                    print >> log, peer['peername'], classobj(self.api).__class__.__name__, object[object.primary_key], dbg
+
+            if verbose:
+                print >>log, 'Exiting sync on',classobj(self.api).__class__.__name__
+
+            return synced
+
+        #
+        # Synchronize foreign sites
+        #
+
+        start = time.time()
+
+       print >>log, 'Dealing with Sites'
+
+        # Compare only the columns returned by the GetPeerData() call
+        if peer_tables['Sites']:
+            columns = peer_tables['Sites'][0].keys()
+        else:
+            columns = None
+
+        # Keyed on foreign site_id
+        old_peer_sites = Sites(self.api, {'peer_id': peer_id}, columns).dict('peer_site_id')
+        sites_at_peer = dict([(site['site_id'], site) for site in peer_tables['Sites']])
+
+        # Synchronize new set (still keyed on foreign site_id)
+        peer_sites = sync(old_peer_sites, sites_at_peer, Site)
+
+        for peer_site_id, site in peer_sites.iteritems():
+            # Bind any newly cached sites to peer
+            if peer_site_id not in old_peer_sites:
+                peer.add_site(site, peer_site_id, commit = False)
+                site['peer_id'] = peer_id
+                site['peer_site_id'] = peer_site_id
+
+        timers['site'] = time.time() - start
+
+        #
+        # XXX Synchronize foreign key types
+        #
+
+       print >>log, 'Dealing with Keys'
+
+        key_types = KeyTypes(self.api).dict()
+
+        #
+        # Synchronize foreign keys
+        #
+
+        start = time.time()
+
+        # Compare only the columns returned by the GetPeerData() call
+        if peer_tables['Keys']:
+            columns = peer_tables['Keys'][0].keys()
+        else:
+            columns = None
+
+        # Keyed on foreign key_id
+        old_peer_keys = Keys(self.api, {'peer_id': peer_id}, columns).dict('peer_key_id')
+        keys_at_peer = dict([(key['key_id'], key) for key in peer_tables['Keys']])
+
+        # Fix up key_type references
+        for peer_key_id, key in keys_at_peer.items():
+            if key['key_type'] not in key_types:
+                # XXX Log an event instead of printing to logfile
+                print >> log, "Warning: Skipping invalid %s key:" % peer['peername'], \
+                      key, ": invalid key type", key['key_type']
+                del keys_at_peer[peer_key_id]
+                continue
+
+        # Synchronize new set (still keyed on foreign key_id)
+        peer_keys = sync(old_peer_keys, keys_at_peer, Key)
+        for peer_key_id, key in peer_keys.iteritems():
+            # Bind any newly cached keys to peer
+            if peer_key_id not in old_peer_keys:
+                peer.add_key(key, peer_key_id, commit = False)
+                key['peer_id'] = peer_id
+                key['peer_key_id'] = peer_key_id
+
+        timers['keys'] = time.time() - start
+
+        #
+        # Synchronize foreign users
+        #
+
+        start = time.time()
+
+       print >>log, 'Dealing with Persons'
+
+        # Compare only the columns returned by the GetPeerData() call
+        if peer_tables['Persons']:
+            columns = peer_tables['Persons'][0].keys()
+        else:
+            columns = None
+
+        # Keyed on foreign person_id
+        old_peer_persons = Persons(self.api, {'peer_id': peer_id}, columns).dict('peer_person_id')
+
+       # artificially attach the persons returned by GetPeerData to the new peer 
+       # this is because validate_email needs peer_id to be correct when checking for duplicates 
+       for person in peer_tables['Persons']: 
+           person['peer_id']=peer_id
+        persons_at_peer = dict([(peer_person['person_id'], peer_person) \
+                                for peer_person in peer_tables['Persons']])
+
+        # XXX Do we care about membership in foreign site(s)?
+
+        # Synchronize new set (still keyed on foreign person_id)
+        peer_persons = sync(old_peer_persons, persons_at_peer, Person)
+
+       # transcoder : retrieve a local key_id from a peer_key_id
+       key_transcoder = dict ( [ (key['key_id'],peer_key_id) \
+                                 for peer_key_id,key in peer_keys.iteritems()])
+
+        for peer_person_id, person in peer_persons.iteritems():
+            # Bind any newly cached users to peer
+            if peer_person_id not in old_peer_persons:
+                peer.add_person(person, peer_person_id, commit = False)
+                person['peer_id'] = peer_id
+                person['peer_person_id'] = peer_person_id
+                person['key_ids'] = []
+
+            # User as viewed by peer
+            peer_person = persons_at_peer[peer_person_id]
+            
+            # Foreign keys currently belonging to the user
+           old_person_key_ids = [key_transcoder[key_id] for key_id in person['key_ids'] \
+                                 if key_transcoder[key_id] in peer_keys]
+
+            # Foreign keys that should belong to the user
+           # this is basically peer_person['key_ids'], we just check it makes sense 
+           # (e.g. we might have failed importing it)
+           person_key_ids = [ key_id for key_id in peer_person['key_ids'] if key_id in peer_keys]
+
+            # Remove stale keys from user
+           for key_id in (set(old_person_key_ids) - set(person_key_ids)):
+               person.remove_key(peer_keys[key_id], commit = False)
+               print >> log, peer['peername'], 'Key', key_id, 'removed from', person['email']
+
+            # Add new keys to user
+           for key_id in (set(person_key_ids) - set(old_person_key_ids)):
+               person.add_key(peer_keys[key_id], commit = False)
+               print >> log, peer['peername'], 'Key', key_id, 'added into', person['email']
+
+        timers['persons'] = time.time() - start
+
+        #
+        # XXX Synchronize foreign boot states
+        #
+
+        boot_states = BootStates(self.api).dict()
+
+        #
+        # Synchronize foreign nodes
+        #
+
+        start = time.time()
+
+       print >>log, 'Dealing with Nodes'
+
+        # Compare only the columns returned by the GetPeerData() call
+        if peer_tables['Nodes']:
+            columns = peer_tables['Nodes'][0].keys()
+        else:
+            columns = None
+
+        # Keyed on foreign node_id
+        old_peer_nodes = Nodes(self.api, {'peer_id': peer_id}, columns).dict('peer_node_id')
+        nodes_at_peer = dict([(node['node_id'], node) \
+                              for node in peer_tables['Nodes']])
+
+        # Fix up site_id and boot_states references
+        for peer_node_id, node in nodes_at_peer.items():
+            errors = []
+            if node['site_id'] not in peer_sites:
+                errors.append("invalid site %d" % node['site_id'])
+            if node['boot_state'] not in boot_states:
+                errors.append("invalid boot state %s" % node['boot_state'])
+            if errors:
+                # XXX Log an event instead of printing to logfile
+                print >> log, "Warning: Skipping invalid %s node:" % peer['peername'], \
+                      node, ":", ", ".join(errors)
+                del nodes_at_peer[peer_node_id]
+                continue
+            else:
+                node['site_id'] = peer_sites[node['site_id']]['site_id']
+
+        # Synchronize new set
+        peer_nodes = sync(old_peer_nodes, nodes_at_peer, Node)
+
+        for peer_node_id, node in peer_nodes.iteritems():
+            # Bind any newly cached foreign nodes to peer
+            if peer_node_id not in old_peer_nodes:
+                peer.add_node(node, peer_node_id, commit = False)
+                node['peer_id'] = peer_id
+                node['peer_node_id'] = peer_node_id
+
+        timers['nodes'] = time.time() - start
+
+        #
+        # Synchronize local nodes
+        #
+
+        start = time.time()
+
+        # Keyed on local node_id
+        local_nodes = Nodes(self.api).dict()
+
+        for node in peer_tables['PeerNodes']:
+            # Foreign identifier for our node as maintained by peer
+            peer_node_id = node['node_id']
+            # Local identifier for our node as cached by peer
+            node_id = node['peer_node_id']
+            if node_id in local_nodes:
+                # Still a valid local node, add it to the synchronized
+                # set of local node objects keyed on foreign node_id.
+                peer_nodes[peer_node_id] = local_nodes[node_id]
+
+        timers['local_nodes'] = time.time() - start
+
+        #
+        # XXX Synchronize foreign slice instantiation states
+        #
+
+        slice_instantiations = SliceInstantiations(self.api).dict()
+
+        #
+        # Synchronize foreign slices
+        #
+
+        start = time.time()
+
+       print >>log, 'Dealing with Slices'
+
+        # Compare only the columns returned by the GetPeerData() call
+        if peer_tables['Slices']:
+            columns = peer_tables['Slices'][0].keys()
+        else:
+            columns = None
+
+        # Keyed on foreign slice_id
+        old_peer_slices = Slices(self.api, {'peer_id': peer_id}, columns).dict('peer_slice_id')
+        slices_at_peer = dict([(slice['slice_id'], slice) \
+                               for slice in peer_tables['Slices']])
+
+        # Fix up site_id, instantiation, and creator_person_id references
+        for peer_slice_id, slice in slices_at_peer.items():
+            errors = []
+            if slice['site_id'] not in peer_sites:
+                errors.append("invalid site %d" % slice['site_id'])
+            if slice['instantiation'] not in slice_instantiations:
+                errors.append("invalid instantiation %s" % slice['instantiation'])
+            if slice['creator_person_id'] not in peer_persons:
+                # Just NULL it out
+                slice['creator_person_id'] = None
+            else:
+                slice['creator_person_id'] = peer_persons[slice['creator_person_id']]['person_id']
+            if errors:
+                print >> log, "Warning: Skipping invalid %s slice:" % peer['peername'], \
+                      slice, ":", ", ".join(errors)
+                del slices_at_peer[peer_slice_id]
+                continue
+            else:
+                slice['site_id'] = peer_sites[slice['site_id']]['site_id']
+
+        # Synchronize new set
+        peer_slices = sync(old_peer_slices, slices_at_peer, Slice)
+
+       # transcoder : retrieve a local node_id from a peer_node_id
+       node_transcoder = dict ( [ (node['node_id'],peer_node_id) \
+                                  for peer_node_id,node in peer_nodes.iteritems()])
+       person_transcoder = dict ( [ (person['person_id'],peer_person_id) \
+                                    for peer_person_id,person in peer_persons.iteritems()])
+
+        for peer_slice_id, slice in peer_slices.iteritems():
+            # Bind any newly cached foreign slices to peer
+            if peer_slice_id not in old_peer_slices:
+                peer.add_slice(slice, peer_slice_id, commit = False)
+                slice['peer_id'] = peer_id
+                slice['peer_slice_id'] = peer_slice_id
+                slice['node_ids'] = []
+                slice['person_ids'] = []
+
+            # Slice as viewed by peer
+            peer_slice = slices_at_peer[peer_slice_id]
+
+            # Nodes that are currently part of the slice
+           old_slice_node_ids = [ node_transcoder[node_id] for node_id in slice['node_ids'] \
+                                  if node_transcoder[node_id] in peer_nodes]
+
+            # Nodes that should be part of the slice
+           slice_node_ids = [ node_id for node_id in peer_slice['node_ids'] if node_id in peer_nodes]
+
+            # Remove stale nodes from slice
+            for node_id in (set(old_slice_node_ids) - set(slice_node_ids)):
+                slice.remove_node(peer_nodes[node_id], commit = False)
+               print >> log, peer['peername'], 'Node', peer_nodes[node_id]['hostname'], 'removed from', slice['name']
+
+            # Add new nodes to slice
+            for node_id in (set(slice_node_ids) - set(old_slice_node_ids)):
+                slice.add_node(peer_nodes[node_id], commit = False)
+               print >> log, peer['peername'], 'Node', peer_nodes[node_id]['hostname'], 'added into', slice['name']
+
+            # N.B.: Local nodes that may have been added to the slice
+            # by hand, are removed. In other words, don't do this.
+
+            # Foreign users that are currently part of the slice
+           #old_slice_person_ids = [ person_transcoder[person_id] for person_id in slice['person_ids'] \
+           #                if person_transcoder[person_id] in peer_persons]
+           # An issue occurred with a user who registered on both sites (same email)
+           # So the remote person could not get cached locally
+           # The one-line map/filter style is nicer but ineffective here
+           old_slice_person_ids = []
+           for person_id in slice['person_ids']:
+               if not person_transcoder.has_key(person_id):
+                   print >> log, 'WARNING : person_id %d in %s not transcodable (1) - skipped'%(person_id,slice['name'])
+               elif person_transcoder[person_id] not in peer_persons:
+                   print >> log, 'WARNING : person_id %d in %s not transcodable (2) - skipped'%(person_id,slice['name'])
+               else:
+                   old_slice_person_ids += [person_transcoder[person_id]]
+
+            # Foreign users that should be part of the slice
+           slice_person_ids = [ person_id for person_id in peer_slice['person_ids'] if person_id in peer_persons ]
+
+            # Remove stale users from slice
+            for person_id in (set(old_slice_person_ids) - set(slice_person_ids)):
+                slice.remove_person(peer_persons[person_id], commit = False)
+               print >> log, peer['peername'], 'User', peer_persons[person_id]['email'], 'removed from', slice['name']
+
+            # Add new users to slice
+            for person_id in (set(slice_person_ids) - set(old_slice_person_ids)):
+                slice.add_person(peer_persons[person_id], commit = False)
+               print >> log, peer['peername'], 'User', peer_persons[person_id]['email'], 'added into', slice['name']
+
+            # N.B.: Local users that may have been added to the slice
+            # by hand, are not touched.
+
+        timers['slices'] = time.time() - start
+
+        # Update peer itself and commit
+        peer.sync(commit = True)
+
+        return timers
diff --git a/PLC/Methods/ResetPassword.py b/PLC/Methods/ResetPassword.py
new file mode 100644 (file)
index 0000000..0e2d2a9
--- /dev/null
@@ -0,0 +1,128 @@
+import random
+import base64
+import time
+import urllib
+
+from types import StringTypes
+
+from PLC.Debug import log
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Messages import Message, Messages
+from PLC.Auth import Auth
+from PLC.sendmail import sendmail
+
+class ResetPassword(Method):
+    """
+    If verification_key is not specified, then a new verification_key
+    will be generated and stored with the user's account. The key will
+    be e-mailed to the user in the form of a link to a web page.
+
+    The web page should verify the key by calling this function again
+    and specifying verification_key. If the key matches what has been
+    stored in the user's account, a new random password will be
+    e-mailed to the user.
+
+    Returns 1 if verification_key was not specified, or was specified
+    and is valid, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+       Person.fields['verification_key'],
+        Person.fields['verification_expires']
+        ]
+
+    returns = Parameter(int, '1 if verification_key is valid')
+
+    def call(self, auth, person_id_or_email, verification_key = None, verification_expires = None):
+       # Get account information
+        # we need to search in local objects only
+        if isinstance (person_id_or_email,StringTypes):
+            filter={'email':person_id_or_email}
+        else:
+            filter={'person_id':person_id_or_email}
+        filter['peer_id']=None
+        persons = Persons(self.api, filter)
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        if not person['enabled']:
+            raise PLCInvalidArgument, "Account must be enabled"
+
+        # Be paranoid and deny password resets for admins
+        if 'admin' in person['roles']:
+            raise PLCInvalidArgument, "Cannot reset admin passwords"
+
+        # Generate 32 random bytes
+        bytes = random.sample(xrange(0, 256), 32)
+        # Base64 encode their string representation
+        random_key = base64.b64encode("".join(map(chr, bytes)))
+
+        if verification_key is not None:
+            if person['verification_key'] is None or \
+               person['verification_expires'] is None or \
+               person['verification_expires'] < time.time():
+                raise PLCPermissionDenied, "Verification key has expired"
+            elif person['verification_key'] != verification_key:
+                raise PLCPermissionDenied, "Verification key incorrect"
+            else:
+                # Reset password to random string
+                person['password'] = random_key
+                person['verification_key'] = None
+                person['verification_expires'] = None
+                person.sync()
+
+                message_id = 'Password reset'
+        else:
+            # Only allow one reset at a time
+            if person['verification_expires'] is not None and \
+               person['verification_expires'] > time.time():
+                raise PLCPermissionDenied, "Password reset request already pending"
+
+            if verification_expires is None:
+                verification_expires = int(time.time() + (24 * 60 * 60))
+
+            person['verification_key'] = random_key
+            person['verification_expires'] = verification_expires
+            person.sync()
+
+            message_id = 'Password reset requested'
+
+        messages = Messages(self.api, [message_id])
+        if messages:
+            # Send password to user
+            message = messages[0]
+
+            params = {'PLC_NAME': self.api.config.PLC_NAME,
+                      'PLC_MAIL_SUPPORT_ADDRESS': self.api.config.PLC_MAIL_SUPPORT_ADDRESS,
+                      'PLC_WWW_HOST': self.api.config.PLC_WWW_HOST,
+                      'PLC_WWW_SSL_PORT': self.api.config.PLC_WWW_SSL_PORT,
+                      'person_id': person['person_id'],
+                      # Will be used in a URL, so must quote appropriately
+                      'verification_key': urllib.quote_plus(random_key),
+                      'password': random_key,
+                      'email': person['email']}
+
+            sendmail(self.api,
+                     To = ("%s %s" % (person['first_name'], person['last_name']), person['email']),
+                     Subject = message['subject'] % params,
+                     Body = message['template'] % params)
+        else:
+            print >> log, "Warning: No message template '%s'" % message_id
+
+       # Logging variables
+        self.event_objects = {'Person': [person['person_id']]}
+        self.message = message_id
+
+        return 1
diff --git a/PLC/Methods/SetPersonPrimarySite.py b/PLC/Methods/SetPersonPrimarySite.py
new file mode 100644 (file)
index 0000000..644826b
--- /dev/null
@@ -0,0 +1,62 @@
+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 Auth
+
+class SetPersonPrimarySite(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 = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    object_type = 'Person'
+
+    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[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        # 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[0]
+
+        if site['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local site"
+
+        if site['site_id'] not in person['site_ids']:
+            raise PLCInvalidArgument, "Not a member of the specified site"
+
+        person.set_primary_site(site)
+
+        return 1
diff --git a/PLC/Methods/SliceCreate.py b/PLC/Methods/SliceCreate.py
new file mode 100644 (file)
index 0000000..cc30b52
--- /dev/null
@@ -0,0 +1,25 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Slices import Slice, Slices
+from PLC.Methods.AddSlice import AddSlice
+
+class SliceCreate(AddSlice):
+    """
+    Deprecated. See AddSlice.
+    """
+
+    status = "deprecated"
+    
+    accepts = [
+        Auth(),
+        Slice.fields['name'],
+        AddSlice.accepts[1]
+        ]
+    
+    returns = Parameter(int, 'New slice_id (> 0) if successful')
+
+    def call(self, auth, name, slice_fields = {}):
+        slice_fields['name'] = name
+        return AddSlice.call(self, auth, slice_fields)
diff --git a/PLC/Methods/SliceDelete.py b/PLC/Methods/SliceDelete.py
new file mode 100644 (file)
index 0000000..70f6696
--- /dev/null
@@ -0,0 +1,29 @@
+import re
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Methods.DeleteSlice import DeleteSlice
+
+class SliceDelete(DeleteSlice):
+    """
+    Deprecated. See DeleteSlice.
+
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name']
+        ]
+
+    returns = Parameter(int, 'Returns 1 if successful, a fault otherwise.')
+
+    def call(self, auth, slice_name):
+
+       return DeleteSlice.call(self, auth, slice_name)
diff --git a/PLC/Methods/SliceExtendedInfo.py b/PLC/Methods/SliceExtendedInfo.py
new file mode 100644 (file)
index 0000000..1211610
--- /dev/null
@@ -0,0 +1,84 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Slices import Slice, Slices
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+from PLC.Sites import Site, Sites
+from PLC.Nodes import Node, Nodes
+from PLC.Persons import Person, Persons
+
+class SliceExtendedInfo(Method):
+    """
+    Deprecated. Can be implemented with GetSlices.
+
+    Returns an array of structs containing details about slices. 
+    The summary can optionally include the list of nodes in and 
+    users of each slice.
+
+    Users may only query slices of which they are members. PIs may
+    query any of the slices at their sites. Admins may query any
+    slice. If a slice that cannot be queried is specified in
+    slice_filter, details about that slice will not be returned.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        [Slice.fields['name']],
+        Parameter(bool, "Whether or not to return users for the slices", nullok = True),
+       Parameter(bool, "Whether or not to return nodes for the slices", nullok = True)
+        ]
+
+    returns = [Slice.fields]
+    
+
+    def call(self, auth, slice_name_list=None, return_users=None, return_nodes=None, return_attributes=None):
+       # If we are not admin, make sure to return only viewable
+       # slices.
+       slice_filter = slice_name_list
+       slices = Slices(self.api, slice_filter)
+       if not slices:
+            raise PLCInvalidArgument, "No such slice"
+
+        if 'admin' not in self.caller['roles']:
+            # Get slices that we are able to view
+            valid_slice_ids = self.caller['slice_ids']
+            if 'pi' in self.caller['roles'] and self.caller['site_ids']:
+                sites = Sites(self.api, self.caller['site_ids'])
+                for site in sites:
+                    valid_slice_ids += site['slice_ids']
+
+            if not valid_slice_ids:
+                return []
+         
+           slices = filter(lambda slice: slice['slice_id'] in valid_slice_ids, slices)
+
+       for slice in slices:
+           index = slices.index(slice)
+            node_ids = slices[index].pop('node_ids')
+            person_ids = slices[index].pop('person_ids')
+           attribute_ids = slices[index].pop('slice_attribute_ids')
+            if return_users or return_users is None:
+                persons = Persons(self.api, person_ids)
+                person_info = [{'email': person['email'], 
+                               'person_id': person['person_id']} \
+                              for person in persons]
+                slices[index]['users'] = person_info
+            if return_nodes or return_nodes is None:
+                nodes = Nodes(self.api, node_ids)
+                node_info = [{'hostname': node['hostname'], 
+                             'node_id': node['node_id']} \
+                            for node in nodes]
+                slices[index]['nodes'] = node_info
+           if return_attributes or return_attributes is None:
+               attributes = SliceAttributes(self.api, attribute_ids)
+               attribute_info = [{'name': attribute['name'],
+                                  'value': attribute['value']} \
+                                 for attribute in attributes]
+               slices[index]['attributes'] = attribute_info
+       
+        return slices
diff --git a/PLC/Methods/SliceGetTicket.py b/PLC/Methods/SliceGetTicket.py
new file mode 100644 (file)
index 0000000..64413c9
--- /dev/null
@@ -0,0 +1,249 @@
+import os
+import sys
+from subprocess import Popen, PIPE, call
+from tempfile import NamedTemporaryFile
+from xml.sax.saxutils import escape, quoteattr, XMLGenerator
+
+from PLC.Faults import *
+from PLC.Slices import Slice, Slices
+from PLC.Nodes import Node, Nodes
+from PLC.Persons import Person, Persons
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+
+from PLC.Methods.GetSliceTicket import GetSliceTicket
+
+class PrettyXMLGenerator(XMLGenerator):
+    """
+    Adds indentation to the beginning and newlines to the end of
+    opening and closing tags.
+    """
+
+    def __init__(self, out = sys.stdout, encoding = "utf-8", indent = "", addindent = "", newl = ""):
+        XMLGenerator.__init__(self, out, encoding)
+        # XMLGenerator does not export _write()
+        self.write = self.ignorableWhitespace
+        self.indents = [indent]
+        self.addindent = addindent
+        self.newl = newl
+
+    def startDocument(self):
+        XMLGenerator.startDocument(self)
+
+    def startElement(self, name, attrs, indent = True, newl = True):
+        if indent:
+            self.ignorableWhitespace("".join(self.indents))
+        self.indents.append(self.addindent)
+
+        XMLGenerator.startElement(self, name, attrs)
+
+        if newl:
+            self.ignorableWhitespace(self.newl)
+
+    def characters(self, content):
+        # " to &quot;
+        # ' to &apos;
+        self.write(escape(content, {
+            '"': '&quot;',
+            "'": '&apos;',
+            }))
+
+    def endElement(self, name, indent = True, newl = True):
+        self.indents.pop()
+        if indent:
+            self.ignorableWhitespace("".join(self.indents))
+
+        XMLGenerator.endElement(self, name)
+
+        if newl:
+            self.ignorableWhitespace(self.newl)
+
+    def simpleElement(self, name, attrs = {}, indent = True, newl = True):
+        if indent:
+            self.ignorableWhitespace("".join(self.indents))
+
+        self.write('<' + name)
+        for (name, value) in attrs.items():
+            self.write(' %s=%s' % (name, quoteattr(value)))
+        self.write('/>')
+
+        if newl:
+            self.ignorableWhitespace(self.newl)
+
+class SliceGetTicket(GetSliceTicket):
+    """
+    Deprecated. See GetSliceTicket.
+
+    Warning: This function exists solely for backward compatibility
+    with the old public PlanetLab 3.0 Node Manager, which will be
+    removed from service by 2007. This call is not intended to be used
+    by any other PLC except the public PlanetLab.
+    """
+
+    status = "deprecated"
+
+    def call(self, auth, slice_id_or_name):
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        # Allow peers to obtain tickets for their own slices
+        if slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local slice"
+
+        if slice['instantiation'] != 'delegated':
+            raise PLCInvalidArgument, "Not in delegated state"
+
+        nodes = Nodes(self.api, slice['node_ids']).dict()
+        persons = Persons(self.api, slice['person_ids']).dict()
+        slice_attributes = SliceAttributes(self.api, slice['slice_attribute_ids']).dict()
+
+        ticket = NamedTemporaryFile()
+
+        xml = PrettyXMLGenerator(out = ticket, encoding = self.api.encoding, indent = "", addindent = "  ", newl = "\n")
+        xml.startDocument()
+
+        # <ticket>
+        xml.startElement('ticket', {})
+
+        # <slice name="site_slice" id="12345" expiry="1138712648">
+        xml.startElement('slice',
+                         {'id': str(slice['slice_id']),
+                          'name': unicode(slice['name']),
+                          'expiry': unicode(int(slice['expires']))})
+        
+        # <nodes>
+        xml.startElement('nodes', {})
+        for node_id in slice['node_ids']:
+            if not nodes.has_key(node_id):
+                continue
+            node = nodes[node_id]
+            # <node id="12345" hostname="node.site.domain"/>
+            xml.simpleElement('node',
+                              {'id': str(node['node_id']),
+                               'hostname': unicode(node['hostname'])})
+        # </nodes>
+        xml.endElement('nodes')
+
+        # <users>
+        xml.startElement('users', {})
+        for person_id in slice['person_ids']:
+            if not persons.has_key(person_id):
+                continue
+            user = persons[person_id]
+            # <user person_id="12345" email="user@site.domain"/>
+            xml.simpleElement('user',
+                              {'person_id': unicode(user['person_id']),
+                               'email': unicode(user['email'])})
+        # </users>
+        xml.endElement('users')
+
+        # <rspec>
+        xml.startElement('rspec', {})
+        for slice_attribute_id in slice['slice_attribute_ids']:
+            if not slice_attributes.has_key(slice_attribute_id):
+                continue
+            slice_attribute = slice_attributes[slice_attribute_id]
+
+            name = slice_attribute['name']
+            value = slice_attribute['value']
+
+            def kbps_to_bps(kbps):
+                bps = int(kbps) * 1000
+                return bps
+
+            def max_kbyte_to_bps(max_kbyte):
+                bps = int(max_kbyte) * 1000 * 8 / 24 / 60 / 60
+                return bps
+
+            # XXX Used to support multiple named values for each attribute type
+            name_type_cast = {
+                'cpu_share': ('nm_cpu_share', 'cpu_share', 'integer', int),
+
+                'net_share': ('nm_net_share', 'rate', 'integer', int),
+                'net_min_rate': ('nm_net_min_rate', 'rate', 'integer', int),
+                'net_max_rate': ('nm_net_max_rate', 'rate', 'integer', int),
+                'net_max_kbyte': ('nm_net_avg_rate', 'rate', 'integer', max_kbyte_to_bps),
+
+                'net_i2_share': ('nm_net_exempt_share', 'rate', 'integer', int),
+                'net_i2_min_rate': ('nm_net_exempt_min_rate', 'rate', 'integer', kbps_to_bps),
+                'net_i2_max_rate': ('nm_net_exempt_max_rate', 'rate', 'integer', kbps_to_bps),
+                'net_i2_max_kbyte': ('nm_net_exempt_avg_rate', 'rate', 'integer', max_kbyte_to_bps),
+
+                'disk_max': ('nm_disk_quota', 'quota', 'integer', int),
+                'plc_agent_version': ('plc_agent_version', 'version', 'string', str),
+                'plc_slice_type': ('plc_slice_type', 'type', 'string', str),
+                'plc_ticket_pubkey': ('plc_ticket_pubkey', 'key', 'string', str),
+                }
+
+            if name == 'initscript':
+                (attribute_name, value_name, type) = ('initscript', 'initscript_id', 'integer')
+                value = slice_attribute['slice_attribute_id']
+            elif name in name_type_cast:
+                (attribute_name, value_name, type, cast) = name_type_cast[name]
+                value = cast(value)
+            else:
+                attribute_name = value_name = name
+                type = "string"
+
+            # <resource name="slice_attribute_type">
+            xml.startElement('resource', {'name': unicode(attribute_name)})
+
+            # <value name="element_name" type="element_type">
+            xml.startElement('value',
+                             {'name': unicode(value_name),
+                              'type': type},
+                             newl = False)
+            # element value
+            xml.characters(unicode(value))
+            # </value>
+            xml.endElement('value', indent = False)
+
+            # </resource>
+            xml.endElement('resource')
+        # </rspec>
+        xml.endElement('rspec')
+
+        # </slice>
+        xml.endElement('slice')
+
+        # Add signature template
+        xml.startElement('Signature', {'xmlns': "http://www.w3.org/2000/09/xmldsig#"})
+        xml.startElement('SignedInfo', {})
+        xml.simpleElement('CanonicalizationMethod', {'Algorithm': "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"})
+        xml.simpleElement('SignatureMethod', {'Algorithm': "http://www.w3.org/2000/09/xmldsig#rsa-sha1"})
+        xml.startElement('Reference', {'URI': ""})
+        xml.startElement('Transforms', {})
+        xml.simpleElement('Transform', {'Algorithm': "http://www.w3.org/2000/09/xmldsig#enveloped-signature"})
+        xml.endElement('Transforms')
+        xml.simpleElement('DigestMethod', {'Algorithm': "http://www.w3.org/2000/09/xmldsig#sha1"})
+        xml.simpleElement('DigestValue', {})
+        xml.endElement('Reference')
+        xml.endElement('SignedInfo')
+        xml.simpleElement('SignatureValue', {})
+        xml.endElement('Signature')
+
+        xml.endElement('ticket')
+        xml.endDocument()
+
+        if not hasattr(self.api.config, 'PLC_API_TICKET_KEY') or \
+           not os.path.exists(self.api.config.PLC_API_TICKET_KEY):
+            raise PLCAPIError, "Slice ticket signing key not found"
+
+        ticket.flush()
+
+        # Sign the ticket
+        p = Popen(["xmlsec1", "--sign",
+                   "--privkey-pem", self.api.config.PLC_API_TICKET_KEY,
+                   ticket.name],
+                  stdin = PIPE, stdout = PIPE, stderr = PIPE, close_fds = True)
+        signed_ticket = p.stdout.read()
+        err = p.stderr.read()
+        rc = p.wait()
+
+        ticket.close()
+
+        if rc:
+            raise PLCAPIError, err
+
+        return signed_ticket
diff --git a/PLC/Methods/SliceInfo.py b/PLC/Methods/SliceInfo.py
new file mode 100644 (file)
index 0000000..9645f99
--- /dev/null
@@ -0,0 +1,75 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Faults import *
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Slices import Slice, Slices
+from PLC.Sites import Site, Sites
+from PLC.Persons import Person, Persons
+from PLC.Nodes import Node, Nodes
+
+class SliceInfo(Method):
+    """
+    Deprecated. Can be implemented with GetSlices.
+
+    Returns an array of structs containing details about slices. 
+    The summary can optionally include the list of nodes in and 
+    users of each slice.
+
+    Users may only query slices of which they are members. PIs may
+    query any of the slices at their sites. Admins may query any
+    slice. If a slice that cannot be queried is specified in
+    slice_filter, details about that slice will not be returned.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        [Mixed(Slice.fields['name'])],
+        Parameter(bool, "Whether or not to return users for the slices", nullok = True),
+       Parameter(bool, "Whether or not to return nodes for the slices", nullok = True)
+        ]
+
+    returns = [Slice.fields]
+    
+
+    def call(self, auth, slice_name_list=None, return_users=None, return_nodes=None):
+       # If we are not admin, make sure to return only viewable
+       # slices.
+       slice_filter = slice_name_list
+       slices = Slices(self.api, slice_filter)
+       if not slices:
+            raise PLCInvalidArgument, "No such slice"
+
+        if 'admin' not in self.caller['roles']:
+            # Get slices that we are able to view
+            valid_slice_ids = self.caller['slice_ids']
+            if 'pi' in self.caller['roles'] and self.caller['site_ids']:
+                sites = Sites(self.api, self.caller['site_ids'])
+                for site in sites:
+                    valid_slice_ids += site['slice_ids']
+
+            if not valid_slice_ids:
+                return []
+       
+           slices = filter(lambda slice: slice['slice_id'] in valid_slice_ids, slices)
+
+
+       for slice in slices:
+           index = slices.index(slice)
+           node_ids = slices[index].pop('node_ids')
+           person_ids = slices[index].pop('person_ids')
+           if return_users or return_users is None:
+               persons = Persons(self.api, person_ids)
+               emails = [person['email'] for person in persons]
+               slices[index]['users'] = emails
+           if return_nodes or return_nodes is None:
+               nodes = Nodes(self.api, node_ids)
+               hostnames = [node['hostname'] for node in nodes]
+               slices[index]['nodes'] = hostnames
+               
+       
+        return slices
diff --git a/PLC/Methods/SliceListNames.py b/PLC/Methods/SliceListNames.py
new file mode 100644 (file)
index 0000000..4d94933
--- /dev/null
@@ -0,0 +1,45 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Slices import Slice, Slices
+from PLC.Methods.GetSlices import GetSlices
+
+class SliceListNames(GetSlices):
+    """
+    Deprecated. Can be implemented with GetSlices.
+
+    List the names of registered slices.
+
+    Users may only query slices of which they are members. PIs may
+    query any of the slices at their sites. Admins may query any
+    slice. If a slice that cannot be queried is specified in
+    slice_filter, details about that slice will not be returned.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Parameter(str, "Slice prefix", nullok = True)
+        ]
+
+    returns = [Slice.fields['name']]
+    
+
+    def call(self, auth, prefix=None):
+
+       slice_filter = None
+        if prefix:
+            slice_filter = {'name': prefix+'*'}
+       
+        slices = GetSlices.call(self, auth, slice_filter)
+       
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+       
+       slice_names = [slice['name'] for slice in slices]
+
+        return slice_names
diff --git a/PLC/Methods/SliceListUserSlices.py b/PLC/Methods/SliceListUserSlices.py
new file mode 100644 (file)
index 0000000..9e054ed
--- /dev/null
@@ -0,0 +1,47 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Slices import Slice, Slices
+from PLC.Persons import Person, Persons
+from PLC.Methods.GetSlices import GetSlices
+from PLC.Methods.GetPersons import GetPersons
+
+class SliceListUserSlices(GetSlices, GetPersons):
+    """
+    Deprecated. Can be implemented with GetPersons and GetSlices.
+
+    Return the slices the specified user (by email address) is a member of.
+
+    Users may only query slices of which they are members. PIs may
+    query any of the slices at their sites. Admins may query any
+    slice. If a slice that cannot be queried is specified in
+    slice_filter, details about that slice will not be returned.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Person.fields['email']
+        ]
+
+    returns = [Slice.fields['name']]
+    
+
+    def call(self, auth, email):
+
+       persons = GetPersons.call(self, auth, [email])
+       if not persons:
+               return []
+       person = persons[0]
+       slice_ids = person['slice_ids']
+       if not slice_ids:
+               return []
+       
+       slices = GetSlices.call(self, auth, slice_ids)
+       slice_names = [slice['name'] for slice in slices]
+
+        return slice_names
diff --git a/PLC/Methods/SliceNodesAdd.py b/PLC/Methods/SliceNodesAdd.py
new file mode 100644 (file)
index 0000000..35ccabe
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Methods.AddSliceToNodes import AddSliceToNodes
+
+class SliceNodesAdd(AddSliceToNodes):
+    """
+    Deprecated. See AddSliceToNodes.
+
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name'],
+        [Node.fields['hostname']]
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_name, nodes_list):
+
+       return AddSliceToNodes.call(self, auth, slice_name, nodes_list)
diff --git a/PLC/Methods/SliceNodesDel.py b/PLC/Methods/SliceNodesDel.py
new file mode 100644 (file)
index 0000000..66c0ed2
--- /dev/null
@@ -0,0 +1,29 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
+
+class SliceNodesDel(DeleteSliceFromNodes):
+    """
+    Deprecated. See DeleteSliceFromNodes.
+
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name'],
+        [Node.fields['hostname']]
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_name, nodes_list):
+
+       return DeleteSliceFromNodes.call(self, auth, slice_name, nodes_list)
diff --git a/PLC/Methods/SliceNodesList.py b/PLC/Methods/SliceNodesList.py
new file mode 100644 (file)
index 0000000..0c44f66
--- /dev/null
@@ -0,0 +1,40 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Slices import Slice, Slices
+from PLC.Nodes import Node, Nodes
+from PLC.Methods.GetSlices import GetSlices
+from PLC.Methods.GetNodes import GetNodes
+
+class SliceNodesList(GetSlices, GetNodes):
+    """
+    Deprecated. Can be implemented with GetSlices and GetNodes.
+
+    """
+  
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name']
+        ]
+
+    returns = [Node.fields['hostname']]
+    
+
+    def call(self, auth, slice_name):
+       slices = GetSlices.call(self, auth, [slice_name])
+       if not slices:
+           return []
+
+       slice = slices[0]
+       nodes = GetNodes.call(self, auth, slice['node_ids'])
+       if not nodes:
+           return []
+       
+       node_hostnames = [node['hostname'] for node in nodes]           
+       
+        return node_hostnames
diff --git a/PLC/Methods/SliceRenew.py b/PLC/Methods/SliceRenew.py
new file mode 100644 (file)
index 0000000..4ac6f89
--- /dev/null
@@ -0,0 +1,34 @@
+import time
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Methods.UpdateSlice import UpdateSlice 
+
+class SliceRenew(UpdateSlice):
+    """
+    Deprecated. See UpdateSlice.    
+
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name'],
+       Slice.fields['expires']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_name, slice_expires):
+
+       slice_fields = {}
+       slice_fields['expires'] = slice_expires
+       
+       return UpdateSlice.call(self, auth, slice_name, slice_fields)
diff --git a/PLC/Methods/SliceTicketGet.py b/PLC/Methods/SliceTicketGet.py
new file mode 100644 (file)
index 0000000..5b2b786
--- /dev/null
@@ -0,0 +1,13 @@
+from PLC.Methods.SliceGetTicket import SliceGetTicket
+
+class SliceTicketGet(SliceGetTicket):
+    """
+    Deprecated. See GetSliceTicket.
+
+    Warning: This function exists solely for backward compatibility
+    with the old public PlanetLab 3.0 Node Manager, which will be
+    removed from service by 2007. This call is not intended to be used
+    by any other PLC except the public PlanetLab.
+    """
+
+    status = "deprecated"
diff --git a/PLC/Methods/SliceUpdate.py b/PLC/Methods/SliceUpdate.py
new file mode 100644 (file)
index 0000000..9e82d3a
--- /dev/null
@@ -0,0 +1,37 @@
+import time
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Methods.UpdateSlice import UpdateSlice
+
+class SliceUpdate(UpdateSlice):
+    """
+    Deprecated. See UpdateSlice.
+   
+    """
+
+    status = 'deprecated'
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name'],
+        Slice.fields['url'],
+        Slice.fields['description'],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_name, url, description):
+
+       slice_fields = {}
+       slice_fields['url'] = url
+       slice_fields['description'] = description
+       
+       return UpdateSlice.call(self, auth, slice_name, slice_fields)
+
+        return 1
diff --git a/PLC/Methods/SliceUserAdd.py b/PLC/Methods/SliceUserAdd.py
new file mode 100644 (file)
index 0000000..560a66a
--- /dev/null
@@ -0,0 +1,32 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Persons import Person, Persons
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Methods.AddPersonToSlice import AddPersonToSlice
+
+class SliceUserAdd(AddPersonToSlice):
+    """
+    Deprecated. See AddPersonToSlice.
+
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name'],
+        [Person.fields['email']],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_name, user_list):
+
+       for user in user_list:
+           AddPersonToSlice.call(self, auth, user, slice_name)
+
+        return 1
diff --git a/PLC/Methods/SliceUserDel.py b/PLC/Methods/SliceUserDel.py
new file mode 100644 (file)
index 0000000..0b41b15
--- /dev/null
@@ -0,0 +1,35 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Persons import Person, Persons
+from PLC.Slices import Slice, Slices
+from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
+
+class SliceUserDel(Method):
+    """
+    Deprecated. Can be implemented with DeletePersonFromSlice.
+
+    Removes the specified users from the specified slice. If the person is
+    already a member of the slice, no errors are returned. 
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name'],
+        [Person.fields['email']],
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_name, user_list):
+       for user in user_list:
+           DeletePersonFromSlice.call(self, auth, user, slice_name)
+
+        return 1
diff --git a/PLC/Methods/SliceUsersList.py b/PLC/Methods/SliceUsersList.py
new file mode 100644 (file)
index 0000000..e3eb1e4
--- /dev/null
@@ -0,0 +1,45 @@
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Slices import Slice, Slices
+from PLC.Persons import Person, Persons
+from PLC.Methods.GetSlices import GetSlices
+from PLC.Methods.GetPersons import GetPersons
+
+class SliceUsersList(GetSlices, GetPersons):
+    """
+    Deprecated. Can be implemented with GetSlices and GetPersons.
+
+    List users that are members of the named slice.
+
+    Users may only query slices of which they are members. PIs may
+    query any of the slices at their sites. Admins may query any
+    slice. If a slice that cannot be queried is specified details 
+    about that slice will not be returned.
+    """
+
+    status = "deprecated"
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        Slice.fields['name']
+       ]
+
+    returns = [Person.fields['email']]
+    
+
+    def call(self, auth, slice_name):
+
+       slice_filter = [slice_name]
+        slices = GetSlices.call(self, auth, slice_filter)
+       if not slices:
+            return []
+       slice = slices[0]
+     
+       persons = GetPersons.call(self, auth, slice['person_ids'])
+       person_emails = [person['email'] for person in persons]
+
+        return person_emails
diff --git a/PLC/Methods/UpdateAddress.py b/PLC/Methods/UpdateAddress.py
new file mode 100644 (file)
index 0000000..ed2fd43
--- /dev/null
@@ -0,0 +1,54 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Addresses import Address, Addresses
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['line1', 'line2', 'line3',
+              'city', 'state', 'postalcode', 'country']
+
+class UpdateAddress(Method):
+    """
+    Updates the parameters of an existing address with the values in
+    address_fields.
+
+    PIs may only update addresses of their own sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    address_fields = dict(filter(can_update, Address.fields.items()))
+
+    accepts = [
+        Auth(),
+        Address.fields['address_id'],
+        address_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, address_id, address_fields):
+        address_fields = dict(filter(can_update, address_fields.items()))
+
+        # Get associated address details
+        addresses = Addresses(self.api, [address_id])
+        if not addresses:
+            raise PLCInvalidArgument, "No such address"
+        address = addresses[0]
+
+        if 'admin' not in self.caller['roles']:
+            if address['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Address must be associated with one of your sites"
+
+        address.update(address_fields)
+        address.sync()
+       
+       # Logging variables
+       self.event_objects = {'Address': [address['address_id']]}
+       self.message = 'Address %d updated: %s' % \
+               (address['address_id'], ", ".join(address_fields.keys()))
+        
+       return 1
diff --git a/PLC/Methods/UpdateAddressType.py b/PLC/Methods/UpdateAddressType.py
new file mode 100644 (file)
index 0000000..922c940
--- /dev/null
@@ -0,0 +1,42 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.AddressTypes import AddressType, AddressTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in ['name', 'description']
+
+class UpdateAddressType(Method):
+    """
+    Updates the parameters of an existing address type with the values
+    in address_type_fields.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    address_type_fields = dict(filter(can_update, AddressType.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(AddressType.fields['address_type_id'],
+              AddressType.fields['name']),
+        address_type_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, address_type_id_or_name, address_type_fields):
+        address_type_fields = dict(filter(can_update, address_type_fields.items()))
+
+        address_types = AddressTypes(self.api, [address_type_id_or_name])
+        if not address_types:
+            raise PLCInvalidArgument, "No such address type"
+        address_type = address_types[0]
+
+        address_type.update(address_type_fields)
+        address_type.sync()
+       self.event_objects = {'AddressType': [address_type['address_type_id']]}
+
+        return 1
diff --git a/PLC/Methods/UpdateConfFile.py b/PLC/Methods/UpdateConfFile.py
new file mode 100644 (file)
index 0000000..6fd0e2a
--- /dev/null
@@ -0,0 +1,42 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.ConfFiles import ConfFile, ConfFiles
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in \
+             ['conf_file_id', 'node_ids', 'nodegroup_ids']
+
+class UpdateConfFile(Method):
+    """
+    Updates a node configuration file. Only the fields specified in
+    conf_file_fields are updated, all other fields are left untouched.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    conf_file_fields = dict(filter(can_update, ConfFile.fields.items()))
+
+    accepts = [
+        Auth(),
+        ConfFile.fields['conf_file_id'],
+        conf_file_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, conf_file_id, conf_file_fields):
+        conf_file_fields = dict(filter(can_update, conf_file_fields.items()))
+
+        conf_files = ConfFiles(self.api, [conf_file_id])
+        if not conf_files:
+            raise PLCInvalidArgument, "No such configuration file"
+
+        conf_file = conf_files[0]
+        conf_file.update(conf_file_fields)
+        conf_file.sync()
+       self.event_objects = {'ConfFile': [conf_file['conf_file_id']]}  
+
+        return 1
diff --git a/PLC/Methods/UpdateInitScript.py b/PLC/Methods/UpdateInitScript.py
new file mode 100644 (file)
index 0000000..bb0f1f0
--- /dev/null
@@ -0,0 +1,42 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.InitScripts import InitScript, InitScripts
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in \
+             ['initscript_id']
+
+class UpdateInitScript(Method):
+    """
+    Updates an initscript. Only the fields specified in
+    initscript_fields are updated, all other fields are left untouched.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    initscript_fields = dict(filter(can_update, InitScript.fields.items()))
+
+    accepts = [
+        Auth(),
+        InitScript.fields['initscript_id'],
+        initscript_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, initscript_id, initscript_fields):
+        initscript_fields = dict(filter(can_update, initscript_fields.items()))
+
+        initscripts = InitScripts(self.api, [initscript_id])
+        if not initscripts:
+            raise PLCInvalidArgument, "No such initscript"
+
+        initscript = initscripts[0]
+        initscript.update(initscript_fields)
+        initscript.sync()
+       self.event_objects = {'InitScript': [initscript['initscript_id']]}      
+
+        return 1
diff --git a/PLC/Methods/UpdateKey.py b/PLC/Methods/UpdateKey.py
new file mode 100644 (file)
index 0000000..0fb560b
--- /dev/null
@@ -0,0 +1,55 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Keys import Key, Keys
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['key_type', 'key']
+
+class UpdateKey(Method):
+    """
+    Updates the parameters of an existing key with the values in
+    key_fields.
+
+    Non-admins may only update their own keys.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    key_fields = dict(filter(can_update, Key.fields.items()))
+
+    accepts = [
+        Auth(),
+        Key.fields['key_id'],
+        key_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, key_id, key_fields):
+        key_fields = dict(filter(can_update, key_fields.items()))
+
+        # Get key information
+        keys = Keys(self.api, [key_id])
+        if not keys:
+            raise PLCInvalidArgument, "No such key"
+        key = keys[0]
+
+        if key['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local key"
+
+        if 'admin' not in self.caller['roles']:
+            if key['key_id'] not in self.caller['key_ids']:
+                raise PLCPermissionDenied, "Key must be associated with one of your accounts"
+
+        key.update(key_fields)
+        key.sync()
+       
+       # Logging variables
+       self.event_objects = {'Key': [key['key_id']]}
+       self.message = 'key %d updated: %s' % \
+               (key['key_id'], ", ".join(key_fields.keys()))
+        return 1
diff --git a/PLC/Methods/UpdateMessage.py b/PLC/Methods/UpdateMessage.py
new file mode 100644 (file)
index 0000000..e44dca4
--- /dev/null
@@ -0,0 +1,43 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Messages import Message, Messages
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['template', 'enabled']
+
+class UpdateMessage(Method):
+    """
+    Updates the parameters of an existing message template with the
+    values in message_fields.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    message_fields = dict(filter(can_update, Message.fields.items()))
+
+    accepts = [
+        Auth(),
+        Message.fields['message_id'],
+        message_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, message_id, message_fields):
+        message_fields = dict(filter(can_update, message_fields.items()))
+
+        # Get message information
+        messages = Messages(self.api, [message_id])
+        if not messages:
+            raise PLCInvalidArgument, "No such message"
+        message = messages[0]
+
+        message.update(message_fields)
+        message.sync()
+       self.event_objects = {'Message': [message['message_id']]}
+
+        return 1
diff --git a/PLC/Methods/UpdateNode.py b/PLC/Methods/UpdateNode.py
new file mode 100644 (file)
index 0000000..d5cdb0d
--- /dev/null
@@ -0,0 +1,81 @@
+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 Auth
+
+related_fields = Node.related_fields.keys()
+can_update = lambda (field, value): field in \
+             ['hostname', 'boot_state', 'model', 'version',
+              'key', 'session', 'boot_nonce'] + \
+            related_fields
+
+class UpdateNode(Method):
+    """
+    Updates a node. Only the fields specified in node_fields are
+    updated, all other fields are left untouched.
+    
+    PIs and techs can update only the nodes at their sites. Only
+    admins can update the key, session, and boot_nonce fields.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    node_fields = dict(filter(can_update, Node.fields.items() + Node.related_fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Node.fields['node_id'],
+              Node.fields['hostname']),
+        node_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, node_id_or_hostname, node_fields):
+        node_fields = dict(filter(can_update, node_fields.items()))
+
+       # Remove admin only fields
+       if 'admin' not in self.caller['roles']:
+            for key in 'key', 'session', 'boot_nonce':
+                if node_fields.has_key(key):
+                    del node_fields[key]
+
+        # Get account information
+        nodes = Nodes(self.api, [node_id_or_hostname])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node"
+        node = nodes[0]
+
+        if node['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local node"
+
+        # 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 at which the node is located.
+        if 'admin' not in self.caller['roles']:
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to delete nodes from specified site"
+
+        # Make requested associations
+        for field in related_fields:
+            if field in node_fields:
+                node.associate(auth, field, node_fields[field])
+                node_fields.pop(field)
+
+       node.update(node_fields)
+       node.update_last_updated(False)
+        node.sync()
+       
+       # Logging variables
+       self.event_objects = {'Node': [node['node_id']]}
+       self.message = 'Node %d updated: %s.' % \
+               (node['node_id'], ", ".join(node_fields.keys()))
+       if 'boot_state' in node_fields.keys():
+               self.message += ' boot_state updated to %s' %  node_fields['boot_state']
+
+        return 1
diff --git a/PLC/Methods/UpdateNodeGroup.py b/PLC/Methods/UpdateNodeGroup.py
new file mode 100644 (file)
index 0000000..c84c7f1
--- /dev/null
@@ -0,0 +1,54 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeGroups import NodeGroup, NodeGroups
+from PLC.Auth import Auth
+
+related_fields = NodeGroup.related_fields.keys()
+can_update = lambda (field, value): field in \
+             ['name', 'description'] + \
+            related_fields
+
+class UpdateNodeGroup(Method):
+    """
+    Updates a custom node group.
+     
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    nodegroup_fields = dict(filter(can_update, NodeGroup.fields.items() + NodeGroup.related_fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(NodeGroup.fields['nodegroup_id'],
+             NodeGroup.fields['name']),
+        nodegroup_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, nodegroup_id_or_name, nodegroup_fields):
+        nodegroup_fields = dict(filter(can_update, nodegroup_fields.items()))
+
+       # Get nodegroup information
+       nodegroups = NodeGroups(self.api, [nodegroup_id_or_name])
+       if not nodegroups:
+            raise PLCInvalidArgument, "No such nodegroup"
+       nodegroup = nodegroups[0]
+
+       # Make requested associations
+        for field in related_fields:
+            if field in nodegroup_fields:
+                nodegroup.associate(auth, field, nodegroup_fields[field])
+                nodegroup_fields.pop(field)
+       
+       nodegroup.update(nodegroup_fields)
+        nodegroup.sync()
+       
+       # Logging variables
+       self.event_objects = {'NodeGroup': [nodegroup['nodegroup_id']]}
+       self.message = 'Node group %d updated: %s' % \
+               (nodegroup['nodegroup_id'], ", ".join(nodegroup_fields.keys()))  
+        return 1
diff --git a/PLC/Methods/UpdateNodeNetwork.py b/PLC/Methods/UpdateNodeNetwork.py
new file mode 100644 (file)
index 0000000..dc1e65a
--- /dev/null
@@ -0,0 +1,69 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Nodes import Node, Nodes
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in \
+             ['nodenetwork_id','node_id']
+
+class UpdateNodeNetwork(Method):
+    """
+    Updates an existing node network. Any values specified in
+    nodenetwork_fields are used, otherwise defaults are
+    used. Acceptable values for method are dhcp and static. If type is
+    static, then ip, gateway, network, broadcast, netmask, and dns1
+    must all be specified in nodenetwork_fields. If type is dhcp,
+    these parameters, even if specified, are ignored.
+    
+    PIs and techs may only update networks associated with their own
+    nodes. Admins may update any node network.
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    nodenetwork_fields = dict(filter(can_update, NodeNetwork.fields.items()))
+
+    accepts = [
+        Auth(),
+       NodeNetwork.fields['nodenetwork_id'],
+       nodenetwork_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, nodenetwork_id, nodenetwork_fields):
+        nodenetwork_fields = dict(filter(can_update, nodenetwork_fields.items()))
+
+       # Get node network information
+       nodenetworks = NodeNetworks(self.api, [nodenetwork_id])
+       if not nodenetworks:
+            raise PLCInvalidArgument, "No such node network"
+
+       nodenetwork = nodenetworks[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 where the node exists.
+        if 'admin' not in self.caller['roles']:
+            nodes = Nodes(self.api, [nodenetwork['node_id']])
+            if not nodes:
+                raise PLCPermissionDenied, "Node network is not associated with a node"
+            node = nodes[0]
+            if node['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to update node network"
+
+       # Update node network
+       nodenetwork.update(nodenetwork_fields)
+        nodenetwork.sync()
+       
+       self.event_objects = {'NodeNetwork': [nodenetwork['nodenetwork_id']]}
+       self.message = "Node network %d updated: %s " % \
+           (nodenetwork['nodenetwork_id'], ", ".join(nodenetwork_fields.keys()))
+
+        return 1
diff --git a/PLC/Methods/UpdateNodeNetworkSetting.py b/PLC/Methods/UpdateNodeNetworkSetting.py
new file mode 100644 (file)
index 0000000..e8f7497
--- /dev/null
@@ -0,0 +1,72 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.NodeNetworkSettings import NodeNetworkSetting, NodeNetworkSettings
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+
+from PLC.Nodes import Nodes
+from PLC.Sites import Sites
+
+class UpdateNodeNetworkSetting(Method):
+    """
+    Updates the value of an existing nodenetwork setting
+
+    Access rights depend on the nodenetwork setting type.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    accepts = [
+        Auth(),
+        NodeNetworkSetting.fields['nodenetwork_setting_id'],
+        NodeNetworkSetting.fields['value']
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    object_type = 'NodeNetwork'
+
+    def call(self, auth, nodenetwork_setting_id, value):
+        nodenetwork_settings = NodeNetworkSettings(self.api, [nodenetwork_setting_id])
+        if not nodenetwork_settings:
+            raise PLCInvalidArgument, "No such nodenetwork setting %r"%nodenetwork_setting_id
+        nodenetwork_setting = nodenetwork_settings[0]
+
+        ### reproducing a check from UpdateSliceAttribute, looks dumb though
+        nodenetworks = NodeNetworks(self.api, [nodenetwork_setting['nodenetwork_id']])
+        if not nodenetworks:
+            raise PLCInvalidArgument, "No such nodenetwork %r"%nodenetwork_setting['nodenetwork_id']
+        nodenetwork = nodenetworks[0]
+
+        assert nodenetwork_setting['nodenetwork_setting_id'] in nodenetwork['nodenetwork_setting_ids']
+
+       # check permission : it not admin, is the user affiliated with the right site
+       if 'admin' not in self.caller['roles']:
+           # locate node
+           node = Nodes (self.api,[nodenetwork['node_id']])[0]
+           # locate site
+           site = Sites (self.api, [node['site_id']])[0]
+           # check caller is affiliated with this site
+           if self.caller['person_id'] not in site['person_ids']:
+               raise PLCPermissionDenied, "Not a member of the hosting site %s"%site['abbreviated_site']
+           
+           required_min_role = nodenetwork_setting_type ['min_role_id']
+           if required_min_role is not None and \
+                   min(self.caller['role_ids']) > required_min_role:
+               raise PLCPermissionDenied, "Not allowed to modify the specified nodenetwork setting, requires role %d",required_min_role
+
+        nodenetwork_setting['value'] = value
+        nodenetwork_setting.sync()
+
+       self.object_ids = [nodenetwork_setting['nodenetwork_setting_id']]
+        return 1
diff --git a/PLC/Methods/UpdateNodeNetworkSettingType.py b/PLC/Methods/UpdateNodeNetworkSettingType.py
new file mode 100644 (file)
index 0000000..b18079b
--- /dev/null
@@ -0,0 +1,48 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.NodeNetworkSettingTypes import NodeNetworkSettingType, NodeNetworkSettingTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['name', 'description', 'category', 'min_role_id']
+
+class UpdateNodeNetworkSettingType(Method):
+    """
+    Updates the parameters of an existing setting type
+    with the values in nodenetwork_setting_type_fields.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    nodenetwork_setting_type_fields = dict(filter(can_update, NodeNetworkSettingType.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(NodeNetworkSettingType.fields['nodenetwork_setting_type_id'],
+              NodeNetworkSettingType.fields['name']),
+        nodenetwork_setting_type_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, nodenetwork_setting_type_id_or_name, nodenetwork_setting_type_fields):
+        nodenetwork_setting_type_fields = dict(filter(can_update, nodenetwork_setting_type_fields.items()))
+
+        nodenetwork_setting_types = NodeNetworkSettingTypes(self.api, [nodenetwork_setting_type_id_or_name])
+        if not nodenetwork_setting_types:
+            raise PLCInvalidArgument, "No such setting type"
+        nodenetwork_setting_type = nodenetwork_setting_types[0]
+
+        nodenetwork_setting_type.update(nodenetwork_setting_type_fields)
+        nodenetwork_setting_type.sync()
+       self.object_ids = [nodenetwork_setting_type['nodenetwork_setting_type_id']]
+
+        return 1
diff --git a/PLC/Methods/UpdatePCU.py b/PLC/Methods/UpdatePCU.py
new file mode 100644 (file)
index 0000000..8916684
--- /dev/null
@@ -0,0 +1,52 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUs import PCU, PCUs
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field not in \
+             ['pcu_id', 'site_id']
+
+class UpdatePCU(Method):
+    """
+    Updates the parameters of an existing PCU with the values in
+    pcu_fields.
+
+    Non-admins may only update PCUs at their sites.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech']
+
+    update_fields = dict(filter(can_update, PCU.fields.items()))
+
+    accepts = [
+        Auth(),
+        PCU.fields['pcu_id'],
+        update_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, pcu_id, pcu_fields):
+        pcu_fields = dict(filter(can_update, pcu_fields.items()))
+
+        # Get associated PCU details
+        pcus = PCUs(self.api, [pcu_id])
+        if not pcus:
+            raise PLCInvalidArgument, "No such PCU"
+        pcu = pcus[0]
+
+        if 'admin' not in self.caller['roles']:
+            if pcu['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Not allowed to update that PCU"
+
+        pcu.update(pcu_fields)
+        pcu.sync()
+       
+       # Logging variables
+       self.event_objects = {'PCU': [pcu['pcu_id']]}
+       self.message = 'PCU %d updated: %s' % \
+               (pcu['pcu_id'], ", ".join(pcu_fields.keys()))
+        return 1
diff --git a/PLC/Methods/UpdatePCUProtocolType.py b/PLC/Methods/UpdatePCUProtocolType.py
new file mode 100644 (file)
index 0000000..b1a30bc
--- /dev/null
@@ -0,0 +1,41 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUProtocolTypes import PCUProtocolType, PCUProtocolTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['pcu_type_id', 'port', 'protocol', 'supported']
+
+class UpdatePCUProtocolType(Method):
+    """
+    Updates a pcu protocol type. Only the fields specified in
+    port_typee_fields are updated, all other fields are left untouched.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    protocol_type_fields = dict(filter(can_update, PCUProtocolType.fields.items()))
+
+    accepts = [
+        Auth(),
+        PCUProtocolType.fields['pcu_protocol_type_id'],
+        protocol_type_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, protocol_type_id, protocol_type_fields):
+        protocol_type_fields = dict(filter(can_update, protocol_type_fields.items()))
+
+        protocol_types = PCUProtocolTypes(self.api, [protocol_type_id])
+        if not protocol_types:
+            raise PLCInvalidArgument, "No such pcu protocol type"
+
+        protocol_type = protocol_types[0]
+        protocol_type.update(protocol_type_fields)
+        protocol_type.sync()
+       self.event_objects = {'PCUProtocolType': [protocol_type['pcu_protocol_type_id']]}       
+        return 1
diff --git a/PLC/Methods/UpdatePCUType.py b/PLC/Methods/UpdatePCUType.py
new file mode 100644 (file)
index 0000000..fc4e886
--- /dev/null
@@ -0,0 +1,42 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.PCUTypes import PCUType, PCUTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['model', 'name']
+
+class UpdatePCUType(Method):
+    """
+    Updates a PCU type. Only the fields specified in
+    pcu_typee_fields are updated, all other fields are left untouched.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    pcu_type_fields = dict(filter(can_update, PCUType.fields.items()))
+
+    accepts = [
+        Auth(),
+        PCUType.fields['pcu_type_id'],
+        pcu_type_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, pcu_type_id, pcu_type_fields):
+        pcu_type_fields = dict(filter(can_update, pcu_type_fields.items()))
+
+        pcu_types = PCUTypes(self.api, [pcu_type_id])
+        if not pcu_types:
+            raise PLCInvalidArgument, "No such pcu type"
+
+        pcu_type = pcu_types[0]
+        pcu_type.update(pcu_type_fields)
+        pcu_type.sync()
+       self.event_objects = {'PCUType': [pcu_type['pcu_type_id']]}     
+
+        return 1
diff --git a/PLC/Methods/UpdatePeer.py b/PLC/Methods/UpdatePeer.py
new file mode 100644 (file)
index 0000000..8586a48
--- /dev/null
@@ -0,0 +1,50 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Peers import Peer, Peers
+
+can_update = lambda (field, value): field in \
+             ['peername', 'peer_url', 'key', 'cacert']
+
+class UpdatePeer(Method):
+    """
+    Updates a peer. Only the fields specified in peer_fields are
+    updated, all other fields are left untouched.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    peer_fields = dict(filter(can_update, Peer.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Peer.fields['peer_id'],
+              Peer.fields['peername']),
+        peer_fields
+        ]
+
+    returns = Parameter(int, "1 if successful")
+
+    def call(self, auth, peer_id_or_name, peer_fields):
+        peer_fields = dict(filter(can_update, peer_fields.items()))
+
+        # Get account information
+        peers = Peers(self.api, [peer_id_or_name])
+        if not peers:
+            raise PLCInvalidArgument, "No such peer"
+        peer = peers[0]
+
+        if isinstance(self.caller, Peer):
+            if self.caller['peer_id'] != peer['peer_id']:
+                raise PLCPermissionDenied, "Not allowed to update specified peer"
+
+        peer.update(peer_fields)
+        peer.sync()
+
+        # Log affected objects
+       self.event_objects = {'Peer': [peer['peer_id']]}
+
+       return 1
diff --git a/PLC/Methods/UpdatePerson.py b/PLC/Methods/UpdatePerson.py
new file mode 100644 (file)
index 0000000..2043604
--- /dev/null
@@ -0,0 +1,90 @@
+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 Auth
+from PLC.sendmail import sendmail
+
+related_fields = Person.related_fields.keys()
+can_update = lambda (field, value): field in \
+             ['first_name', 'last_name', 'title', 'email',
+              'password', 'phone', 'url', 'bio', 'accepted_aup',
+              'enabled'] + related_fields
+
+class UpdatePerson(Method):
+    """
+    Updates a person. Only the fields specified in person_fields are
+    updated, all other fields are left untouched.
+    
+    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']
+
+    person_fields = dict(filter(can_update, Person.fields.items() + Person.related_fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+        person_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, person_id_or_email, person_fields):
+        person_fields = dict(filter(can_update, person_fields.items()))
+
+        # Get account information
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account"
+        person = persons[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account"
+
+        # 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"
+       
+       # Make requested associations
+        for field in related_fields:
+            if field in person_fields:
+                person.associate(auth, field, person_fields[field])
+                person_fields.pop(field)
+
+        person.update(person_fields)
+       person.update_last_updated(False)
+        person.sync()
+
+       if 'enabled' in person_fields:
+           To = [("%s %s" % (person['first_name'], person['last_name']), person['email'])]
+           Cc = []     
+           if person['enabled']:
+               Subject = "%s account enabled" % (self.api.config.PLC_NAME)
+               Body = "Your %s account has been enabled. Please visit %s to access your account." % (self.api.config.PLC_NAME, self.api.config.PLC_WWW_HOST) 
+           else:
+               Subject = "%s account disabled" % (self.api.config.PLC_NAME)
+               Body = "Your %s account has been disabled. Please contact your PI or PlanetLab support for more information" % (self.api.config.PLC_NAME)
+           sendmail(self.api, To = To, Cc = Cc, Subject = Subject, Body = Body)                
+
+                               
+       # Logging variables
+       self.event_objects = {'Person': [person['person_id']]}
+
+        # Redact password
+        if 'password' in person_fields:
+            person_fields['password'] = "Removed by API"
+        self.message = 'Person %d updated: %s.' % \
+                       (person['person_id'], person_fields.keys())
+       if 'enabled' in person_fields:
+            self.message += ' Person enabled'  
+
+        return 1
diff --git a/PLC/Methods/UpdateSite.py b/PLC/Methods/UpdateSite.py
new file mode 100644 (file)
index 0000000..6a33c5e
--- /dev/null
@@ -0,0 +1,79 @@
+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 Auth
+
+related_fields = Site.related_fields.keys()
+can_update = lambda (field, value): field in \
+             ['name', 'abbreviated_name', 'login_base',
+              'is_public', 'latitude', 'longitude', 'url',
+              'max_slices', 'max_slivers', 'enabled', 'ext_consortium_id'] + \
+             related_fields    
+
+class UpdateSite(Method):
+    """
+    Updates a site. Only the fields specified in update_fields are
+    updated, all other fields are left untouched.
+
+    PIs can only update sites they are a member of. Only admins can 
+    update max_slices, max_slivers, and login_base.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi']
+
+    site_fields = dict(filter(can_update, Site.fields.items() + Site.related_fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Site.fields['site_id'],
+              Site.fields['login_base']),
+        site_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, site_id_or_login_base, site_fields):
+        site_fields = dict(filter(can_update, site_fields.items()))
+
+        # Get site information
+        sites = Sites(self.api, [site_id_or_login_base])
+        if not sites:
+            raise PLCInvalidArgument, "No such site"
+        site = sites[0]
+
+        if site['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local site"
+
+        # 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']:
+                raise PLCPermissionDenied, "Not allowed to modify specified site"
+
+            # Remove admin only fields
+            for key in 'max_slices', 'max_slivers', 'login_base':
+                if key in site_fields:
+                    del site_fields[key]
+
+       # Make requested associations
+       for field in related_fields:
+           if field in site_fields:
+               site.associate(auth, field, site_fields[field])
+               site_fields.pop(field)  
+       
+        site.update(site_fields)
+       site.update_last_updated(False)
+       site.sync()
+       
+       # Logging variables
+       self.event_objects = {'Site': [site['site_id']]}
+       self.message = 'Site %d updated: %s' % \
+               (site['site_id'], ", ".join(site_fields.keys()))        
+       
+       return 1
diff --git a/PLC/Methods/UpdateSlice.py b/PLC/Methods/UpdateSlice.py
new file mode 100644 (file)
index 0000000..901ecbc
--- /dev/null
@@ -0,0 +1,107 @@
+import time
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Slices import Slice, Slices
+from PLC.Auth import Auth
+from PLC.Sites import Site, Sites
+
+related_fields = Slice.related_fields.keys() 
+can_update = lambda (field, value): field in \
+             ['instantiation', 'url', 'description', 'max_nodes', 'expires'] + \
+            related_fields
+
+
+class UpdateSlice(Method):
+    """
+    Updates the parameters of an existing slice with the values in
+    slice_fields.
+
+    Users may only update slices of which they are members. PIs may
+    update any of the slices at their sites, or any slices of which
+    they are members. Admins may update any slice.
+
+    Only PIs and admins may update max_nodes. Slices cannot be renewed
+    (by updating the expires parameter) more than 8 weeks into the
+    future.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    slice_fields = dict(filter(can_update, Slice.fields.items() + Slice.related_fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+        slice_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_id_or_name, slice_fields):
+        slice_fields = dict(filter(can_update, slice_fields.items()))
+        
+       slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        if slice['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local slice"
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+
+        # Renewing
+        if 'expires' in slice_fields and slice_fields['expires'] > slice['expires']:
+            sites = Sites(self.api, [slice['site_id']])
+            assert sites
+            site = sites[0]
+
+            if site['max_slices'] < 0:
+                raise PLCInvalidArgument, "Slice creation and renewal have been disabled for the site"
+
+            # Maximum expiration date is 8 weeks from now
+            # XXX Make this configurable
+            max_expires = time.time() + (8 * 7 * 24 * 60 * 60)
+
+            if 'admin' not in self.caller['roles'] and slice_fields['expires'] > max_expires:
+                raise PLCInvalidArgument, "Cannot renew a slice beyond 8 weeks from now"
+
+           # XXX Make this a configurable policy
+            if slice['description'] is None or not slice['description'].strip():
+               if 'description' not in slice_fields or slice_fields['description'] is None or \
+                  not slice_fields['description'].strip():
+                    raise PLCInvalidArgument, "Cannot renew a slice with an empty description or URL"  
+               
+           if slice['url'] is None or not slice['url'].strip():
+               if 'url' not in slice_fields or slice_fields['url'] is None or \
+                  not slice_fields['url'].strip():
+                    raise PLCInvalidArgument, "Cannot renew a slice with an empty description or URL"
+           
+        if 'max_nodes' in slice_fields and slice_fields['max_nodes'] != slice['max_nodes']:
+            if 'admin' not in self.caller['roles'] and \
+               'pi' not in self.caller['roles']:
+                raise PLCInvalidArgument, "Only admins and PIs may update max_nodes"
+
+       # Make requested associations
+       for field in related_fields:
+           if field in slice_fields:
+               slice.associate(auth, field, slice_fields[field])
+               slice_fields.pop(field)
+
+       slice.update(slice_fields)
+        slice.sync()
+
+       self.event_objects = {'Slice': [slice['slice_id']]}
+
+        return 1
diff --git a/PLC/Methods/UpdateSliceAttribute.py b/PLC/Methods/UpdateSliceAttribute.py
new file mode 100644 (file)
index 0000000..9451291
--- /dev/null
@@ -0,0 +1,65 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+from PLC.Slices import Slice, Slices
+from PLC.InitScripts import InitScript, InitScripts
+from PLC.Auth import Auth
+
+class UpdateSliceAttribute(Method):
+    """
+    Updates the value of an existing slice or sliver attribute.
+
+    Users may only update attributes of slices or slivers of which
+    they are members. PIs may only update attributes of slices or
+    slivers at their sites, or of which they are members. Admins may
+    update attributes of any slice or sliver.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+        SliceAttribute.fields['slice_attribute_id'],
+       Mixed(SliceAttribute.fields['value'],
+              InitScript.fields['name'])
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, slice_attribute_id, value):
+        slice_attributes = SliceAttributes(self.api, [slice_attribute_id])
+        if not slice_attributes:
+            raise PLCInvalidArgument, "No such slice attribute"
+        slice_attribute = slice_attributes[0]
+
+        slices = Slices(self.api, [slice_attribute['slice_id']])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice"
+        slice = slices[0]
+
+        assert slice_attribute['slice_attribute_id'] in slice['slice_attribute_ids']
+
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+
+            if slice_attribute['min_role_id'] is not None and \
+               min(self.caller['role_ids']) > slice_attribute['min_role_id']:
+                raise PLCPermissionDenied, "Not allowed to update the specified attribute"
+       
+       if slice_attribute['name'] in ['initscript']:
+            initscripts = InitScripts(self.api, {'enabled': True, 'name': value})
+            if not initscripts:
+                raise PLCInvalidArgument, "No such plc initscript"     
+
+        slice_attribute['value'] = unicode(value)
+        slice_attribute.sync()
+       self.event_objects = {'SliceAttribute': [slice_attribute['slice_attribute_id']]}
+        return 1
diff --git a/PLC/Methods/UpdateSliceAttributeType.py b/PLC/Methods/UpdateSliceAttributeType.py
new file mode 100644 (file)
index 0000000..145a51d
--- /dev/null
@@ -0,0 +1,43 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.SliceAttributeTypes import SliceAttributeType, SliceAttributeTypes
+from PLC.Auth import Auth
+
+can_update = lambda (field, value): field in \
+             ['name', 'description', 'min_role_id']
+
+class UpdateSliceAttributeType(Method):
+    """
+    Updates the parameters of an existing attribute with the values in
+    attribute_type_fields.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin']
+
+    attribute_type_fields = dict(filter(can_update, SliceAttributeType.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed(SliceAttributeType.fields['attribute_type_id'],
+              SliceAttributeType.fields['name']),
+        attribute_type_fields
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+    def call(self, auth, attribute_type_id_or_name, attribute_type_fields):
+        attribute_type_fields = dict(filter(can_update, attribute_type_fields.items()))
+
+        attribute_types = SliceAttributeTypes(self.api, [attribute_type_id_or_name])
+        if not attribute_types:
+            raise PLCInvalidArgument, "No such attribute"
+        attribute_type = attribute_types[0]
+
+        attribute_type.update(attribute_type_fields)
+        attribute_type.sync()
+       self.event_objects = {'AttributeType': [attribute_type['attribute_type_id']]}
+
+        return 1
diff --git a/PLC/Methods/VerifyPerson.py b/PLC/Methods/VerifyPerson.py
new file mode 100644 (file)
index 0000000..9dd784b
--- /dev/null
@@ -0,0 +1,156 @@
+import random
+import base64
+import time
+import urllib
+
+from PLC.Debug import log
+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.Messages import Message, Messages
+from PLC.Auth import Auth
+from PLC.sendmail import sendmail
+
+class VerifyPerson(Method):
+    """
+    Verify a new (must be disabled) user's e-mail address and registration.
+
+    If verification_key is not specified, then a new verification_key
+    will be generated and stored with the user's account. The key will
+    be e-mailed to the user in the form of a link to a web page.
+
+    The web page should verify the key by calling this function again
+    and specifying verification_key. If the key matches what has been
+    stored in the user's account, then an e-mail will be sent to the
+    user's PI (and support if the user is requesting a PI role),
+    asking the PI (or support) to enable the account.
+
+    Returns 1 if the verification key if valid.
+    """
+
+    roles = ['admin']
+
+    accepts = [
+        Auth(),
+        Mixed(Person.fields['person_id'],
+              Person.fields['email']),
+       Person.fields['verification_key'],
+        Person.fields['verification_expires']
+        ]
+
+    returns = Parameter(int, '1 if verification_key is valid')
+
+    def call(self, auth, person_id_or_email, verification_key = None, verification_expires = None):
+       # Get account information
+        persons = Persons(self.api, [person_id_or_email])
+        if not persons:
+            raise PLCInvalidArgument, "No such account %r"%person_id_or_email
+        person = persons[0]
+
+        if person['peer_id'] is not None:
+            raise PLCInvalidArgument, "Not a local account %r"%person_id_or_email
+
+        if person['enabled']:
+            raise PLCInvalidArgument, "Account %r must be new (disabled)"%person_id_or_email
+
+        # Get the primary site name
+        person_sites = Sites(self.api, person['site_ids'])
+        if person_sites:
+            site_name = person_sites[0]['name']
+        else:
+            site_name = "No Site"
+
+        # Generate 32 random bytes
+        bytes = random.sample(xrange(0, 256), 32)
+        # Base64 encode their string representation
+        random_key = base64.b64encode("".join(map(chr, bytes)))
+
+        if verification_key is None or \
+       (verification_key is not None and person['verification_expires'] and \
+       person['verification_expires'] < time.time()):
+           # Only allow one verification at a time
+            if person['verification_expires'] is not None and \
+               person['verification_expires'] > time.time():
+                raise PLCPermissionDenied, "Verification request already pending"
+
+            if verification_expires is None:
+                verification_expires = int(time.time() + (24 * 60 * 60))
+
+            person['verification_key'] = random_key
+            person['verification_expires'] = verification_expires
+            person.sync()
+
+            # Send e-mail to user
+            To = ("%s %s" % (person['first_name'], person['last_name']), person['email'])
+            Cc = None
+
+            message_id = 'Verify account'
+
+
+       elif verification_key is not None:
+            if person['verification_key'] is None or \
+               person['verification_expires'] is None:
+                raise PLCPermissionDenied, "Invalid Verification key"
+            elif person['verification_key'] != verification_key:
+               raise PLCPermissionDenied, "Verification key incorrect"
+            else:
+                person['verification_key'] = None
+                person['verification_expires'] = None
+                person.sync()
+
+                # Get the PI(s) of each site that the user is registering with
+                person_ids = set()
+                for site in person_sites:
+                    person_ids.update(site['person_ids'])
+                persons = Persons(self.api, person_ids)
+                pis = filter(lambda person: 'pi' in person['roles'] and person['enabled'], persons)
+
+                # Send e-mail to PI(s) and copy the user
+                To = [("%s %s" % (pi['first_name'], pi['last_name']), pi['email']) for pi in pis]
+                Cc = ("%s %s" % (person['first_name'], person['last_name']), person['email'])
+
+                if 'pi' in person['roles']:
+                    # And support if user is requesting a PI role
+                    To.append(("%s Support" % self.api.config.PLC_NAME,
+                               self.api.config.PLC_MAIL_SUPPORT_ADDRESS))
+                    message_id = 'New PI account'
+                else:
+                    message_id = 'New account'
+
+        messages = Messages(self.api, [message_id])
+        if messages:
+            # Send message to user
+            message = messages[0]
+
+            params = {'PLC_NAME': self.api.config.PLC_NAME,
+                      'PLC_MAIL_SUPPORT_ADDRESS': self.api.config.PLC_MAIL_SUPPORT_ADDRESS,
+                      'PLC_WWW_HOST': self.api.config.PLC_WWW_HOST,
+                      'PLC_WWW_SSL_PORT': self.api.config.PLC_WWW_SSL_PORT,
+                      'person_id': person['person_id'],
+                      # Will be used in a URL, so must quote appropriately
+                      'verification_key': urllib.quote_plus(random_key),
+                      'site_name': site_name,
+                      'first_name': person['first_name'],
+                      'last_name': person['last_name'],
+                      'email': person['email'],
+                      'roles': ", ".join(person['roles'])}
+
+            sendmail(self.api,
+                     To = To,
+                     Cc = Cc,
+                     Subject = message['subject'] % params,
+                     Body = message['template'] % params)
+        else:
+            print >> log, "Warning: No message template '%s'" % message_id
+
+       # Logging variables
+        self.event_objects = {'Person': [person['person_id']]}
+        self.message = message_id
+       
+       if verification_key is not None and person['verification_expires'] and \
+        person['verification_expires'] < time.time():
+           raise PLCPermissionDenied, "Verification key has expired. Another email has been sent."
+
+        return 1
diff --git a/PLC/Methods/__init__.py b/PLC/Methods/__init__.py
new file mode 100644 (file)
index 0000000..45c9228
--- /dev/null
@@ -0,0 +1,231 @@
+methods = """
+AddAddressType
+AddAddressTypeToAddress
+AddBootState
+AddConfFile
+AddConfFileToNodeGroup
+AddConfFileToNode
+AddInitScript
+AddKeyType
+AddMessage
+AddNetworkMethod
+AddNetworkType
+AddNodeGroup
+AddNodeNetwork
+AddNodeNetworkSetting
+AddNodeNetworkSettingType
+AddNode
+AddNodeToNodeGroup
+AddNodeToPCU
+AddPCUProtocolType
+AddPCU
+AddPCUType
+AddPeer
+AddPersonKey
+AddPerson
+AddPersonToSite
+AddPersonToSlice
+AddRole
+AddRoleToPerson
+AddSession
+AddSiteAddress
+AddSite
+AddSliceAttribute
+AddSliceAttributeType
+AddSliceInstantiation
+AddSlice
+AddSliceToNodes
+AddSliceToNodesWhitelist
+AdmAddAddressType
+AdmAddNodeGroup
+AdmAddNodeNetwork
+AdmAddNode
+AdmAddNodeToNodeGroup
+AdmAddPersonKey
+AdmAddPerson
+AdmAddPersonToSite
+AdmAddSitePowerControlUnit
+AdmAddSite
+AdmAssociateNodeToPowerControlUnitPort
+AdmAuthCheck
+AdmDeleteAddressType
+AdmDeleteAllPersonKeys
+AdmDeleteNodeGroup
+AdmDeleteNodeNetwork
+AdmDeleteNode
+AdmDeletePersonKeys
+AdmDeletePerson
+AdmDeleteSitePowerControlUnit
+AdmDeleteSite
+AdmDisassociatePowerControlUnitPort
+AdmGenerateNodeConfFile
+AdmGetAllAddressTypes
+AdmGetAllKeyTypes
+AdmGetAllNodeNetworks
+AdmGetAllRoles
+AdmGetNodeGroupNodes
+AdmGetNodeGroups
+AdmGetNodes
+AdmGetPersonKeys
+AdmGetPersonRoles
+AdmGetPersonSites
+AdmGetPersons
+AdmGetPowerControlUnitNodes
+AdmGetPowerControlUnits
+AdmGetSiteNodes
+AdmGetSitePersons
+AdmGetSitePIs
+AdmGetSitePowerControlUnits
+AdmGetSites
+AdmGetSiteTechContacts
+AdmGrantRoleToPerson
+AdmIsPersonInRole
+AdmQueryConfFile
+AdmQueryNode
+AdmQueryPerson
+AdmQueryPowerControlUnit
+AdmQuerySite
+AdmRebootNode
+AdmRemoveNodeFromNodeGroup
+AdmRemovePersonFromSite
+AdmRevokeRoleFromPerson
+AdmSetPersonEnabled
+AdmSetPersonPrimarySite
+AdmUpdateNodeGroup
+AdmUpdateNodeNetwork
+AdmUpdateNode
+AdmUpdatePerson
+AdmUpdateSitePowerControlUnit
+AdmUpdateSite
+AnonAdmGetNodeGroups
+AuthCheck
+BlacklistKey
+BootCheckAuthentication
+BootGetNodeDetails
+BootNotifyOwners
+BootUpdateNode
+DeleteAddress
+DeleteAddressTypeFromAddress
+DeleteAddressType
+DeleteBootState
+DeleteConfFileFromNodeGroup
+DeleteConfFileFromNode
+DeleteConfFile
+DeleteInitScript
+DeleteKey
+DeleteKeyType
+DeleteMessage
+DeleteNetworkMethod
+DeleteNetworkType
+DeleteNodeFromNodeGroup
+DeleteNodeFromPCU
+DeleteNodeGroup
+DeleteNodeNetwork
+DeleteNodeNetworkSetting
+DeleteNodeNetworkSettingType
+DeleteNode
+DeletePCUProtocolType
+DeletePCU
+DeletePCUType
+DeletePeer
+DeletePersonFromSite
+DeletePersonFromSlice
+DeletePerson
+DeleteRoleFromPerson
+DeleteRole
+DeleteSession
+DeleteSite
+DeleteSliceAttribute
+DeleteSliceAttributeType
+DeleteSliceFromNodes
+DeleteSliceFromNodesWhitelist
+DeleteSliceInstantiation
+DeleteSlice
+GenerateNodeConfFile
+GetAddresses
+GetAddressTypes
+GetBootMedium
+GetBootStates
+GetConfFiles
+GetEventObjects
+GetEvents
+GetInitScripts
+GetKeys
+GetKeyTypes
+GetMessages
+GetNetworkMethods
+GetNetworkTypes
+GetNodeGroups
+GetNodeNetworkSettings
+GetNodeNetworkSettingTypes
+GetNodeNetworks
+GetNodes
+GetPCUProtocolTypes
+GetPCUs
+GetPCUTypes
+GetPeerData
+GetPeerName
+GetPeers
+GetPersons
+GetPlcRelease
+GetRoles
+GetSession
+GetSessions
+GetSites
+GetSliceAttributes
+GetSliceAttributeTypes
+GetSliceInstantiations
+GetSliceKeys
+GetSlicesMD5
+GetSlices
+GetSliceTicket
+GetSlivers
+GetWhitelist
+NotifyPersons
+NotifySupport
+RebootNode
+RefreshPeer
+ResetPassword
+SetPersonPrimarySite
+SliceCreate
+SliceDelete
+SliceExtendedInfo
+SliceGetTicket
+SliceInfo
+SliceListNames
+SliceListUserSlices
+SliceNodesAdd
+SliceNodesDel
+SliceNodesList
+SliceRenew
+SliceTicketGet
+SliceUpdate
+SliceUserAdd
+SliceUserDel
+SliceUsersList
+system.listMethods
+system.methodHelp
+system.methodSignature
+system.multicall
+UpdateAddress
+UpdateAddressType
+UpdateConfFile
+UpdateInitScript
+UpdateKey
+UpdateMessage
+UpdateNodeGroup
+UpdateNodeNetwork
+UpdateNodeNetworkSetting
+UpdateNodeNetworkSettingType
+UpdateNode
+UpdatePCUProtocolType
+UpdatePCU
+UpdatePCUType
+UpdatePeer
+UpdatePerson
+UpdateSite
+UpdateSliceAttribute
+UpdateSliceAttributeType
+UpdateSlice
+VerifyPerson
+""".split()
diff --git a/PLC/Methods/system/.cvsignore b/PLC/Methods/system/.cvsignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
diff --git a/PLC/Methods/system/__init__.py b/PLC/Methods/system/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/PLC/Methods/system/listMethods.py b/PLC/Methods/system/listMethods.py
new file mode 100644 (file)
index 0000000..c8cfa37
--- /dev/null
@@ -0,0 +1,20 @@
+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
diff --git a/PLC/Methods/system/methodHelp.py b/PLC/Methods/system/methodHelp.py
new file mode 100644 (file)
index 0000000..22a0dc1
--- /dev/null
@@ -0,0 +1,20 @@
+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()
diff --git a/PLC/Methods/system/methodSignature.py b/PLC/Methods/system/methodSignature.py
new file mode 100644 (file)
index 0000000..4b049a1
--- /dev/null
@@ -0,0 +1,60 @@
+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
diff --git a/PLC/Methods/system/multicall.py b/PLC/Methods/system/multicall.py
new file mode 100644 (file)
index 0000000..64563ef
--- /dev/null
@@ -0,0 +1,54 @@
+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
diff --git a/PLC/NetworkMethods.py b/PLC/NetworkMethods.py
new file mode 100644 (file)
index 0000000..d6b6a63
--- /dev/null
@@ -0,0 +1,53 @@
+#
+# Functions for interacting with the network_methods table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: NetworkMethods.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+
+class NetworkMethod(Row):
+    """
+    Representation of a row in the network_methods table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'network_methods'
+    primary_key = 'method'
+    join_tables = ['nodenetworks']
+    fields = {
+        'method': Parameter(str, "Network method", max = 20),
+        }
+
+    def validate_method(self, name):
+       # Make sure name is not blank
+        if not len(name):
+            raise PLCInvalidArgument, "Network method must be specified"
+       
+       # Make sure network method does not alredy exist
+       conflicts = NetworkMethods(self.api, [name])
+        if conflicts:
+            raise PLCInvalidArgument, "Network method name already in use"
+
+       return name
+
+class NetworkMethods(Table):
+    """
+    Representation of the network_methods table in the database.
+    """
+
+    def __init__(self, api, methods = None):
+        Table.__init__(self, api, NetworkMethod)
+
+        sql = "SELECT %s FROM network_methods" % \
+              ", ".join(NetworkMethod.fields)
+        
+        if methods:
+            sql += " WHERE method IN (%s)" % ", ".join(map(api.db.quote, methods))
+
+        self.selectall(sql)
diff --git a/PLC/NetworkTypes.py b/PLC/NetworkTypes.py
new file mode 100644 (file)
index 0000000..b42b42e
--- /dev/null
@@ -0,0 +1,53 @@
+#
+# Functions for interacting with the network_types table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: NetworkTypes.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+
+class NetworkType(Row):
+    """
+    Representation of a row in the network_types table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'network_types'
+    primary_key = 'type'
+    join_tables = ['nodenetworks']
+    fields = {
+        'type': Parameter(str, "Network type", max = 20),
+        }
+
+    def validate_type(self, name):
+       # Make sure name is not blank
+        if not len(name):
+            raise PLCInvalidArgument, "Network type must be specified"
+       
+       # Make sure network type does not alredy exist
+       conflicts = NetworkTypes(self.api, [name])
+        if conflicts:
+            raise PLCInvalidArgument, "Network type name already in use"
+
+       return name
+
+class NetworkTypes(Table):
+    """
+    Representation of the network_types table in the database.
+    """
+
+    def __init__(self, api, types = None):
+        Table.__init__(self, api, NetworkType)
+
+        sql = "SELECT %s FROM network_types" % \
+              ", ".join(NetworkType.fields)
+        
+        if types:
+            sql += " WHERE type IN (%s)" % ", ".join(map(api.db.quote, types))
+
+        self.selectall(sql)
diff --git a/PLC/NodeGroups.py b/PLC/NodeGroups.py
new file mode 100644 (file)
index 0000000..65b4a41
--- /dev/null
@@ -0,0 +1,182 @@
+#
+# 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: NodeGroups.py 5666 2007-11-06 21:52:21Z tmack $
+#
+
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.Nodes import Node, Nodes
+
+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 sync().
+    """
+
+    table_name = 'nodegroups'
+    primary_key = 'nodegroup_id'
+    join_tables = ['nodegroup_node', 'conf_file_nodegroup']
+    fields = {
+        'nodegroup_id': Parameter(int, "Node group identifier"),
+        'name': Parameter(str, "Node group name", max = 50),
+        'description': Parameter(str, "Node group description", max = 200, nullok = True),
+        'node_ids': Parameter([int], "List of nodes in this node group"),
+        'conf_file_ids': Parameter([int], "List of configuration files specific to this node group"),
+        }
+    related_fields = {
+       'conf_files': [Parameter(int, "ConfFile identifier")],
+       'nodes': [Mixed(Parameter(int, "Node identifier"),
+                        Parameter(str, "Fully qualified hostname"))]
+       }
+
+    def validate_name(self, name):
+       # Make sure name is not blank
+        if not len(name):
+                raise PLCInvalidArgument, "Invalid node group name"
+       
+       # Make sure node group does not alredy exist
+       conflicts = NodeGroups(self.api, [name])
+       for nodegroup in conflicts:
+            if 'nodegroup_id' not in self or self['nodegroup_id'] != nodegroup['nodegroup_id']:
+               raise PLCInvalidArgument, "Node group name already in use"
+
+       return name
+
+    def add_node(self, node, commit = True):
+        """
+        Add node to existing nodegroup.
+        """
+
+        assert 'nodegroup_id' in self
+        assert isinstance(node, Node)
+        assert 'node_id' in node
+
+        node_id = node['node_id']
+        nodegroup_id = self['nodegroup_id']
+
+        if node_id not in self['node_ids']:
+            assert nodegroup_id not in node['nodegroup_ids']
+
+            self.api.db.do("INSERT INTO nodegroup_node (nodegroup_id, node_id)" \
+                           " VALUES(%(nodegroup_id)d, %(node_id)d)",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['node_ids'].append(node_id)
+            node['nodegroup_ids'].append(nodegroup_id)
+
+    def remove_node(self, node, commit = True):
+        """
+        Remove node from existing nodegroup.
+        """
+
+        assert 'nodegroup_id' in self
+        assert isinstance(node, Node)
+        assert 'node_id' in node
+
+        node_id = node['node_id']
+        nodegroup_id = self['nodegroup_id']
+
+        if node_id in self['node_ids']:
+            assert nodegroup_id in node['nodegroup_ids']
+
+            self.api.db.do("DELETE FROM nodegroup_node" \
+                           " WHERE nodegroup_id = %(nodegroup_id)d" \
+                           " AND node_id = %(node_id)d",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['node_ids'].remove(node_id)
+            node['nodegroup_ids'].remove(nodegroup_id)
+
+    def associate_nodes(self, auth, field, value):
+        """
+        Adds nodes found in value list to this nodegroup (using AddNodeToNodeGroup).
+        Deletes nodes not found in value list from this slice (using DeleteNodeFromNodeGroup).
+        """
+
+        assert 'node_ids' in self
+        assert 'nodegroup_id' in self
+        assert isinstance(value, list)
+
+        (node_ids, hostnames) = self.separate_types(value)[0:2]
+
+        # Translate hostnames into node_ids
+        if hostnames:
+            nodes = Nodes(self.api, hostnames, ['node_id']).dict('node_id')
+            node_ids += nodes.keys()
+
+        # Add new ids, remove stale ids
+        if self['node_ids'] != node_ids:
+            from PLC.Methods.AddNodeToNodeGroup import AddNodeToNodeGroup
+            from PLC.Methods.DeleteNodeFromNodeGroup import DeleteNodeFromNodeGroup
+            new_nodes = set(node_ids).difference(self['node_ids'])
+            stale_nodes = set(self['node_ids']).difference(node_ids)
+
+            for new_node in new_nodes:
+                AddNodeToNodeGroup.__call__(AddNodeToNodeGroup(self.api), auth, new_node, self['nodegroup_id'])
+            for stale_node in stale_nodes:
+                DeleteNodeFromNodeGroup.__call__(DeleteNodeFromNodeGroup(self.api), auth, stale_node, self['nodegroup_id'])
+
+    def associate_conf_files(self, auth, field, value):
+        """
+        Add conf_files found in value list (AddConfFileToNodeGroup)
+        Delets conf_files not found in value list (DeleteConfFileFromNodeGroup)
+        """
+
+        assert 'conf_file_ids' in self
+        assert 'nodegroup_id' in self
+        assert isinstance(value, list)
+
+        conf_file_ids = self.separate_types(value)[0]
+
+        if self['conf_file_ids'] != conf_file_ids:
+            from PLC.Methods.AddConfFileToNodeGroup import AddConfFileToNodeGroup
+            from PLC.Methods.DeleteConfFileFromNodeGroup import DeleteConfFileFromNodeGroup
+            new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
+            stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
+
+            for new_conf_file in new_conf_files:
+                AddConfFileToNodeGroup.__call__(AddConfFileToNodeGroup(self.api), auth, new_conf_file, self['nodegroup_id'])
+            for stale_conf_file in stale_conf_files:
+                DeleteConfFileFromNodeGroup.__call__(DeleteConfFileFromNodeGroup(self.api), auth, stale_conf_file, self['nodegroup_id'])
+
+
+class NodeGroups(Table):
+    """
+    Representation of row(s) from the nodegroups table in the
+    database.
+    """
+
+    def __init__(self, api, nodegroup_filter = None, columns = None):
+        Table.__init__(self, api, NodeGroup, columns)
+
+        sql = "SELECT %s FROM view_nodegroups WHERE True" % \
+              ", ".join(self.columns)
+
+        if nodegroup_filter is not None:
+            if isinstance(nodegroup_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), nodegroup_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), nodegroup_filter)
+                nodegroup_filter = Filter(NodeGroup.fields, {'nodegroup_id': ints, 'name': strs})
+                sql += " AND (%s) %s" % nodegroup_filter.sql(api, "OR")
+            elif isinstance(nodegroup_filter, dict):
+                nodegroup_filter = Filter(NodeGroup.fields, nodegroup_filter)
+                sql += " AND (%s) %s" % nodegroup_filter.sql(api, "AND")
+
+        self.selectall(sql)
diff --git a/PLC/NodeNetworkSettingTypes.py b/PLC/NodeNetworkSettingTypes.py
new file mode 100644 (file)
index 0000000..69f36b7
--- /dev/null
@@ -0,0 +1,83 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+from PLC.Roles import Role, Roles
+
+class NodeNetworkSettingType (Row):
+
+    """
+    Representation of a row in the nodenetwork_setting_types table.
+    """
+
+    table_name = 'nodenetwork_setting_types'
+    primary_key = 'nodenetwork_setting_type_id'
+    join_tables = ['nodenetwork_setting']
+    fields = {
+        'nodenetwork_setting_type_id': Parameter(int, "Nodenetwork setting type identifier"),
+        'name': Parameter(str, "Nodenetwork setting type name", max = 100),
+        'description': Parameter(str, "Nodenetwork setting type description", max = 254),
+        'category' : Parameter (str, "Nodenetwork setting category", max=64),
+        'min_role_id': Parameter(int, "Minimum (least powerful) role that can set or change this attribute"),
+        }
+
+    # for Cache
+    class_key = 'name'
+    foreign_fields = ['category','description','min_role_id']
+    foreign_xrefs = []
+
+    def validate_name(self, name):
+        if not len(name):
+            raise PLCInvalidArgument, "nodenetwork setting type name must be set"
+
+        conflicts = NodeNetworkSettingTypes(self.api, [name])
+        for setting_type in conflicts:
+            if 'nodenetwork_setting_type_id' not in self or \
+                   self['nodenetwork_setting_type_id'] != setting_type['nodenetwork_setting_type_id']:
+                raise PLCInvalidArgument, "nodenetwork setting type name already in use"
+
+        return name
+
+    def validate_min_role_id(self, role_id):
+        roles = [row['role_id'] for row in Roles(self.api)]
+        if role_id not in roles:
+            raise PLCInvalidArgument, "Invalid role"
+
+        return role_id
+
+class NodeNetworkSettingTypes(Table):
+    """
+    Representation of row(s) from the nodenetwork_setting_types table
+    in the database.
+    """
+
+    def __init__(self, api, nodenetwork_setting_type_filter = None, columns = None):
+        Table.__init__(self, api, NodeNetworkSettingType, columns)
+
+        sql = "SELECT %s FROM nodenetwork_setting_types WHERE True" % \
+              ", ".join(self.columns)
+
+        if nodenetwork_setting_type_filter is not None:
+            if isinstance(nodenetwork_setting_type_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), nodenetwork_setting_type_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), nodenetwork_setting_type_filter)
+                nodenetwork_setting_type_filter = Filter(NodeNetworkSettingType.fields, {'nodenetwork_setting_type_id': ints, 'name': strs})
+                sql += " AND (%s) %s" % nodenetwork_setting_type_filter.sql(api, "OR")
+            elif isinstance(nodenetwork_setting_type_filter, dict):
+                nodenetwork_setting_type_filter = Filter(NodeNetworkSettingType.fields, nodenetwork_setting_type_filter)
+                sql += " AND (%s) %s" % nodenetwork_setting_type_filter.sql(api, "AND")
+            elif isinstance (nodenetwork_setting_type_filter, StringTypes):
+                nodenetwork_setting_type_filter = Filter(NodeNetworkSettingType.fields, {'name':[nodenetwork_setting_type_filter]})
+                sql += " AND (%s) %s" % nodenetwork_setting_type_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong nodenetwork setting type filter %r"%nodenetwork_setting_type_filter
+
+        self.selectall(sql)
diff --git a/PLC/NodeNetworkSettings.py b/PLC/NodeNetworkSettings.py
new file mode 100644 (file)
index 0000000..bcf2506
--- /dev/null
@@ -0,0 +1,57 @@
+#
+# Thierry Parmentelat - INRIA
+#
+# $Revision: 5574 $
+#
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+from PLC.NodeNetworkSettingTypes import NodeNetworkSettingType, NodeNetworkSettingTypes
+
+class NodeNetworkSetting(Row):
+    """
+    Representation of a row in the nodenetwork_setting.
+    To use, instantiate with a dict of values.
+    """
+
+    table_name = 'nodenetwork_setting'
+    primary_key = 'nodenetwork_setting_id'
+    fields = {
+        'nodenetwork_setting_id': Parameter(int, "Nodenetwork setting identifier"),
+        'nodenetwork_id': Parameter(int, "NodeNetwork identifier"),
+        'nodenetwork_setting_type_id': NodeNetworkSettingType.fields['nodenetwork_setting_type_id'],
+        'name': NodeNetworkSettingType.fields['name'],
+        'description': NodeNetworkSettingType.fields['description'],
+        'category': NodeNetworkSettingType.fields['category'],
+        'min_role_id': NodeNetworkSettingType.fields['min_role_id'],
+        'value': Parameter(str, "Nodenetwork setting value"),
+       ### relations
+       
+        }
+
+class NodeNetworkSettings(Table):
+    """
+    Representation of row(s) from the nodenetwork_setting table in the
+    database.
+    """
+
+    def __init__(self, api, nodenetwork_setting_filter = None, columns = None):
+        Table.__init__(self, api, NodeNetworkSetting, columns)
+
+        sql = "SELECT %s FROM view_nodenetwork_settings WHERE True" % \
+              ", ".join(self.columns)
+
+        if nodenetwork_setting_filter is not None:
+            if isinstance(nodenetwork_setting_filter, (list, tuple, set)):
+                nodenetwork_setting_filter = Filter(NodeNetworkSetting.fields, {'nodenetwork_setting_id': nodenetwork_setting_filter})
+            elif isinstance(nodenetwork_setting_filter, dict):
+                nodenetwork_setting_filter = Filter(NodeNetworkSetting.fields, nodenetwork_setting_filter)
+            elif isinstance(nodenetwork_setting_filter, int):
+                nodenetwork_setting_filter = Filter(NodeNetworkSetting.fields, {'nodenetwork_setting_id': [nodenetwork_setting_filter]})
+            else:
+                raise PLCInvalidArgument, "Wrong nodenetwork setting filter %r"%nodenetwork_setting_filter
+            sql += " AND (%s) %s" % nodenetwork_setting_filter.sql(api)
+
+
+        self.selectall(sql)
diff --git a/PLC/NodeNetworks.py b/PLC/NodeNetworks.py
new file mode 100644 (file)
index 0000000..1d072a6
--- /dev/null
@@ -0,0 +1,231 @@
+#
+# 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: NodeNetworks.py 7159 2007-11-27 22:05:24Z dhozac $
+#
+
+from types import StringTypes
+import socket
+import struct
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.NetworkTypes import NetworkType, NetworkTypes
+from PLC.NetworkMethods import NetworkMethod, NetworkMethods
+import PLC.Nodes
+
+def valid_ip(ip):
+    try:
+        ip = socket.inet_ntoa(socket.inet_aton(ip))
+        return True
+    except socket.error:
+        return False
+
+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 sync().
+    """
+
+    table_name = 'nodenetworks'
+    primary_key = 'nodenetwork_id'
+    join_tables = ['nodenetwork_setting']
+    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", nullok = True),
+        'mac': Parameter(str, "MAC address", nullok = True),
+        'gateway': Parameter(str, "IP address of primary gateway", nullok = True),
+        'network': Parameter(str, "Subnet address", nullok = True),
+        'broadcast': Parameter(str, "Network broadcast address", nullok = True),
+        'netmask': Parameter(str, "Subnet mask", nullok = True),
+        'dns1': Parameter(str, "IP address of primary DNS server", nullok = True),
+        'dns2': Parameter(str, "IP address of secondary DNS server", nullok = True),
+        'bwlimit': Parameter(int, "Bandwidth limit", min = 0, nullok = True),
+        'hostname': Parameter(str, "(Optional) Hostname", nullok = True),
+        'node_id': Parameter(int, "Node associated with this interface"),
+        'is_primary': Parameter(bool, "Is the primary interface for this node"),
+        'nodenetwork_setting_ids' : Parameter([int], "List of nodenetwork settings"),
+        }
+
+    def validate_method(self, method):
+        network_methods = [row['method'] for row in NetworkMethods(self.api)]
+        if method not in network_methods:
+            raise PLCInvalidArgument, "Invalid addressing method %s"%method
+       return method
+
+    def validate_type(self, type):
+        network_types = [row['type'] for row in NetworkTypes(self.api)]
+        if type not in network_types:
+            raise PLCInvalidArgument, "Invalid address type %s"%type
+       return type
+
+    def validate_ip(self, ip):
+        if ip and not valid_ip(ip):
+            raise PLCInvalidArgument, "Invalid IP address %s"%ip
+        return ip
+
+    def validate_mac(self, mac):
+        if not mac:
+            return 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 %s"%mac
+
+        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 not bwlimit:
+           return bwlimit
+
+       if bwlimit < 500000:
+           raise PLCInvalidArgument, 'Minimum bw is 500 kbs'
+
+       return bwlimit  
+
+    def validate_hostname(self, hostname):
+        # Optional
+        if not hostname:
+            return hostname
+
+        if not PLC.Nodes.valid_hostname(hostname):
+            raise PLCInvalidArgument, "Invalid hostname %s"%hostname
+
+        return hostname
+
+    def validate_node_id(self, node_id):
+        nodes = PLC.Nodes.Nodes(self.api, [node_id])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node %d"%node_id
+
+        return node_id
+
+    def validate_is_primary(self, is_primary):
+        """
+        Set this interface to be the primary one.
+        """
+
+        if is_primary:
+            nodes = PLC.Nodes.Nodes(self.api, [self['node_id']])
+            if not nodes:
+                raise PLCInvalidArgument, "No such node %d"%node_id
+            node = nodes[0]
+
+            if node['nodenetwork_ids']:
+                conflicts = NodeNetworks(self.api, node['nodenetwork_ids'])
+                for nodenetwork in conflicts:
+                    if ('nodenetwork_id' not in self or \
+                        self['nodenetwork_id'] != nodenetwork['nodenetwork_id']) and \
+                       nodenetwork['is_primary']:
+                        raise PLCInvalidArgument, "Can only set one primary interface per node"
+
+        return is_primary
+
+    def validate(self):
+        """
+        Flush changes back to the database.
+        """
+
+        # Basic validation
+        Row.validate(self)
+
+        assert 'method' in self
+        method = self['method']
+
+        if method == "proxy" or method == "tap":
+            if 'mac' in self and self['mac']:
+                raise PLCInvalidArgument, "For %s method, mac should not be specified" % method
+            if 'ip' not in self or not self['ip']:
+                raise PLCInvalidArgument, "For %s method, ip is required" % method
+            if method == "tap" and ('gateway' not in self or not self['gateway']):
+                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":
+            if 'is_primary' in self and self['is_primary'] is True:
+                for key in ['gateway', 'dns1']:
+                    if key not in self or not self[key]:
+                        raise PLCInvalidArgument, "For static method primary network, %s is required" % key
+                    globals()[key] = self[key]
+            for key in ['ip', 'network', 'broadcast', 'netmask']:
+                if key not in self or not self[key]:
+                    raise PLCInvalidArgument, "For static method, %s is required" % key
+                globals()[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 'gateway' in globals() and 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 or not self['ip']:
+                raise PLCInvalidArgument, "For ipmi method, ip is required"
+
+class NodeNetworks(Table):
+    """
+    Representation of row(s) from the nodenetworks table in the
+    database.
+    """
+
+    def __init__(self, api, nodenetwork_filter = None, columns = None):
+        Table.__init__(self, api, NodeNetwork, columns)
+
+        sql = "SELECT %s FROM view_nodenetworks WHERE True" % \
+              ", ".join(self.columns)
+
+        if nodenetwork_filter is not None:
+            if isinstance(nodenetwork_filter, (list, tuple, set)):
+                nodenetwork_filter = Filter(NodeNetwork.fields, {'nodenetwork_id': nodenetwork_filter})
+            elif isinstance(nodenetwork_filter, dict):
+                nodenetwork_filter = Filter(NodeNetwork.fields, nodenetwork_filter)
+            elif isinstance(nodenetwork_filter, int):
+                nodenetwork_filter = Filter(NodeNetwork.fields, {'nodenetwork_id': [nodenetwork_filter]})
+            else:
+                raise PLCInvalidArgument, "Wrong node network filter %r"%nodenetwork_filter
+            sql += " AND (%s) %s" % nodenetwork_filter.sql(api)
+
+        self.selectall(sql)
diff --git a/PLC/Nodes.py b/PLC/Nodes.py
new file mode 100644 (file)
index 0000000..ac7b126
--- /dev/null
@@ -0,0 +1,326 @@
+#
+# 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: Nodes.py 5654 2007-11-06 03:43:55Z tmack $
+#
+
+from types import StringTypes
+import re
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.NodeNetworks import NodeNetwork, NodeNetworks
+from PLC.BootStates import BootStates
+
+def valid_hostname(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}$'
+    return hostname and \
+           re.match(good_hostname, hostname, re.IGNORECASE)
+
+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 sync().
+    """
+
+    table_name = 'nodes'
+    primary_key = 'node_id'
+    # Thierry -- we use delete on nodenetworks so the related NodeNetworkSettings get deleted too
+    join_tables = ['nodegroup_node', 'conf_file_node', 'pcu_node', 'slice_node', 'slice_attribute', 'node_session', 'peer_node','node_slice_whitelist']
+    fields = {
+        'node_id': Parameter(int, "Node identifier"),
+        'hostname': Parameter(str, "Fully qualified hostname", max = 255),
+        'site_id': Parameter(int, "Site at which this node is located"),
+        'boot_state': Parameter(str, "Boot state", max = 20),
+        'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
+        'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
+        'version': Parameter(str, "Apparent Boot CD version", max = 64),
+        'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
+        'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
+        'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
+       'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), 
+        'key': Parameter(str, "(Admin only) Node key", max = 256),
+        'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
+        'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
+        '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"),
+        'slice_ids': Parameter([int], "List of slices on this node"),
+       'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
+        'pcu_ids': Parameter([int], "List of PCUs that control this node"),
+        'ports': Parameter([int], "List of PCU ports that this node is connected to"),
+        'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
+        'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
+        }
+    related_fields = {
+       'nodenetworks': [Mixed(Parameter(int, "NodeNetwork identifier"),
+                                      Filter(NodeNetwork.fields))],
+       'nodegroups': [Mixed(Parameter(int, "NodeGroup identifier"),
+                             Parameter(str, "NodeGroup name"))],
+       'conf_files': [Parameter(int, "ConfFile identifier")],
+       'slices': [Mixed(Parameter(int, "Slice identifier"),
+                         Parameter(str, "Slice name"))],
+       'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
+                                   Parameter(str, "Slice name"))]
+       }
+    # for Cache
+    class_key = 'hostname'
+    foreign_fields = ['boot_state','model','version']
+    # forget about these ones, they are read-only anyway
+    # handling them causes Cache to re-sync all over again 
+    # 'date_created','last_updated'
+    foreign_xrefs = [
+       # in this case, we dont need the 'table' but Cache will look it up, so...
+        {'field' : 'site_id' , 'class' : 'Site' , 'table' : 'unused-on-direct-refs' } ,
+       ]
+
+    def validate_hostname(self, hostname):
+        if not valid_hostname(hostname):
+            raise PLCInvalidArgument, "Invalid hostname"
+
+        conflicts = Nodes(self.api, [hostname])
+        for node in conflicts:
+            if 'node_id' not in self or self['node_id'] != node['node_id']:
+                raise PLCInvalidArgument, "Hostname already in use"
+
+        return hostname
+
+    def validate_boot_state(self, boot_state):
+        boot_states = [row['boot_state'] for row in BootStates(self.api)]
+        if boot_state not in boot_states:
+            raise PLCInvalidArgument, "Invalid boot state"
+
+        return boot_state
+
+    validate_date_created = Row.validate_timestamp
+    validate_last_updated = Row.validate_timestamp
+    validate_last_contact = Row.validate_timestamp
+
+    def update_last_contact(self, commit = True):
+       """
+       Update last_contact field with current time
+       """
+       
+       assert 'node_id' in self
+       assert self.table_name
+
+       self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
+                      " where node_id = %d" % ( self['node_id']) )
+       self.sync(commit)
+
+
+    def update_last_updated(self, commit = True):
+        """
+        Update last_updated field with current time
+        """
+
+        assert 'node_id' in self
+        assert self.table_name
+
+        self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
+                       " where node_id = %d" % (self['node_id']) )
+        self.sync(commit)
+
+    def associate_nodenetworks(self, auth, field, value):
+       """
+       Delete nodenetworks not found in value list (using DeleteNodeNetwor)k   
+       Add nodenetworks found in value list (using AddNodeNetwork)
+       Updates nodenetworks found w/ nodenetwork_id in value list (using UpdateNodeNetwork) 
+       """
+
+       assert 'nodenetworkp_ids' in self
+        assert 'node_id' in self
+        assert isinstance(value, list)
+
+        (nodenetwork_ids, blank, nodenetworks) = self.separate_types(value)
+
+        if self['nodenetwork_ids'] != nodenetwork_ids:
+            from PLC.Methods.DeleteNodeNetwork import DeleteNodeNetwork
+
+            stale_nodenetworks = set(self['nodenetwork_ids']).difference(nodenetwork_ids)
+
+            for stale_nodenetwork in stale_nodenetworks:
+                DeleteNodeNetwork.__call__(DeleteNodeNetwork(self.api), auth, stale_nodenetwork['nodenetwork_id'])
+
+    def associate_nodegroups(self, auth, field, value):
+       """
+       Add node to nodegroups found in value list (AddNodeToNodegroup)
+       Delete node from nodegroup not found in value list (DeleteNodeFromNodegroup)
+       """
+       
+       from PLC.NodeGroups import NodeGroups
+       
+       assert 'nodegroup_ids' in self
+       assert 'node_id' in self
+       assert isinstance(value, list)
+
+       (nodegroup_ids, nodegroup_names) = self.separate_types(value)[0:2]
+       
+       if nodegroup_names:
+           nodegroups = NodeGroups(self.api, nodegroup_names, ['nodegroup_id']).dict('nodegroup_id')
+           nodegroup_ids += nodegroups.keys()
+
+       if self['nodegroup_ids'] != nodegroup_ids:
+           from PLC.Methods.AddNodeToNodeGroup import AddNodeToNodeGroup
+           from PLC.Methods.DeleteNodeFromNodeGroup import DeleteNodeFromNodeGroup
+       
+           new_nodegroups = set(nodegroup_ids).difference(self['nodegroup_ids'])
+           stale_nodegroups = set(self['nodegroup_ids']).difference(nodegroup_ids)
+       
+           for new_nodegroup in new_nodegroups:
+               AddNodeToNodeGroup.__call__(AddNodeToNodeGroup(self.api), auth, self['node_id'], new_nodegroup)
+           for stale_nodegroup in stale_nodegroups:
+               DeleteNodeFromNodeGroup.__call__(DeleteNodeFromNodeGroup(self.api), auth, self['node_id'], stale_nodegroup)
+         
+
+    def associate_conf_files(self, auth, field, value):
+       """
+       Add conf_files found in value list (AddConfFileToNode)
+       Delets conf_files not found in value list (DeleteConfFileFromNode)
+       """
+       
+       assert 'conf_file_ids' in self
+       assert 'node_id' in self
+       assert isinstance(value, list)
+       
+       conf_file_ids = self.separate_types(value)[0]
+       
+       if self['conf_file_ids'] != conf_file_ids:
+           from PLC.Methods.AddConfFileToNode import AddConfFileToNode
+           from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
+           new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
+           stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
+       
+           for new_conf_file in new_conf_files:
+               AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
+           for stale_conf_file in stale_conf_files:
+               DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
+
+    def associate_slices(self, auth, field, value):
+       """
+       Add slices found in value list to (AddSliceToNode)
+       Delete slices not found in value list (DeleteSliceFromNode)
+       """
+       
+       from PLC.Slices import Slices
+       
+       assert 'slice_ids' in self
+       assert 'node_id' in self
+       assert isinstance(value, list)
+       
+       (slice_ids, slice_names) = self.separate_types(value)[0:2]
+
+       if slice_names:
+           slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
+           slice_ids += slices.keys()
+
+       if self['slice_ids'] != slice_ids:
+           from PLC.Methods.AddSliceToNodes import AddSliceToNodes
+           from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
+           new_slices = set(slice_ids).difference(self['slice_ids'])
+           stale_slices = set(self['slice_ids']).difference(slice_ids)
+       
+       for new_slice in new_slices:
+           AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
+       for stale_slice in stale_slices:
+           DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
+
+    def associate_slices_whitelist(self, auth, field, value):
+       """
+       Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
+       Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
+       """
+
+       from PLC.Slices import Slices
+
+       assert 'slice_ids_whitelist' in self
+        assert 'node_id' in self
+        assert isinstance(value, list)
+
+       (slice_ids, slice_names) = self.separate_types(value)[0:2]
+
+        if slice_names:
+            slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
+            slice_ids += slices.keys()
+
+        if self['slice_ids_whitelist'] != slice_ids:
+            from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
+            from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
+            new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
+            stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
+
+        for new_slice in new_slices:
+            AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
+        for stale_slice in stale_slices:
+            DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
+               
+
+    def delete(self, commit = True):
+        """
+        Delete existing node.
+        """
+
+        assert 'node_id' in self
+       assert 'nodenetwork_ids' in self
+
+       # we need to clean up NodeNetworkSettings, so handling nodenetworks as part of join_tables does not work
+       for nodenetwork in NodeNetworks(self.api,self['nodenetwork_ids']):
+           nodenetwork.delete()
+
+        # Clean up miscellaneous join tables
+        for table in self.join_tables:
+            self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
+                           (table, self['node_id']))
+
+        # Mark as deleted
+        self['deleted'] = True
+        self.sync(commit)
+
+
+class Nodes(Table):
+    """
+    Representation of row(s) from the nodes table in the
+    database.
+    """
+
+    def __init__(self, api, node_filter = None, columns = None):
+        Table.__init__(self, api, Node, columns)
+
+        sql = "SELECT %s FROM view_nodes WHERE deleted IS False" % \
+              ", ".join(self.columns)
+
+        if node_filter is not None:
+            if isinstance(node_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
+                node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
+                sql += " AND (%s) %s" % node_filter.sql(api, "OR")
+            elif isinstance(node_filter, dict):
+                node_filter = Filter(Node.fields, node_filter)
+                sql += " AND (%s) %s" % node_filter.sql(api, "AND")
+            elif isinstance (node_filter, StringTypes):
+                node_filter = Filter(Node.fields, {'hostname':[node_filter]})
+                sql += " AND (%s) %s" % node_filter.sql(api, "AND")
+            elif isinstance (node_filter, int):
+                node_filter = Filter(Node.fields, {'node_id':[node_filter]})
+                sql += " AND (%s) %s" % node_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
+
+        self.selectall(sql)
diff --git a/PLC/PCUProtocolTypes.py b/PLC/PCUProtocolTypes.py
new file mode 100644 (file)
index 0000000..9475d31
--- /dev/null
@@ -0,0 +1,75 @@
+#
+# Functions for interacting with the pcu_type_port 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.Table import Row, Table
+from PLC.Filter import Filter
+
+class PCUProtocolType(Row):
+    """
+    Representation of a row in the pcu_protocol_type table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'pcu_protocol_type'
+    primary_key = 'pcu_protocol_type_id'
+    join_tables = []
+    fields = {
+       'pcu_protocol_type_id': Parameter(int, "PCU protocol type identifier"),
+       'pcu_type_id': Parameter(int, "PCU type identifier"), 
+       'port': Parameter(int, "PCU port"),
+       'protocol': Parameter(str, "Protocol"),
+       'supported': Parameter(bool, "Is the port/protocol supported by PLC") 
+        }
+
+    def validate_port(self, port):
+       # make sure port is not blank
+       
+       if not port:
+            raise PLCInvalidArgument, "Port must be specified"
+       
+       return port
+
+    def validate_protocol(self, protocol):
+        # make sure port is not blank
+        if not len(protocol):
+            raise PLCInvalidArgument, "protocol must be specified"
+
+       return protocol
+
+class PCUProtocolTypes(Table):
+    """
+    Representation of the pcu_protocol_types table in the database.
+    """
+
+    def __init__(self, api, protocol_type_filter = None, columns = None):
+        Table.__init__(self, api, PCUProtocolType, columns)
+
+        sql = "SELECT %s FROM pcu_protocol_type WHERE True" % \
+              ", ".join(self.columns)
+        
+       if protocol_type_filter is not None:
+            if isinstance(protocol_type_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), protocol_type_filter)
+                protocol_type_filter = Filter(PCUProtocolType.fields, {'pcu_protocol_type_id': ints})
+                sql += " AND (%s) %s" % protocol_type_filter.sql(api, "OR")
+            elif isinstance(protocol_type_filter, dict):
+                protocol_type_filter = Filter(PCUProtocolType.fields, protocol_type_filter)
+                sql += " AND (%s) %s" % protocol_type_filter.sql(api, "AND")
+            elif isinstance (protocol_type_filter, int):
+                protocol_type_filter = Filter(PCUProtocolType.fields, {'pcu_protocol_type_id':[protocol_type_filter]})
+
+                sql += " AND (%s) %s" % protocol_type_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong pcu_protocol_type filter %r"%protocol_type_filter     
+
+
+        self.selectall(sql)
diff --git a/PLC/PCUTypes.py b/PLC/PCUTypes.py
new file mode 100644 (file)
index 0000000..f0e7376
--- /dev/null
@@ -0,0 +1,104 @@
+#
+# Functions for interacting with the pcu_types table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: 
+#
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+from PLC.Filter import Filter
+
+class PCUType(Row):
+    """
+    Representation of a row in the pcu_types table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'pcu_types'
+    primary_key = 'pcu_type_id'
+    join_tables = ['pcu_protocol_type']
+    fields = {
+        'pcu_type_id': Parameter(int, "PCU Type Identifier"),
+       'model': Parameter(str, "PCU model", max = 254),
+       'name': Parameter(str, "PCU full name", max = 254),
+        'pcu_protocol_type_ids': Parameter([int], "PCU Protocol Type Identifiers"),
+       'pcu_protocol_types': Parameter([dict], "PCU Protocol Type List")
+        }
+
+    def validate_model(self, model):
+       # Make sure name is not blank
+        if not len(model):
+            raise PLCInvalidArgument, "Model must be specified"
+       
+       # Make sure boot state does not alredy exist
+       conflicts = PCUTypes(self.api, [model])
+        for pcu_type in conflicts:
+           if 'pcu_type_id' not in self or self['pcu_type_id'] != pcu_type['pcu_type_id']: 
+               raise PLCInvalidArgument, "Model already in use"
+
+       return model
+
+class PCUTypes(Table):
+    """
+    Representation of the pcu_types table in the database.
+    """
+
+    def __init__(self, api, pcu_type_filter = None, columns = None):
+
+       # Remove pcu_protocol_types from query since its not really a field
+       # in the db. We will add it later
+       if columns == None:
+           columns = PCUType.fields.keys()
+       if 'pcu_protocol_types' in columns:
+           removed_fields = ['pcu_protocol_types']
+           columns.remove('pcu_protocol_types')
+       else:
+           removed_fields = []
+
+        Table.__init__(self, api, PCUType, columns)
+
+        sql = "SELECT %s FROM view_pcu_types WHERE True" % \
+              ", ".join(self.columns)
+        
+       if pcu_type_filter is not None:
+            if isinstance(pcu_type_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), pcu_type_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), pcu_type_filter)
+                pcu_type_filter = Filter(PCUType.fields, {'pcu_type_id': ints, 'model': strs})
+                sql += " AND (%s) %s" % pcu_type_filter.sql(api, "OR")
+            elif isinstance(pcu_type_filter, dict):
+                pcu_type_filter = Filter(PCUType.fields, pcu_type_filter)
+                sql += " AND (%s) %s" % pcu_type_filter.sql(api, "AND")
+            elif isinstance (pcu_type_filter, StringTypes):
+                pcu_type_filter = Filter(PCUType.fields, {'model':[pcu_type_filter]})
+                sql += " AND (%s) %s" % pcu_type_filter.sql(api, "AND")
+            elif isinstance (pcu_type_filter, int):
+                pcu_type_filter = Filter(PCUType.fields, {'pcu_type_id':[pcu_type_filter]})
+                sql += " AND (%s) %s" % pcu_type_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong pcu_type filter %r"%pcu_type_filter   
+
+
+        self.selectall(sql)
+
+        # return a list of protocol type objects for each port type
+        if 'pcu_protocol_types' in removed_fields:
+           from PLC.PCUProtocolTypes import PCUProtocolTypes
+            protocol_type_ids = set()
+            for pcu_type in self:
+                protocol_type_ids.update(pcu_type['pcu_protocol_type_ids'])
+
+            protocol_return_fields = ['pcu_protocol_type_id', 'port', 'protocol', 'supported']
+            all_protocol_types = PCUProtocolTypes(self.api, list(protocol_type_ids), \
+                                                 protocol_return_fields).dict('pcu_protocol_type_id')
+
+            for pcu_type in self:
+                pcu_type['pcu_protocol_types'] = []
+                for protocol_type_id in pcu_type['pcu_protocol_type_ids']:
+                    pcu_type['pcu_protocol_types'].append(all_protocol_types[protocol_type_id])
diff --git a/PLC/PCUs.py b/PLC/PCUs.py
new file mode 100644 (file)
index 0000000..0ab56cc
--- /dev/null
@@ -0,0 +1,116 @@
+#
+# 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: PCUs.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.NodeNetworks import valid_ip, NodeNetwork, NodeNetworks
+from PLC.Nodes import Node, Nodes
+
+class PCU(Row):
+    """
+    Representation of a row in the pcus table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'pcus'
+    primary_key = 'pcu_id'
+    join_tables = ['pcu_node']
+    fields = {
+        'pcu_id': Parameter(int, "PCU identifier"),
+        'site_id': Parameter(int, "Identifier of site where PCU is located"),
+        'hostname': Parameter(str, "PCU hostname", max = 254),
+        'ip': Parameter(str, "PCU IP address", max = 254),
+        'protocol': Parameter(str, "PCU protocol, e.g. ssh, https, telnet", max = 16, nullok = True),
+        'username': Parameter(str, "PCU username", max = 254, nullok = True),
+        'password': Parameter(str, "PCU username", max = 254, nullok = True),
+        'notes': Parameter(str, "Miscellaneous notes", max = 254, nullok = True),
+        'model': Parameter(str, "PCU model string", max = 32, nullok = True),
+        'node_ids': Parameter([int], "List of nodes that this PCU controls"),
+        'ports': Parameter([int], "List of the port numbers that each node is connected to"),
+        }
+
+    def validate_ip(self, ip):
+        if not valid_ip(ip):
+            raise PLCInvalidArgument, "Invalid IP address " + ip
+        return ip
+
+    def add_node(self, node, port, commit = True):
+        """
+        Add node to existing PCU.
+        """
+
+        assert 'pcu_id' in self
+        assert isinstance(node, Node)
+        assert isinstance(port, (int, long))
+        assert 'node_id' in node
+
+        pcu_id = self['pcu_id']
+        node_id = node['node_id']
+
+        if node_id not in self['node_ids'] and port not in self['ports']:
+            self.api.db.do("INSERT INTO pcu_node (pcu_id, node_id, port)" \
+                           " VALUES(%(pcu_id)d, %(node_id)d, %(port)d)",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['node_ids'].append(node_id)
+            self['ports'].append(port)
+
+    def remove_node(self, node, commit = True):
+        """
+        Remove node from existing PCU.
+        """
+
+        assert 'pcu_id' in self
+        assert isinstance(node, Node)
+        assert 'node_id' in node
+
+        pcu_id = self['pcu_id']
+        node_id = node['node_id']
+
+        if node_id in self['node_ids']:
+            i = self['node_ids'].index(node_id)
+            port = self['ports'][i]
+
+            self.api.db.do("DELETE FROM pcu_node" \
+                           " WHERE pcu_id = %(pcu_id)d" \
+                           " AND node_id = %(node_id)d",
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+            self['node_ids'].remove(node_id)
+            self['ports'].remove(port)
+
+class PCUs(Table):
+    """
+    Representation of row(s) from the pcus table in the
+    database.
+    """
+
+    def __init__(self, api, pcu_filter = None, columns = None):
+        Table.__init__(self, api, PCU, columns)
+
+        sql = "SELECT %s FROM view_pcus WHERE True" % \
+              ", ".join(self.columns)
+
+        if pcu_filter is not None:
+            if isinstance(pcu_filter, (list, tuple, set)):
+                pcu_filter = Filter(PCU.fields, {'pcu_id': pcu_filter})
+            elif isinstance(pcu_filter, dict):
+                pcu_filter = Filter(PCU.fields, pcu_filter)
+            sql += " AND (%s) %s" % pcu_filter.sql(api)
+
+        self.selectall(sql)
diff --git a/PLC/POD.py b/PLC/POD.py
new file mode 100644 (file)
index 0000000..01a7ddd
--- /dev/null
@@ -0,0 +1,90 @@
+# Marc E. Fiuczynski <mef@cs.princeton.edu>
+# Copyright (C) 2004 The Trustees of Princeton University
+#
+# Client ping of death program for both udp & icmp
+#
+# modified for inclusion by api by Aaron K
+
+import struct
+import os
+import array
+import getopt
+from socket import *
+
+UPOD_PORT = 664
+
+def _in_cksum(packet):
+    """THE RFC792 states: 'The 16 bit one's complement of
+    the one's complement sum of all 16 bit words in the header.'
+    Generates a checksum of a (ICMP) packet. Based on in_chksum found
+    in ping.c on FreeBSD.
+    """
+
+    # add byte if not dividable by 2
+    if len(packet) & 1:
+        packet = packet + '\0'
+
+    # split into 16-bit word and insert into a binary array
+    words = array.array('h', packet)
+    sum = 0
+
+    # perform ones complement arithmetic on 16-bit words
+    for word in words:
+        sum += (word & 0xffff)
+
+    hi = sum >> 16
+    lo = sum & 0xffff
+    sum = hi + lo
+    sum = sum + (sum >> 16)
+
+    return (~sum) & 0xffff # return ones complement
+
+def _construct(id, data):
+    """Constructs a ICMP IPOD packet
+    """
+    ICMP_TYPE = 6 # ping of death code used by PLK
+    ICMP_CODE = 0
+    ICMP_CHECKSUM = 0
+    ICMP_ID = 0
+    ICMP_SEQ_NR = 0
+
+    header = struct.pack('bbHHh', ICMP_TYPE, ICMP_CODE, ICMP_CHECKSUM, \
+                         ICMP_ID, ICMP_SEQ_NR+id)
+
+    packet = header + data          # ping packet without checksum
+    checksum = _in_cksum(packet)    # make checksum
+
+    # construct header with correct checksum
+    header = struct.pack('bbHHh', ICMP_TYPE, ICMP_CODE, checksum, ICMP_ID, \
+                         ICMP_SEQ_NR+id)
+
+    # ping packet *with* checksum
+    packet = header + data
+
+    # a perfectly formatted ICMP echo packet
+    return packet
+
+def icmp_pod(host,key):
+    uid = os.getuid()
+    if uid <> 0:
+        print "must be root to send icmp pod"
+        return
+    
+    s = socket(AF_INET, SOCK_RAW, getprotobyname("icmp"))
+    packet = _construct(0, key) # make a ping packet
+    addr = (host,1)
+    print 'pod sending icmp-based reboot request to %s' % host
+    for i in range(1,10):
+        s.sendto(packet, addr)
+
+def udp_pod(host,key,fromaddr=('', 0)):
+    addr = host, UPOD_PORT
+    s = socket(AF_INET, SOCK_DGRAM)
+    s.bind(fromaddr)
+    packet = key
+    print 'pod sending udp-based reboot request to %s' % host
+    for i in range(1,10):
+        s.sendto(packet, addr)
+
+def noop_pod(host,key):
+    pass
diff --git a/PLC/Parameter.py b/PLC/Parameter.py
new file mode 100644 (file)
index 0000000..474ad78
--- /dev/null
@@ -0,0 +1,105 @@
+#
+# Shared type definitions
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Parameter.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from types import *
+from PLC.Faults import *
+
+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 = "",
+                 min = None, max = None,
+                 optional = None,
+                 ro = False,
+                 nullok = False):
+        # Basic type of the parameter. Must be a builtin type
+        # that can be marshalled by XML-RPC.
+        self.type = type
+
+        # Documentation string for the parameter
+        self.doc = doc
+
+        # Basic value checking. For numeric types, the minimum and
+        # maximum possible values, inclusive. For string types, the
+        # minimum and maximum possible UTF-8 encoded byte lengths.
+        self.min = min
+        self.max = max
+
+        # Whether the sub-parameter is optional or not. If None,
+        # unknown whether it is optional.
+        self.optional = optional
+
+        # Whether the DB field is read-only.
+        self.ro = ro
+
+        # Whether the DB field can be NULL.
+        self.nullok = nullok
+
+    def type(self):
+        return self.type
+
+    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)
+
+
+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
diff --git a/PLC/Peers.py b/PLC/Peers.py
new file mode 100644 (file)
index 0000000..0973c3d
--- /dev/null
@@ -0,0 +1,235 @@
+#
+# Thierry Parmentelat - INRIA
+# 
+
+import re
+from types import StringTypes
+from urlparse import urlparse
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+import PLC.Auth
+
+from PLC.Sites import Site, Sites
+from PLC.Persons import Person, Persons
+from PLC.Keys import Key, Keys
+from PLC.Nodes import Node, Nodes
+from PLC.SliceAttributeTypes import SliceAttributeType, SliceAttributeTypes
+from PLC.SliceAttributes import SliceAttribute, SliceAttributes
+from PLC.Slices import Slice, Slices
+
+class Peer(Row):
+    """
+    Stores the list of peering PLCs in the peers table. 
+    See the Row class for more details
+    """
+
+    table_name = 'peers'
+    primary_key = 'peer_id'
+    join_tables = ['peer_site', 'peer_person', 'peer_key', 'peer_node',
+                   'peer_slice_attribute_type', 'peer_slice_attribute', 'peer_slice']
+    fields = {
+       'peer_id': Parameter (int, "Peer identifier"),
+       'peername': Parameter (str, "Peer name"),
+       'peer_url': Parameter (str, "Peer API URL"),
+       'key': Parameter(str, "Peer GPG public key"),
+       'cacert': Parameter(str, "Peer SSL public certificate"),
+        ### cross refs
+        'site_ids': Parameter([int], "List of sites for which this peer is authoritative"),
+        'person_ids': Parameter([int], "List of users for which this peer is authoritative"),
+        'key_ids': Parameter([int], "List of keys for which this peer is authoritative"),
+        'node_ids': Parameter([int], "List of nodes for which this peer is authoritative"),
+        'slice_ids': Parameter([int], "List of slices for which this peer is authoritative"),
+       }
+
+    def validate_peername(self, peername):
+        if not len(peername):
+            raise PLCInvalidArgument, "Peer name must be specified"
+
+        conflicts = Peers(self.api, [peername])
+        for peer in conflicts:
+            if 'peer_id' not in self or self['peer_id'] != peer['peer_id']:
+                raise PLCInvalidArgument, "Peer name already in use"
+
+        return peername
+
+    def validate_peer_url(self, url):
+       """
+       Validate URL. Must be HTTPS.
+       """
+
+        (scheme, netloc, path, params, query, fragment) = urlparse(url)
+        if scheme != "https":
+            raise PLCInvalidArgument, "Peer URL scheme must be https"
+
+       return url
+
+    def delete(self, commit = True):
+       """
+       Deletes this peer and all related entities.
+       """
+
+       assert 'peer_id' in self
+
+        # Remove all related entities
+        for obj in \
+            Slices(self.api, self['slice_ids']) + \
+            Keys(self.api, self['key_ids']) + \
+            Persons(self.api, self['person_ids']) + \
+            Nodes(self.api, self['node_ids']) + \
+            Sites(self.api, self['site_ids']):
+            assert obj['peer_id'] == self['peer_id']
+            obj.delete(commit = False)
+
+        # Mark as deleted
+       self['deleted'] = True
+       self.sync(commit)
+
+    def add_site(self, site, peer_site_id, commit = True):
+        """
+        Associate a local site entry with this peer.
+        """
+
+        add = Row.add_object(Site, 'peer_site')
+        add(self, site,
+            {'peer_id': self['peer_id'],
+             'site_id': site['site_id'],
+             'peer_site_id': peer_site_id},
+            commit = commit)
+
+    def add_person(self, person, peer_person_id, commit = True):
+        """
+        Associate a local user entry with this peer.
+        """
+
+        add = Row.add_object(Person, 'peer_person')
+        add(self, person,
+            {'peer_id': self['peer_id'],
+             'person_id': person['person_id'],
+             'peer_person_id': peer_person_id},
+            commit = commit)
+
+    def add_key(self, key, peer_key_id, commit = True):
+        """
+        Associate a local key entry with this peer.
+        """
+
+        add = Row.add_object(Key, 'peer_key')
+        add(self, key,
+            {'peer_id': self['peer_id'],
+             'key_id': key['key_id'],
+             'peer_key_id': peer_key_id},
+            commit = commit)
+
+    def add_node(self, node, peer_node_id, commit = True):
+        """
+        Associate a local node entry with this peer.
+        """
+
+        add = Row.add_object(Node, 'peer_node')
+        add(self, node,
+            {'peer_id': self['peer_id'],
+             'node_id': node['node_id'],
+             'peer_node_id': peer_node_id},
+            commit = commit)
+
+    def add_slice(self, slice, peer_slice_id, commit = True):
+        """
+        Associate a local slice entry with this peer.
+        """
+
+        add = Row.add_object(Slice, 'peer_slice')
+        add(self, slice,
+            {'peer_id': self['peer_id'],
+             'slice_id': slice['slice_id'],
+             'peer_slice_id': peer_slice_id},
+            commit = commit)
+
+    def connect(self, **kwds):
+        """
+        Connect to this peer via XML-RPC.
+        """
+
+        import xmlrpclib
+        from PLC.PyCurl import PyCurlTransport
+        self.server = xmlrpclib.ServerProxy(self['peer_url'],
+                                            PyCurlTransport(self['peer_url'], self['cacert']),
+                                            allow_none = 1, **kwds)
+
+    def add_auth(self, function, methodname, **kwds):
+        """
+        Sign the specified XML-RPC call and add an auth struct as the
+        first argument of the call.
+        """
+
+        def wrapper(*args, **kwds):
+            from PLC.GPG import gpg_sign
+            signature = gpg_sign(args,
+                                 self.api.config.PLC_ROOT_GPG_KEY,
+                                 self.api.config.PLC_ROOT_GPG_KEY_PUB,
+                                 methodname)
+
+            auth = {'AuthMethod': "gpg",
+                    'name': self.api.config.PLC_NAME,
+                    'signature': signature}
+
+            # Automagically add auth struct to every call
+            args = (auth,) + args
+
+            return function(*args)
+
+        return wrapper
+
+    def __getattr__(self, attr):
+        """
+        Returns a callable API function if attr is the name of a
+        PLCAPI function; otherwise, returns the specified attribute.
+        """
+
+        try:
+            # Figure out if the specified attribute is the name of a
+            # PLCAPI function. If so and the function requires an
+            # authentication structure as its first argument, return a
+            # callable that automagically adds an auth struct to the
+            # call.
+            methodname = attr
+            api_function = self.api.callable(methodname)
+            if api_function.accepts and \
+               (isinstance(api_function.accepts[0], PLC.Auth.Auth) or \
+                (isinstance(api_function.accepts[0], Mixed) and \
+                 filter(lambda param: isinstance(param, Auth), api_function.accepts[0]))):
+                function = getattr(self.server, methodname)
+                return self.add_auth(function, methodname)
+        except Exception, err:
+            pass
+
+        if hasattr(self, attr):
+            return getattr(self, attr)
+        else:
+            raise AttributeError, "type object 'Peer' has no attribute '%s'" % attr
+
+class Peers (Table):
+    """ 
+    Maps to the peers table in the database
+    """
+    
+    def __init__ (self, api, peer_filter = None, columns = None):
+        Table.__init__(self, api, Peer, columns)
+
+       sql = "SELECT %s FROM view_peers WHERE deleted IS False" % \
+              ", ".join(self.columns)
+
+        if peer_filter is not None:
+            if isinstance(peer_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), peer_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), peer_filter)
+                peer_filter = Filter(Peer.fields, {'peer_id': ints, 'peername': strs})
+                sql += " AND (%s) %s" % peer_filter.sql(api, "OR")
+            elif isinstance(peer_filter, dict):
+                peer_filter = Filter(Peer.fields, peer_filter)
+                sql += " AND (%s) %s" % peer_filter.sql(api, "AND")
+
+       self.selectall(sql)
diff --git a/PLC/Persons.py b/PLC/Persons.py
new file mode 100644 (file)
index 0000000..d2bb510
--- /dev/null
@@ -0,0 +1,501 @@
+#
+# 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: Persons.py 5652 2007-11-06 03:42:57Z tmack $
+#
+
+from types import StringTypes
+from datetime import datetime
+import md5
+import time
+from random import Random
+import re
+import crypt
+
+from PLC.Faults import *
+from PLC.Debug import log
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+from PLC.Roles import Role, Roles
+from PLC.Keys import Key, Keys
+from PLC.Messages import Message, Messages
+
+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 sync().
+    """
+
+    table_name = 'persons'
+    primary_key = 'person_id'
+    join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
+    fields = {
+        'person_id': Parameter(int, "User identifier"),
+        'first_name': Parameter(str, "Given name", max = 128),
+        'last_name': Parameter(str, "Surname", max = 128),
+        'title': Parameter(str, "Title", max = 128, nullok = True),
+        'email': Parameter(str, "Primary e-mail address", max = 254),
+        'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
+        'url': Parameter(str, "Home page", max = 254, nullok = True),
+        'bio': Parameter(str, "Biography", max = 254, nullok = True),
+        'enabled': Parameter(bool, "Has been enabled"),
+        'password': Parameter(str, "Account password in crypt() form", max = 254),
+        'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
+       'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
+       'last_updated': Parameter(int, "Date and time of last update", ro = True),
+        'date_created': Parameter(int, "Date and time when account was created", ro = True),
+        'role_ids': Parameter([int], "List of role identifiers"),
+        'roles': Parameter([str], "List of roles"),
+        'site_ids': Parameter([int], "List of site identifiers"),
+        'key_ids': Parameter([int], "List of key identifiers"),
+        'slice_ids': Parameter([int], "List of slice identifiers"),
+        'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
+        'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
+        }
+    related_fields = {
+       'roles': [Mixed(Parameter(int, "Role identifier"),
+                       Parameter(str, "Role name"))],
+       'sites': [Mixed(Parameter(int, "Site identifier"),
+                       Parameter(str, "Site name"))],
+       'keys': [Mixed(Parameter(int, "Key identifier"),
+                      Filter(Key.fields))],
+       'slices': [Mixed(Parameter(int, "Slice identifier"),
+                        Parameter(str, "Slice name"))]
+       }       
+
+       
+
+    # for Cache
+    class_key = 'email'
+    foreign_fields = ['first_name', 'last_name', 'title', 'email', 'phone', 'url',
+                     'bio', 'enabled', 'password', ]
+    # forget about these ones, they are read-only anyway
+    # handling them causes Cache to re-sync all over again 
+    # 'last_updated', 'date_created'
+    foreign_xrefs = [
+        {'field' : 'key_ids',  'class': 'Key',  'table' : 'person_key' } ,
+        {'field' : 'site_ids', 'class': 'Site', 'table' : 'person_site'},
+#       xxx this is not handled by Cache yet
+#        'role_ids': Parameter([int], "List of role identifiers"),
+]
+
+    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 not allowed
+        if not domain:
+            raise invalid_email
+        if len(domain) < 2:
+            raise invalid_email
+
+               # check only against users on the same peer  
+       if 'peer_id' in self:
+            namespace_peer_id = self['peer_id']
+        else:
+            namespace_peer_id = None
+         
+       conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id}) 
+       
+       for person in conflicts:
+            if 'person_id' not in self or self['person_id'] != person['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.
+        """
+
+        magic = "$1$"
+
+        if len(password) > len(magic) and \
+           password[0:len(magic)] == magic:
+            return password
+        else:
+            # Generate a somewhat unique 8 character salt string
+            salt = str(time.time()) + str(Random().random())
+            salt = md5.md5(salt).hexdigest()[:8] 
+            return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
+
+    validate_date_created = Row.validate_timestamp
+    validate_last_updated = Row.validate_timestamp
+    validate_verification_expires = Row.validate_timestamp
+
+    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.
+        """
+
+        assert isinstance(person, Person)
+
+        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.
+        """
+
+        assert isinstance(person, Person)
+
+        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
+
+    add_role = Row.add_object(Role, 'person_role')
+    remove_role = Row.remove_object(Role, 'person_role')
+
+    add_key = Row.add_object(Key, 'person_key')
+    remove_key = Row.remove_object(Key, 'person_key')
+
+    def set_primary_site(self, site, commit = True):
+        """
+        Set the primary site for an existing user.
+        """
+
+        assert 'person_id' in self
+        assert 'site_id' in site
+
+        person_id = self['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())
+
+        if commit:
+            self.api.db.commit()
+
+        assert 'site_ids' in self
+        assert site_id in self['site_ids']
+
+        # Make sure that the primary site is first in the list
+        self['site_ids'].remove(site_id)
+        self['site_ids'].insert(0, site_id)
+
+    def update_last_updated(self, commit = True):
+        """
+        Update last_updated field with current time
+        """
+       
+       assert 'person_id' in self
+       assert self.table_name
+       
+       self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
+                       " where person_id = %d" % (self['person_id']) )
+        self.sync(commit)
+
+    def associate_roles(self, auth, field, value):
+       """
+       Adds roles found in value list to this person (using AddRoleToPerson).
+       Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
+       """
+       
+       assert 'role_ids' in self
+       assert 'person_id' in self
+       assert isinstance(value, list)
+       
+       (role_ids, roles_names) = self.separate_types(value)[0:2]
+       
+       # Translate roles into role_ids
+       if roles_names:
+           roles = Roles(self.api, role_names, ['role_id']).dict('role_id')
+           role_ids += roles.keys()
+       
+       # Add new ids, remove stale ids
+       if self['role_ids'] != role_ids:
+           from PLC.Methods.AddRoleToPerson import AddRoleToPerson
+           from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
+           new_roles = set(role_ids).difference(self['role_ids'])
+           stale_roles = set(self['role_ids']).difference(role_ids)
+
+           for new_role in new_roles:
+               AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
+           for stale_role in stale_roles:
+               DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
+
+
+    def associate_sites(self, auth, field, value):
+        """
+        Adds person to sites found in value list (using AddPersonToSite).
+        Deletes person from site not found in value list (using DeletePersonFromSite).
+        """
+
+       from PLC.Sites import Sites
+
+        assert 'site_ids' in self
+        assert 'person_id' in self
+        assert isinstance(value, list)
+
+        (site_ids, site_names) = self.separate_types(value)[0:2]
+
+        # Translate roles into role_ids
+        if site_names:
+            sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
+            site_ids += sites.keys()
+
+        # Add new ids, remove stale ids
+        if self['site_ids'] != site_ids:
+            from PLC.Methods.AddPersonToSite import AddPersonToSite
+            from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
+            new_sites = set(site_ids).difference(self['site_ids'])
+            stale_sites = set(self['site_ids']).difference(site_ids)
+
+            for new_site in new_sites:
+                AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
+            for stale_site in stale_sites:
+                DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
+
+
+    def associate_keys(self, auth, field, value):
+       """
+        Deletes key_ids not found in value list (using DeleteKey).
+        Adds key if key_fields w/o key_id is found (using AddPersonKey).
+        Updates key if key_fields w/ key_id is found (using UpdateKey).
+        """
+       assert 'key_ids' in self
+       assert 'person_id' in self
+       assert isinstance(value, list)
+       
+       (key_ids, blank, keys) = self.separate_types(value)
+       
+       if self['key_ids'] != key_ids:
+           from PLC.Methods.DeleteKey import DeleteKey
+           stale_keys = set(self['key_ids']).difference(key_ids)
+       
+           for stale_key in stale_keys:
+               DeleteKey.__call__(DeleteKey(self.api), auth, stale_key) 
+
+       if keys:
+           from PLC.Methods.AddPersonKey import AddPersonKey
+           from PLC.Methods.UpdateKey import UpdateKey         
+           updated_keys = filter(lambda key: 'key_id' in key, keys)
+           added_keys = filter(lambda key: 'key_id' not in key, keys)
+               
+           for key in added_keys:
+               AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
+           for key in updated_keys:
+               key_id = key.pop('key_id')
+               UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
+                 
+       
+    def associate_slices(self, auth, field, value):
+        """
+        Adds person to slices found in value list (using AddPersonToSlice).
+        Deletes person from slices found in value list (using DeletePersonFromSlice).
+        """
+
+       from PLC.Slices import Slices
+
+        assert 'slice_ids' in self
+        assert 'person_id' in self
+        assert isinstance(value, list)
+
+        (slice_ids, slice_names) = self.separate_types(value)[0:2]
+
+        # Translate roles into role_ids
+        if slice_names:
+            slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
+            slice_ids += slices.keys()
+
+        # Add new ids, remove stale ids
+        if self['slice_ids'] != slice_ids:
+            from PLC.Methods.AddPersonToSlice import AddPersonToSlice
+            from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
+            new_slices = set(slice_ids).difference(self['slice_ids'])
+            stale_slices = set(self['slice_ids']).difference(slice_ids)
+
+            for new_slice in new_slices:
+                AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
+            for stale_slice in stale_slices:
+                DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
+    
+
+    def delete(self, commit = True):
+        """
+        Delete existing user.
+        """
+
+        # Delete all keys
+        keys = Keys(self.api, self['key_ids'])
+        for key in keys:
+            key.delete(commit = False)
+
+        # Clean up miscellaneous join tables
+        for table in self.join_tables:
+            self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
+                           (table, self['person_id']))
+
+        # Mark as deleted
+        self['deleted'] = True
+        self.sync(commit)
+
+class Persons(Table):
+    """
+    Representation of row(s) from the persons table in the
+    database.
+    """
+
+    def __init__(self, api, person_filter = None, columns = None):
+        Table.__init__(self, api, Person, columns)
+        #sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
+        #      ", ".join(self.columns)
+       foreign_fields = {'role_ids': ('role_id', 'person_role'),
+                         'roles': ('name', 'roles'),
+                          'site_ids': ('site_id', 'person_site'),
+                          'key_ids': ('key_id', 'person_key'),
+                          'slice_ids': ('slice_id', 'slice_person')
+                          }
+       foreign_keys = {}
+       db_fields = filter(lambda field: field not in foreign_fields.keys(), Person.fields.keys())
+       all_fields = db_fields + [value[0] for value in foreign_fields.values()]
+       fields = []
+       _select = "SELECT "
+       _from = " FROM persons "
+       _join = " LEFT JOIN peer_person USING (person_id) "  
+       _where = " WHERE deleted IS False "
+
+       if not columns:
+           # include all columns       
+           fields = all_fields
+           tables = [value[1] for value in foreign_fields.values()]
+           tables.sort()
+           for key in foreign_fields.keys():
+               foreign_keys[foreign_fields[key][0]] = key  
+           for table in tables:
+               if table in ['roles']:
+                   _join += " LEFT JOIN roles USING(role_id) "
+               else:   
+                   _join += " LEFT JOIN %s USING (person_id) " % (table)
+       else: 
+           tables = set()
+           columns = filter(lambda column: column in db_fields+foreign_fields.keys(), columns)
+           columns.sort()
+           for column in columns: 
+               if column in foreign_fields.keys():
+                   (field, table) = foreign_fields[column]
+                   foreign_keys[field] = column
+                   fields += [field]
+                   tables.add(table)
+                   if column in ['roles']:
+                       _join += " LEFT JOIN roles USING(role_id) "
+                   else:
+                       _join += " LEFT JOIN %s USING (person_id)" % \
+                               (foreign_fields[column][1])
+               
+               else:
+                   fields += [column]  
+       
+       # postgres will return timestamps as datetime objects. 
+       # XMLPRC cannot marshal datetime so convert to int
+       timestamps = ['date_created', 'last_updated', 'verification_expires']
+       for field in fields:
+           if field in timestamps:
+               fields[fields.index(field)] = \
+                "CAST(date_part('epoch', %s) AS bigint) AS %s" % (field, field)
+
+       _select += ", ".join(fields)
+       sql = _select + _from + _join + _where
+
+       # deal with filter                      
+        if person_filter is not None:
+            if isinstance(person_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
+                person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
+                sql += " AND (%s) %s" % person_filter.sql(api, "OR")
+            elif isinstance(person_filter, dict):
+                person_filter = Filter(Person.fields, person_filter)
+                sql += " AND (%s) %s" % person_filter.sql(api, "AND")
+            elif isinstance (person_filter, StringTypes):
+                person_filter = Filter(Person.fields, {'email':[person_filter]})
+                sql += " AND (%s) %s" % person_filter.sql(api, "AND")
+            elif isinstance (person_filter, int):
+                person_filter = Filter(Person.fields, {'person_id':[person_filter]})
+                sql += " AND (%s) %s" % person_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
+
+       # aggregate data
+       all_persons = {}
+       for row in self.api.db.selectall(sql):
+           person_id = row['person_id']
+
+           if all_persons.has_key(person_id):
+               for (key, key_list) in foreign_keys.items():
+                   data = row.pop(key)
+                   row[key_list] = [data]
+                   if data and data not in all_persons[person_id][key_list]:
+                       all_persons[person_id][key_list].append(data)
+            else:
+               for key in foreign_keys.keys():
+                    value = row.pop(key)
+                   if value:   
+                       row[foreign_keys[key]] = [value]
+                   else:
+                       row[foreign_keys[key]] = []
+               if row: 
+                   all_persons[person_id] = row
+               
+       # populate self
+       for row in all_persons.values():
+           obj = self.classobj(self.api, row)
+            self.append(obj)
+
diff --git a/PLC/PostgreSQL.py b/PLC/PostgreSQL.py
new file mode 100644 (file)
index 0000000..23baa99
--- /dev/null
@@ -0,0 +1,263 @@
+#
+# 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: PostgreSQL.py 10071 2008-07-31 18:10:11Z tmack $
+#
+
+import psycopg2
+import psycopg2.extensions
+psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
+# UNICODEARRAY not exported yet
+psycopg2.extensions.register_type(psycopg2._psycopg.UNICODEARRAY)
+
+import pgdb
+from types import StringTypes, NoneType
+import traceback
+import commands
+import re
+from pprint import pformat
+
+from PLC.Debug import profile, log
+from PLC.Faults import *
+
+if not psycopg2:
+    is8bit = re.compile("[\x80-\xff]").search
+
+    def unicast(typecast):
+        """
+        pgdb returns raw UTF-8 strings. This function casts strings that
+        appear to contain non-ASCII characters to unicode objects.
+        """
+    
+        def wrapper(*args, **kwds):
+            value = typecast(*args, **kwds)
+
+            # pgdb always encodes unicode objects as UTF-8 regardless of
+            # the DB encoding (and gives you no option for overriding
+            # the encoding), so always decode 8-bit objects as UTF-8.
+            if isinstance(value, str) and is8bit(value):
+                value = unicode(value, "utf-8")
+
+            return value
+
+        return wrapper
+
+    pgdb.pgdbTypeCache.typecast = unicast(pgdb.pgdbTypeCache.typecast)
+
+class PostgreSQL:
+    def __init__(self, api):
+        self.api = api
+        self.debug = False
+        self.connection = None
+
+    def cursor(self):
+        if self.connection is None:
+            # (Re)initialize database connection
+            if psycopg2:
+                try:
+                    # Try UNIX socket first
+                    self.connection = psycopg2.connect(user = self.api.config.PLC_DB_USER,
+                                                       password = self.api.config.PLC_DB_PASSWORD,
+                                                       database = self.api.config.PLC_DB_NAME)
+                except psycopg2.OperationalError:
+                    # Fall back on TCP
+                    self.connection = psycopg2.connect(user = self.api.config.PLC_DB_USER,
+                                                       password = self.api.config.PLC_DB_PASSWORD,
+                                                       database = self.api.config.PLC_DB_NAME,
+                                                       host = self.api.config.PLC_DB_HOST,
+                                                       port = self.api.config.PLC_DB_PORT)
+                self.connection.set_client_encoding("UNICODE")
+            else:
+                self.connection = pgdb.connect(user = self.api.config.PLC_DB_USER,
+                                               password = self.api.config.PLC_DB_PASSWORD,
+                                               host = "%s:%d" % (api.config.PLC_DB_HOST, api.config.PLC_DB_PORT),
+                                               database = self.api.config.PLC_DB_NAME)
+
+        (self.rowcount, self.description, self.lastrowid) = \
+                        (None, None, None)
+
+        return self.connection.cursor()
+
+    def close(self):
+        if self.connection is not None:
+            self.connection.close()
+            self.connection = None
+
+    def quote(self, value):
+        """
+        Returns quoted version of the specified value.
+        """
+
+        # The pgdb._quote function is good enough for general SQL
+        # quoting, except for array types.
+        if isinstance(value, (list, tuple, set)):
+            return "ARRAY[%s]" % ", ".join(map, self.quote, value)
+        else:
+            return pgdb._quote(value)
+
+    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.connection.commit()
+
+    def rollback(self):
+        self.connection.rollback()
+
+    def do(self, query, params = None):
+        cursor = self.execute(query, params)
+        cursor.close()
+        return self.rowcount
+
+    def next_id(self, table_name, primary_key):
+        sequence = "%(table_name)s_%(primary_key)s_seq" % locals()
+        sql = "SELECT nextval('%(sequence)s')" % locals()
+        rows = self.selectall(sql, hashref = False)
+        if rows:
+            return rows[0][0]
+
+        return None
+
+    def last_insert_id(self, table_name, primary_key):
+        if isinstance(self.lastrowid, int):
+            sql = "SELECT %s FROM %s WHERE oid = %d" % \
+                  (primary_key, table_name, self.lastrowid)
+            rows = self.selectall(sql, hashref = False)
+            if rows:
+                return rows[0][0]
+
+        return None
+
+    # modified for psycopg2-2.0.7 
+    # executemany is undefined for SELECT's
+    # see http://www.python.org/dev/peps/pep-0249/
+    # accepts either None, a single dict, a tuple of single dict - in which case it execute's
+    # or a tuple of several dicts, in which case it executemany's
+    def execute(self, query, params = None):
+
+        cursor = self.cursor()
+        try:
+
+            # psycopg2 requires %()s format for all parameters,
+            # regardless of type.
+            if psycopg2:
+                query = re.sub(r'(%\([^)]*\)|%)[df]', r'\1s', query)
+
+            if not params:
+                if self.debug:
+                    print >> log,'execute0',query
+                cursor.execute(query)
+            elif isinstance(params,dict):
+                if self.debug:
+                    print >> log,'execute-dict: params',params,'query',query%params
+                cursor.execute(query,params)
+            elif isinstance(params,tuple) and len(params)==1:
+                if self.debug:
+                    print >> log,'execute-tuple',query%params[0]
+                cursor.execute(query,params[0])
+            else:
+                param_seq=(params,)
+                if self.debug:
+                    for params in param_seq:
+                        print >> log,'executemany',query%params
+                cursor.executemany(query, param_seq)
+            (self.rowcount, self.description, self.lastrowid) = \
+                            (cursor.rowcount, cursor.description, cursor.lastrowid)
+        except Exception, e:
+            try:
+                self.rollback()
+            except:
+                pass
+            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(params)
+            raise PLCDBError("Please contact " + \
+                             self.api.config.PLC_NAME + " Support " + \
+                             "<" + self.api.config.PLC_MAIL_SUPPORT_ADDRESS + ">" + \
+                             " and reference " + uuid)
+
+        return cursor
+
+    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.
+        """
+
+        cursor = self.execute(query, params)
+        rows = cursor.fetchall()
+        cursor.close()
+
+        if hashref or key_field is not None:
+            # 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
+
+    def fields(self, table, notnull = None, hasdef = None):
+        """
+        Return the names of the fields of the specified table.
+        """
+
+        if hasattr(self, 'fields_cache'):
+            if self.fields_cache.has_key((table, notnull, hasdef)):
+                return self.fields_cache[(table, notnull, hasdef)]
+        else:
+            self.fields_cache = {}
+
+        sql = "SELECT attname FROM pg_attribute, pg_class" \
+              " WHERE pg_class.oid = attrelid" \
+              " AND attnum > 0 AND relname = %(table)s"
+
+        if notnull is not None:
+            sql += " AND attnotnull is %(notnull)s"
+
+        if hasdef is not None:
+            sql += " AND atthasdef is %(hasdef)s"
+
+        rows = self.selectall(sql, locals(), hashref = False)
+
+        self.fields_cache[(table, notnull, hasdef)] = [row[0] for row in rows]
+
+        return self.fields_cache[(table, notnull, hasdef)]
diff --git a/PLC/PyCurl.py b/PLC/PyCurl.py
new file mode 100644 (file)
index 0000000..548e290
--- /dev/null
@@ -0,0 +1,82 @@
+#
+# Replacement for xmlrpclib.SafeTransport, which does not validate
+# SSL certificates. Requires PyCurl.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: PyCurl.py 7535 2007-12-17 15:31:38Z thierry $
+#
+
+import os
+import xmlrpclib
+import pycurl
+from tempfile import NamedTemporaryFile
+
+class PyCurlTransport(xmlrpclib.Transport):
+    def __init__(self, uri, cert = None, timeout = 300):
+        xmlrpclib.Transport.__init__(self)
+        self.curl = pycurl.Curl()
+
+        # Suppress signals
+        self.curl.setopt(pycurl.NOSIGNAL, 1)
+
+        # Follow redirections
+        self.curl.setopt(pycurl.FOLLOWLOCATION, 1)
+
+        # Set URL
+        self.url = uri
+        self.curl.setopt(pycurl.URL, str(uri))
+
+        # Set certificate path
+        if cert is not None:
+            if os.path.exists(cert):
+                cert_path = str(cert)
+            else:
+                # Keep a reference so that it does not get deleted
+                self.cert = NamedTemporaryFile(prefix = "cert")
+                self.cert.write(cert)
+                self.cert.flush()
+                cert_path = self.cert.name
+            self.curl.setopt(pycurl.CAINFO, cert_path)
+            self.curl.setopt(pycurl.SSL_VERIFYPEER, 2)
+
+        # Set connection timeout
+        if timeout:
+            self.curl.setopt(pycurl.CONNECTTIMEOUT, timeout)
+            self.curl.setopt(pycurl.TIMEOUT, timeout)
+
+        # Set request callback
+        self.body = ""
+        def body(buf):
+            self.body += buf
+        self.curl.setopt(pycurl.WRITEFUNCTION, body)        
+
+    def request(self, host, handler, request_body, verbose = 1):
+        # Set verbosity
+        self.curl.setopt(pycurl.VERBOSE, verbose)
+
+        # Post request
+        self.curl.setopt(pycurl.POST, 1)
+        self.curl.setopt(pycurl.POSTFIELDS, request_body)
+
+        try:
+            self.curl.perform()
+            errcode = self.curl.getinfo(pycurl.HTTP_CODE)
+            response = self.body
+            self.body = ""
+            errmsg="<no known errmsg>"
+        except pycurl.error, err:
+            (errcode, errmsg) = err
+
+        if errcode == 60:
+            raise Exception, "PyCurl: SSL certificate validation failed"
+        elif errcode != 200:
+           raise Exception, "PyCurl: HTTP error %d -- %r" % (errcode,errmsg)
+
+        # Parse response
+        p, u = self.getparser()
+        p.feed(response)
+        p.close()
+
+        return u.close()
diff --git a/PLC/Roles.py b/PLC/Roles.py
new file mode 100644 (file)
index 0000000..53d68d0
--- /dev/null
@@ -0,0 +1,72 @@
+#
+# 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: Roles.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from types import StringTypes
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+
+class Role(Row):
+    """
+    Representation of a row in the roles table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'roles'
+    primary_key = 'role_id'
+    join_tables = ['person_role', ('slice_attribute_types', 'min_role_id')]
+    fields = {
+        'role_id': Parameter(int, "Role identifier"),
+        'name': Parameter(str, "Role", max = 100),
+        }
+
+    def validate_role_id(self, role_id):
+       # Make sure role does not already exist
+       conflicts = Roles(self.api, [role_id])
+        if conflicts:
+            raise PLCInvalidArgument, "Role ID already in use"
+
+        return role_id
+
+    def validate_name(self, name):
+       # Make sure name is not blank
+        if not len(name):
+            raise PLCInvalidArgument, "Role must be specified"
+       
+       # Make sure role does not already exist
+       conflicts = Roles(self.api, [name])
+        if conflicts:
+            raise PLCInvalidArgument, "Role name already in use"
+
+       return name
+
+class Roles(Table):
+    """
+    Representation of the roles table in the database.
+    """
+
+    def __init__(self, api, role_filter = None):
+        Table.__init__(self, api, Role)
+
+        sql = "SELECT %s FROM roles WHERE True" % \
+              ", ".join(Role.fields)
+        
+        if role_filter is not None:
+            if isinstance(role_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), role_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), role_filter)
+                role_filter = Filter(Role.fields, {'role_id': ints, 'name': strs})
+                sql += " AND (%s) %s" % role_filter.sql(api, "OR")
+            elif isinstance(role_filter, dict):
+                role_filter = Filter(Role.fields, role_filter)
+                sql += " AND (%s) %s" % role_filter.sql(api, "AND")
+
+        self.selectall(sql)
diff --git a/PLC/Sessions.py b/PLC/Sessions.py
new file mode 100644 (file)
index 0000000..e0a57b3
--- /dev/null
@@ -0,0 +1,91 @@
+from types import StringTypes
+import random
+import base64
+import time
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.Persons import Person, Persons
+from PLC.Nodes import Node, Nodes
+
+class Session(Row):
+    """
+    Representation of a row in the sessions table. To use, instantiate
+    with a dict of values.
+    """
+
+    table_name = 'sessions'
+    primary_key = 'session_id'
+    join_tables = ['person_session', 'node_session']
+    fields = {
+        'session_id': Parameter(str, "Session key"),
+        'person_id': Parameter(int, "Account identifier, if applicable"),
+        'node_id': Parameter(int, "Node identifier, if applicable"),
+        'expires': Parameter(int, "Date and time when session expires, in seconds since UNIX epoch"),
+        }
+
+    def validate_expires(self, expires):
+        if expires < time.time():
+            raise PLCInvalidArgument, "Expiration date must be in the future"
+
+        return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(expires))
+
+    add_person = Row.add_object(Person, 'person_session')
+
+    def add_node(self, node, commit = True):
+        # Nodes can have only one session at a time
+        self.api.db.do("DELETE FROM node_session WHERE node_id = %d" % \
+                       node['node_id'])
+
+        add = Row.add_object(Node, 'node_session')
+        add(self, node, commit = commit)
+
+    def sync(self, commit = True, insert = None):
+        if not self.has_key('session_id'):
+            # Before a new session is added, delete expired sessions
+            expired = Sessions(self.api, expires = -int(time.time()))
+            for session in expired:
+                session.delete(commit)
+
+            # Generate 32 random bytes
+            bytes = random.sample(xrange(0, 256), 32)
+            # Base64 encode their string representation
+            self['session_id'] = base64.b64encode("".join(map(chr, bytes)))
+            # Force insert
+            insert = True
+
+        Row.sync(self, commit, insert)
+
+class Sessions(Table):
+    """
+    Representation of row(s) from the session table in the database.
+    """
+
+    def __init__(self, api, session_filter = None, expires = int(time.time())):
+       Table.__init__(self, api, Session)
+
+        sql = "SELECT %s FROM view_sessions WHERE True" % \
+              ", ".join(Session.fields)
+
+       if session_filter is not None:
+           if isinstance(session_filter, (list, tuple, set)):
+               # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), session_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), session_filter)
+                session_filter = Filter(Session.fields, {'person_id': ints, 'session_id': strs})
+                sql += " AND (%s) %s" % session_filter.sql(api, "OR")
+           elif isinstance(session_filter, dict):
+               session_filter = Filter(Session.fields, session_filter)
+               sql += " AND (%s) %s" % session_filter.sql(api, "AND")
+
+        if expires is not None:
+            if expires >= 0:
+                sql += " AND expires > %(expires)d"
+            else:
+                expires = -expires
+                sql += " AND expires < %(expires)d"
+
+        self.selectall(sql, locals())
diff --git a/PLC/Shell.py b/PLC/Shell.py
new file mode 100644 (file)
index 0000000..43317a0
--- /dev/null
@@ -0,0 +1,260 @@
+#!/usr/bin/python
+#
+# Interactive shell for testing PLCAPI
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2005 The Trustees of Princeton University
+#
+# $Id: Shell.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+import os
+import pydoc
+import xmlrpclib
+
+from PLC.API import PLCAPI
+from PLC.Parameter import Mixed
+from PLC.Auth import Auth
+from PLC.Config import Config
+from PLC.Method import Method
+from PLC.PyCurl import PyCurlTransport
+import PLC.Methods
+
+class Callable:
+    """
+    Wrapper to call a method either directly or remotely and
+    automagically add the authentication structure if necessary.
+    """
+
+    def __init__(self, shell, name, func, auth = None):
+        self.shell = shell
+        self.name = name
+        self.func = func
+        self.auth = auth
+
+    def __call__(self, *args, **kwds):
+        """
+        Automagically add the authentication structure if the function
+        requires it and it has not been specified.
+        """
+
+        if self.auth and \
+           (not args or not isinstance(args[0], dict) or \
+            (not args[0].has_key('AuthMethod') and \
+             not args[0].has_key('session'))):
+            args = (self.auth,) + args
+
+        if self.shell.multi:
+            self.shell.calls.append({'methodName': self.name, 'params': list(args)})
+            return None
+        else:
+            return self.func(*args, **kwds)
+
+class Shell:
+    def __init__(self,
+                 # Add API functions to global scope
+                 globals = None,
+                 # Configuration file
+                 config = None,
+                 # XML-RPC server
+                 url = None, xmlrpc = False, cacert = None,
+                 # API authentication method
+                 method = None,
+                 # Password authentication
+                 role = None, user = None, password = None,
+                 # Session authentication
+                 session = None):
+        """
+        Initialize a new shell instance. Re-initializes globals.
+        """
+
+        try:
+            # If any XML-RPC options have been specified, do not try
+            # connecting directly to the DB.
+            if (url, method, user, password, role, cacert, xmlrpc) != \
+                   (None, None, None, None, None, None, False):
+                raise Exception
+
+            # Otherwise, first try connecting directly to the DB. This
+            # absolutely requires a configuration file; the API
+            # instance looks for one in a default location if one is
+            # not specified. If this fails, try connecting to the API
+            # server via XML-RPC.
+            if config is None:
+                self.api = PLCAPI()
+            else:
+                self.api = PLCAPI(config)
+            self.config = self.api.config
+            self.url = None
+            self.server = None
+        except Exception, err:
+            # Try connecting to the API server via XML-RPC
+            self.api = PLCAPI(None)
+
+            try:
+                if config is None:
+                    self.config = Config()
+                else:
+                    self.config = Config(config)
+            except Exception, err:
+                # Try to continue if no configuration file is available
+                self.config = None
+
+            if url is None:
+                if self.config is None:
+                    raise Exception, "Must specify API URL"
+
+                url = "https://" + self.config.PLC_API_HOST + \
+                      ":" + str(self.config.PLC_API_PORT) + \
+                      "/" + self.config.PLC_API_PATH + "/"
+
+                if cacert is None:
+                    cacert = self.config.PLC_API_CA_SSL_CRT
+
+            self.url = url
+            if cacert is not None:
+                self.server = xmlrpclib.ServerProxy(url, PyCurlTransport(url, cacert), allow_none = 1)
+            else:
+                self.server = xmlrpclib.ServerProxy(url, allow_none = 1)
+
+        # Set up authentication structure
+
+        # Default is to use session or capability authentication
+        if (method, user, password) == (None, None, None):
+            if session is not None or os.path.exists("/etc/planetlab/session"):
+                method = "session"
+                if session is None:
+                    session = "/etc/planetlab/session"
+            else:
+                method = "capability"
+
+        if method == "capability":
+            # Load defaults from configuration file if using capability
+            # authentication.
+            if user is None and self.config is not None:
+                user = self.config.PLC_API_MAINTENANCE_USER
+            if password is None and self.config is not None:
+                password = self.config.PLC_API_MAINTENANCE_PASSWORD
+            if role is None:
+                role = "admin"
+        elif method is None:
+            # Otherwise, default to password authentication
+            method = "password"
+
+        if role == "anonymous" or method == "anonymous":
+            self.auth = {'AuthMethod': "anonymous"}
+        elif method == "session":
+            if session is None:
+                raise Exception, "Must specify session"
+
+            if os.path.exists(session):
+                session = file(session).read()
+
+            self.auth = {'AuthMethod': "session", 'session': session}
+        else:
+            if user is None:
+                raise Exception, "Must specify username"
+
+            if password is None:
+                raise Exception, "Must specify password"
+
+            self.auth = {'AuthMethod': method,
+                         'Username': user,
+                         'AuthString': password}
+
+            if role is not None:
+                self.auth['Role'] = role
+
+        for method in PLC.Methods.methods:
+            api_function = self.api.callable(method)
+
+            if self.server is None:
+                # Can just call it directly
+                func = api_function
+            else:
+                func = getattr(self.server, method)
+
+            # If the function requires an authentication structure as
+            # its first argument, automagically add an auth struct to
+            # the call.
+            if api_function.accepts and \
+               (isinstance(api_function.accepts[0], Auth) or \
+                (isinstance(api_function.accepts[0], Mixed) and \
+                 filter(lambda param: isinstance(param, Auth), api_function.accepts[0]))):
+                auth = self.auth
+            else:
+                auth = None
+
+            callable = Callable(self, method, func, auth)
+
+            # Add to ourself and the global environment. Add dummy
+            # subattributes to support tab completion of methods with
+            # dots in their names (e.g., system.listMethods).
+            class Dummy: pass
+            paths = method.split(".")
+            if len(paths) > 1:
+                first = paths.pop(0)
+
+                if not hasattr(self, first):
+                    obj = Dummy()
+                    setattr(self, first, obj)
+                    # Also add to global environment if specified
+                    if globals is not None:
+                        globals[first] = obj
+
+                obj = getattr(self, first)
+
+                for path in paths:
+                    if not hasattr(obj, path):
+                        if path == paths[-1]:
+                            setattr(obj, path, callable)
+                        else:
+                            setattr(obj, path, Dummy())
+                    obj = getattr(obj, path)
+            else:
+                setattr(self, method, callable)
+                # Also add to global environment if specified
+                if globals is not None:
+                    globals[method] = callable
+
+        # Override help(), begin(), and commit()
+        if globals is not None:
+            globals['help'] = self.help
+            globals['begin'] = self.begin
+            globals['commit'] = self.commit
+
+        # Multicall support
+        self.calls = []
+        self.multi = False
+
+    def help(self, topic = None):
+        if isinstance(topic, Callable):
+            pydoc.pager(self.system.methodHelp(topic.name))
+        else:
+            pydoc.help(topic)
+
+    def begin(self):
+        if self.calls:
+            raise Exception, "multicall already in progress"
+
+        self.multi = True
+
+    def commit(self):
+        if self.calls:
+            ret = []
+            self.multi = False
+            results = self.system.multicall(self.calls)
+            for result in results:
+                if type(result) == type({}):
+                    raise xmlrpclib.Fault(result['faultCode'], result['faultString'])
+                elif type(result) == type([]):
+                    ret.append(result[0])
+                else:
+                    raise ValueError, "unexpected type in multicall result"
+        else:
+            ret = None
+
+        self.calls = []
+        self.multi = False
+
+        return ret
diff --git a/PLC/Sites.py b/PLC/Sites.py
new file mode 100644 (file)
index 0000000..6035bb7
--- /dev/null
@@ -0,0 +1,271 @@
+from types import StringTypes
+import string
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.Slices import Slice, Slices
+from PLC.PCUs import PCU, PCUs
+from PLC.Nodes import Node, Nodes
+from PLC.Addresses import Address, Addresses
+from PLC.Persons import Person, Persons
+
+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 sync().
+    """
+
+    table_name = 'sites'
+    primary_key = 'site_id'
+    join_tables = ['person_site', 'site_address', 'peer_site']
+    fields = {
+        'site_id': Parameter(int, "Site identifier"),
+        'name': Parameter(str, "Full site name", max = 254),
+        'abbreviated_name': Parameter(str, "Abbreviated site name", max = 50),
+        'login_base': Parameter(str, "Site slice prefix", max = 20),
+        'is_public': Parameter(bool, "Publicly viewable site"),
+        'enabled': Parameter(bool, "Has been enabled"),
+       'latitude': Parameter(float, "Decimal latitude of the site", min = -90.0, max = 90.0, nullok = True),
+        'longitude': Parameter(float, "Decimal longitude of the site", min = -180.0, max = 180.0, nullok = True),
+        'url': Parameter(str, "URL of a page that describes the site", max = 254, nullok = True),
+        'date_created': Parameter(int, "Date and time when site entry was created, in seconds since UNIX epoch", ro = True),
+        'last_updated': Parameter(int, "Date and time when site entry was last updated, in seconds since UNIX epoch", ro = True),
+        'max_slices': Parameter(int, "Maximum number of slices that the site is able to create"),
+        'max_slivers': Parameter(int, "Maximum number of slivers that the site is able to create"),
+        'person_ids': Parameter([int], "List of account identifiers"),
+        'slice_ids': Parameter([int], "List of slice identifiers"),
+        'address_ids': Parameter([int], "List of address identifiers"),
+        'pcu_ids': Parameter([int], "List of PCU identifiers"),
+        'node_ids': Parameter([int], "List of site node identifiers"),
+        'peer_id': Parameter(int, "Peer to which this site belongs", nullok = True),
+        'peer_site_id': Parameter(int, "Foreign site identifier at peer", nullok = True),
+       'ext_consortium_id': Parameter(int, "external consortium id", nullok = True)
+        }
+    related_fields = {
+       'persons': [Mixed(Parameter(int, "Person identifier"),
+                          Parameter(str, "Email address"))],
+       'addresses': [Mixed(Parameter(int, "Address identifer"),
+                            Filter(Address.fields))]
+       }
+    # for Cache
+    class_key = 'login_base'
+    foreign_fields = ['abbreviated_name', 'name', 'is_public', 'latitude', 'longitude',
+                     'url', 'max_slices', 'max_slivers',
+                     ]
+    # forget about these ones, they are read-only anyway
+    # handling them causes Cache to re-sync all over again 
+    # 'last_updated', 'date_created'
+    foreign_xrefs = []
+
+    def validate_name(self, name):
+        if not len(name):
+            raise PLCInvalidArgument, "Name must be specified"
+
+        return name
+
+    validate_abbreviated_name = validate_name
+
+    def validate_login_base(self, login_base):
+        if not len(login_base):
+            raise PLCInvalidArgument, "Login base must be specified"
+
+        if not set(login_base).issubset(string.lowercase + string.digits):
+            raise PLCInvalidArgument, "Login base must consist only of lowercase ASCII letters or numbers"
+
+        conflicts = Sites(self.api, [login_base])
+        for site in conflicts:
+            if 'site_id' not in self or self['site_id'] != site['site_id']:
+                raise PLCInvalidArgument, "login_base already in use"
+
+        return login_base
+
+    def validate_latitude(self, latitude):
+        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 not self.has_key('latitude') or \
+           self['latitude'] is None:
+            raise PLCInvalidArgument, "Latitude must also be specified"
+
+        return longitude
+
+    validate_date_created = Row.validate_timestamp
+    validate_last_updated = Row.validate_timestamp
+
+    add_person = Row.add_object(Person, 'person_site')
+    remove_person = Row.remove_object(Person, 'person_site')
+
+    add_address = Row.add_object(Address, 'site_address')
+    remove_address = Row.remove_object(Address, 'site_address')
+
+    def update_last_updated(self, commit = True):
+        """
+        Update last_updated field with current time
+        """
+
+        assert 'site_id' in self
+        assert self.table_name
+
+        self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
+                       " where site_id = %d" % (self['site_id']) )
+        self.sync(commit)    
+
+
+    def associate_persons(self, auth, field, value):
+       """
+       Adds persons found in value list to this site (using AddPersonToSite).
+       Deletes persons not found in value list from this site (using DeletePersonFromSite).
+       """
+       
+       assert 'person_ids' in self
+       assert 'site_id' in self
+       assert isinstance(value, list)
+
+       (person_ids, emails) = self.separate_types(value)[0:2]
+
+       # Translate emails into person_ids
+       if emails:
+           persons = Persons(self.api, emails, ['person_id']).dict('person_id')
+           person_ids += persons.keys()
+
+       # Add new ids, remove stale ids
+       if self['person_ids'] != person_ids:
+           from PLC.Methods.AddPersonToSite import AddPersonToSite
+           from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
+           new_persons = set(person_ids).difference(self['person_ids'])
+           stale_persons = set(self['person_ids']).difference(person_ids)
+        
+           for new_person in new_persons:
+               AddPersonToSite.__call__(AddPersonToSite(self.api), auth, new_person, self['site_id'])
+           for stale_person in stale_persons:
+               DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, stale_person, self['site_id'])              
+
+    def associate_addresses(self, auth, field, value):
+       """
+       Deletes addresses_ids not found in value list (using DeleteAddress).  
+       Adds address if slice_fields w/o address_id found in value list (using AddSiteAddress).
+       Update address if slice_fields w/ address_id found in value list (using UpdateAddress).
+       """
+       
+       assert 'address_ids' in self
+       assert 'site_id' in self
+        assert isinstance(value, list)
+
+        (address_ids, blank, addresses) = self.separate_types(value)
+
+       for address in addresses:
+           if 'address_id' in address:
+               address_ids.append(address['address_id'])               
+
+        # Add new ids, remove stale ids
+        if self['address_ids'] != address_ids:
+            from PLC.Methods.DeleteAddress import DeleteAddress
+            stale_addresses = set(self['address_ids']).difference(address_ids)
+
+            for stale_address in stale_addresses:
+                DeleteAddress.__call__(DeleteAddress(self.api), auth, stale_address)   
+       
+       if addresses:
+           from PLC.Methods.AddSiteAddress import AddSiteAddress
+           from PLC.Methods.UpdateAddress import UpdateAddress
+               
+           updated_addresses = filter(lambda address: 'address_id' in address, addresses)
+           added_addresses = filter(lambda address: 'address_id' not in address, addresses)
+               
+           for address in added_addresses:
+               AddSiteAddress.__call__(AddSiteAddress(self.api), auth, self['site_id'], address)       
+           for address in updated_addresses:
+               address_id = address.pop('address_id')
+               UpdateAddress.__call__(UpdateAddress(self.api), auth, address_id, address)
+
+    def delete(self, commit = True):
+        """
+        Delete existing site.
+        """
+
+        assert 'site_id' in self
+
+        # 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 in persons:
+            delete = True
+
+            person_sites = Sites(self.api, person['site_ids'])
+            for person_site in person_sites:
+                if person_site['site_id'] != self['site_id']:
+                    delete = False
+                    break
+
+            if delete:
+                person.delete(commit = False)
+
+        # Delete all site addresses
+        addresses = Addresses(self.api, self['address_ids'])
+        for address in addresses:
+            address.delete(commit = False)
+
+        # Delete all site slices
+        slices = Slices(self.api, self['slice_ids'])
+        for slice in slices:
+            slice.delete(commit = False)
+
+        # Delete all site PCUs
+        pcus = PCUs(self.api, self['pcu_ids'])
+        for pcu in pcus:
+            pcu.delete(commit = False)
+
+        # Delete all site nodes
+        nodes = Nodes(self.api, self['node_ids'])
+        for node in nodes:
+            node.delete(commit = False)
+
+        # Clean up miscellaneous join tables
+        for table in self.join_tables:
+            self.api.db.do("DELETE FROM %s WHERE site_id = %d" % \
+                           (table, self['site_id']))
+
+        # Mark as deleted
+        self['deleted'] = True
+        self.sync(commit)
+
+class Sites(Table):
+    """
+    Representation of row(s) from the sites table in the
+    database.
+    """
+
+    def __init__(self, api, site_filter = None, columns = None):
+        Table.__init__(self, api, Site, columns)
+
+        sql = "SELECT %s FROM view_sites WHERE deleted IS False" % \
+              ", ".join(self.columns)
+
+        if site_filter is not None:
+            if isinstance(site_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), site_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), site_filter)
+                site_filter = Filter(Site.fields, {'site_id': ints, 'login_base': strs})
+                sql += " AND (%s) %s" % site_filter.sql(api, "OR")
+            elif isinstance(site_filter, dict):
+                site_filter = Filter(Site.fields, site_filter)
+                sql += " AND (%s) %s" % site_filter.sql(api, "AND")
+            elif isinstance (site_filter, StringTypes):
+                site_filter = Filter(Site.fields, {'login_base':[site_filter]})
+                sql += " AND (%s) %s" % site_filter.sql(api, "AND")
+            elif isinstance (site_filter, int):
+                site_filter = Filter(Site.fields, {'site_id':[site_filter]})
+                sql += " AND (%s) %s" % site_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong site filter %r"%site_filter
+
+        self.selectall(sql)
diff --git a/PLC/SliceAttributeTypes.py b/PLC/SliceAttributeTypes.py
new file mode 100644 (file)
index 0000000..5884fe0
--- /dev/null
@@ -0,0 +1,72 @@
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+from PLC.Roles import Role, Roles
+
+class SliceAttributeType(Row):
+    """
+    Representation of a row in the slice_attribute_types table. To
+    use, instantiate with a dict of values.
+    """
+
+    table_name = 'slice_attribute_types'
+    primary_key = 'attribute_type_id'
+    join_tables = ['slice_attribute']
+    fields = {
+        'attribute_type_id': Parameter(int, "Slice attribute type identifier"),
+        'name': Parameter(str, "Slice attribute type name", max = 100),
+        'description': Parameter(str, "Slice attribute type description", max = 254),
+        'min_role_id': Parameter(int, "Minimum (least powerful) role that can set or change this attribute"),
+        }
+
+    # for Cache
+    class_key = 'name'
+    foreign_fields = ['description','min_role_id']
+    foreign_xrefs = []
+
+    def validate_name(self, name):
+        if not len(name):
+            raise PLCInvalidArgument, "Slice attribute type name must be set"
+
+        conflicts = SliceAttributeTypes(self.api, [name])
+        for attribute in conflicts:
+            if 'attribute_type_id' not in self or \
+               self['attribute_type_id'] != attribute['attribute_type_id']:
+                raise PLCInvalidArgument, "Slice attribute type name already in use"
+
+        return name
+
+    def validate_min_role_id(self, role_id):
+        roles = [row['role_id'] for row in Roles(self.api)]
+        if role_id not in roles:
+            raise PLCInvalidArgument, "Invalid role"
+
+        return role_id
+
+class SliceAttributeTypes(Table):
+    """
+    Representation of row(s) from the slice_attribute_types table in the
+    database.
+    """
+
+    def __init__(self, api, attribute_type_filter = None, columns = None):
+        Table.__init__(self, api, SliceAttributeType, columns)
+
+        sql = "SELECT %s FROM slice_attribute_types WHERE True" % \
+              ", ".join(self.columns)
+
+        if attribute_type_filter is not None:
+            if isinstance(attribute_type_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), attribute_type_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), attribute_type_filter)
+                attribute_type_filter = Filter(SliceAttributeType.fields, {'attribute_type_id': ints, 'name': strs})
+                sql += " AND (%s) %s" % attribute_type_filter.sql(api, "OR")
+            elif isinstance(attribute_type_filter, dict):
+                attribute_type_filter = Filter(SliceAttributeType.fields, attribute_type_filter)
+                sql += " AND (%s) %s" % attribute_type_filter.sql(api, "AND")
+
+        self.selectall(sql)
diff --git a/PLC/SliceAttributes.py b/PLC/SliceAttributes.py
new file mode 100644 (file)
index 0000000..9f0b1fb
--- /dev/null
@@ -0,0 +1,46 @@
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+from PLC.SliceAttributeTypes import SliceAttributeType, SliceAttributeTypes
+
+class SliceAttribute(Row):
+    """
+    Representation of a row in the slice_attribute table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'slice_attribute'
+    primary_key = 'slice_attribute_id'
+    fields = {
+        'slice_attribute_id': Parameter(int, "Slice attribute identifier"),
+        'slice_id': Parameter(int, "Slice identifier"),
+        'node_id': Parameter(int, "Node identifier, if a sliver attribute"),
+       'nodegroup_id': Parameter(int, "Nodegroup identifier, if a sliver attribute"),
+        'attribute_type_id': SliceAttributeType.fields['attribute_type_id'],
+        'name': SliceAttributeType.fields['name'],
+        'description': SliceAttributeType.fields['description'],
+        'min_role_id': SliceAttributeType.fields['min_role_id'],
+        'value': Parameter(str, "Slice attribute value"),
+        }
+
+class SliceAttributes(Table):
+    """
+    Representation of row(s) from the slice_attribute table in the
+    database.
+    """
+
+    def __init__(self, api, slice_attribute_filter = None, columns = None):
+        Table.__init__(self, api, SliceAttribute, columns)
+
+        sql = "SELECT %s FROM view_slice_attributes WHERE True" % \
+              ", ".join(self.columns)
+
+        if slice_attribute_filter is not None:
+            if isinstance(slice_attribute_filter, (list, tuple, set)):
+                slice_attribute_filter = Filter(SliceAttribute.fields, {'slice_attribute_id': slice_attribute_filter})
+            elif isinstance(slice_attribute_filter, dict):
+                slice_attribute_filter = Filter(SliceAttribute.fields, slice_attribute_filter)
+            sql += " AND (%s) %s" % slice_attribute_filter.sql(api)
+
+        self.selectall(sql)
diff --git a/PLC/SliceInstantiations.py b/PLC/SliceInstantiations.py
new file mode 100644 (file)
index 0000000..db11838
--- /dev/null
@@ -0,0 +1,53 @@
+#
+# Functions for interacting with the slice_instantiations table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: SliceInstantiations.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+
+class SliceInstantiation(Row):
+    """
+    Representation of a row in the slice_instantiations table. To use,
+    instantiate with a dict of values.
+    """
+
+    table_name = 'slice_instantiations'
+    primary_key = 'instantiation'
+    join_tables = ['slices']
+    fields = {
+        'instantiation': Parameter(str, "Slice instantiation state", max = 100),
+        }
+
+    def validate_instantiation(self, instantiation):
+       # Make sure name is not blank
+        if not len(instantiation):
+            raise PLCInvalidArgument, "Slice instantiation state name must be specified"
+       
+       # Make sure slice instantiation does not alredy exist
+       conflicts = SliceInstantiations(self.api, [instantiation])
+        if conflicts:
+            raise PLCInvalidArgument, "Slice instantiation state name already in use"
+
+       return instantiation
+        
+class SliceInstantiations(Table):
+    """
+    Representation of the slice_instantiations table in the database.
+    """
+
+    def __init__(self, api, instantiations = None):
+        Table.__init__(self, api, SliceInstantiation)
+
+        sql = "SELECT %s FROM slice_instantiations" % \
+              ", ".join(SliceInstantiation.fields)
+        
+        if instantiations:
+            sql += " WHERE instantiation IN (%s)" % ", ".join(map(api.db.quote, instantiations))
+
+        self.selectall(sql)
diff --git a/PLC/Slices.py b/PLC/Slices.py
new file mode 100644 (file)
index 0000000..1a1786c
--- /dev/null
@@ -0,0 +1,296 @@
+from types import StringTypes
+import time
+import re
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Debug import profile
+from PLC.Table import Row, Table
+from PLC.SliceInstantiations import SliceInstantiation, SliceInstantiations
+from PLC.Nodes import Node
+from PLC.Persons import Person, Persons
+from PLC.SliceAttributes import SliceAttribute
+
+class Slice(Row):
+    """
+    Representation of a row in the slices table. To use, optionally
+    instantiate with a dict of values. Update as you would a
+    dict. Commit to the database with sync().To use, instantiate
+    with a dict of values.
+    """
+
+    table_name = 'slices'
+    primary_key = 'slice_id'
+    join_tables = ['slice_node', 'slice_person', 'slice_attribute', 'peer_slice', 'node_slice_whitelist']
+    fields = {
+        'slice_id': Parameter(int, "Slice identifier"),
+        'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
+        'name': Parameter(str, "Slice name", max = 32),
+        'instantiation': Parameter(str, "Slice instantiation state"),
+        'url': Parameter(str, "URL further describing this slice", max = 254, nullok = True),
+        'description': Parameter(str, "Slice description", max = 2048, nullok = True),
+        'max_nodes': Parameter(int, "Maximum number of nodes that can be assigned to this slice"),
+        'creator_person_id': Parameter(int, "Identifier of the account that created this slice"),
+        'created': Parameter(int, "Date and time when slice was created, in seconds since UNIX epoch", ro = True),
+        'expires': Parameter(int, "Date and time when slice expires, in seconds since UNIX epoch"),
+        'node_ids': Parameter([int], "List of nodes in this slice", ro = True),
+        'person_ids': Parameter([int], "List of accounts that can use this slice", ro = True),
+        'slice_attribute_ids': Parameter([int], "List of slice attributes", ro = True),
+        'peer_id': Parameter(int, "Peer to which this slice belongs", nullok = True),
+        'peer_slice_id': Parameter(int, "Foreign slice identifier at peer", nullok = True),
+        }
+    related_fields = {
+       'persons': [Mixed(Parameter(int, "Person identifier"),
+                         Parameter(str, "Email address"))],
+       'nodes': [Mixed(Parameter(int, "Node identifier"),
+                       Parameter(str, "Fully qualified hostname"))]
+       }
+    # for Cache
+    class_key = 'name'
+    foreign_fields = ['instantiation', 'url', 'description', 'max_nodes', 'expires']
+    foreign_xrefs = [
+        {'field': 'node_ids' ,         'class': 'Node',   'table': 'slice_node' },
+       {'field': 'person_ids',        'class': 'Person', 'table': 'slice_person'},
+       {'field': 'creator_person_id', 'class': 'Person', 'table': 'unused-on-direct-refs'},
+        {'field': 'site_id',           'class': 'Site',   'table': 'unused-on-direct-refs'},
+    ]
+    # forget about this one, it is read-only anyway
+    # handling it causes Cache to re-sync all over again 
+    # 'created'
+
+    def validate_name(self, name):
+        # N.B.: Responsibility of the caller to ensure that login_base
+        # portion of the slice name corresponds to a valid site, if
+        # desired.
+
+        # 1. Lowercase.
+        # 2. Begins with login_base (letters or numbers).
+        # 3. Then single underscore after login_base.
+        # 4. Then letters, numbers, or underscores.
+        good_name = r'^[a-z0-9]+_[a-zA-Z0-9_]+$'
+        if not name or \
+           not re.match(good_name, name):
+            raise PLCInvalidArgument, "Invalid slice name"
+
+        conflicts = Slices(self.api, [name])
+        for slice in conflicts:
+            if 'slice_id' not in self or self['slice_id'] != slice['slice_id']:
+                raise PLCInvalidArgument, "Slice name already in use, %s"%name
+
+        return name
+
+    def validate_instantiation(self, instantiation):
+        instantiations = [row['instantiation'] for row in SliceInstantiations(self.api)]
+        if instantiation not in instantiations:
+            raise PLCInvalidArgument, "No such instantiation state"
+
+        return instantiation
+
+    validate_created = Row.validate_timestamp
+
+    def validate_expires(self, expires):
+        # N.B.: Responsibility of the caller to ensure that expires is
+        # not too far into the future.
+        check_future = not ('is_deleted' in self and self['is_deleted'])
+        return Row.validate_timestamp(self, expires, check_future = check_future)
+
+    add_person = Row.add_object(Person, 'slice_person')
+    remove_person = Row.remove_object(Person, 'slice_person')
+
+    add_node = Row.add_object(Node, 'slice_node')
+    remove_node = Row.remove_object(Node, 'slice_node')
+
+    add_to_node_whitelist = Row.add_object(Node, 'node_slice_whitelist')
+    delete_from_node_whitelist = Row.remove_object(Node, 'node_slice_whitelist')
+
+    def associate_persons(self, auth, field, value):
+        """
+        Adds persons found in value list to this slice (using AddPersonToSlice).
+       Deletes persons not found in value list from this slice (using DeletePersonFromSlice).
+        """
+       
+       assert 'person_ids' in self
+       assert 'slice_id' in self
+        assert isinstance(value, list)
+
+       (person_ids, emails) = self.separate_types(value)[0:2]
+
+       # Translate emails into person_ids      
+       if emails:
+           persons = Persons(self.api, emails, ['person_id']).dict('person_id')
+           person_ids += persons.keys()
+       
+       # Add new ids, remove stale ids
+        if self['person_ids'] != person_ids:
+            from PLC.Methods.AddPersonToSlice import AddPersonToSlice
+            from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
+            new_persons = set(person_ids).difference(self['person_ids'])
+            stale_persons = set(self['person_ids']).difference(person_ids)
+
+            for new_person in new_persons:
+                AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, new_person, self['slice_id'])
+            for stale_person in stale_persons:
+                DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, stale_person, self['slice_id'])
+
+    def associate_nodes(self, auth, field, value):
+       """
+       Adds nodes found in value list to this slice (using AddSliceToNodes).
+       Deletes nodes not found in value list from this slice (using DeleteSliceFromNodes).
+       """
+
+        from PLC.Nodes import Nodes
+
+       assert 'node_ids' in self
+       assert 'slice_id' in self
+       assert isinstance(value, list)
+       
+       (node_ids, hostnames) = self.separate_types(value)[0:2]
+       
+       # Translate hostnames into node_ids
+       if hostnames:
+           nodes = Nodes(self.api, hostnames, ['node_id']).dict('node_id')
+           node_ids += nodes.keys()
+       
+       # Add new ids, remove stale ids
+       if self['node_ids'] != node_ids:
+           from PLC.Methods.AddSliceToNodes import AddSliceToNodes
+           from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
+           new_nodes = set(node_ids).difference(self['node_ids'])
+           stale_nodes = set(self['node_ids']).difference(node_ids)
+           
+           if new_nodes:
+               AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, self['slice_id'], list(new_nodes))
+           if stale_nodes:
+               DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, self['slice_id'], list(stale_nodes))                        
+    def associate_slice_attributes(self, auth, fields, value):
+       """
+       Deletes slice_attribute_ids not found in value list (using DeleteSliceAttribute). 
+       Adds slice_attributes if slice_fields w/o slice_id is found (using AddSliceAttribute).
+       Updates slice_attribute if slice_fields w/ slice_id is found (using UpdateSlceiAttribute).  
+       """
+       
+       assert 'slice_attribute_ids' in self
+       assert isinstance(value, list)
+
+       (attribute_ids, blank, attributes) = self.separate_types(value)
+       
+       # There is no way to add attributes by id. They are
+       # associated with a slice when they are created.
+       # So we are only looking to delete here 
+       if self['slice_attribute_ids'] != attribute_ids:
+           from PLC.Methods.DeleteSliceAttribute import DeleteSliceAttribute
+           stale_attributes = set(self['slice_attribute_ids']).difference(attribute_ids)
+       
+           for stale_attribute in stale_attributes:
+               DeleteSliceAttribute.__call__(DeleteSliceAttribute(self.api), auth, stale_attribute['slice_attribute_id'])              
+       
+       # If dictionary exists, we are either adding new
+        # attributes or updating existing ones.
+        if attributes:
+            from PLC.Methods.AddSliceAttribute import AddSliceAttribute
+            from PLC.Methods.UpdateSliceAttribute import UpdateSliceAttribute
+       
+           added_attributes = filter(lambda x: 'slice_attribute_id' not in x, attributes)
+           updated_attributes = filter(lambda x: 'slice_attribute_id' in x, attributes)
+
+           for added_attribute in added_attributes:
+               if 'attribute_type' in added_attribute:
+                   type = added_attribute['attribute_type']
+               elif 'attribute_type_id' in added_attribute:
+                   type = added_attribute['attribute_type_id']
+               else:
+                   raise PLCInvalidArgument, "Must specify attribute_type or attribute_type_id"
+
+               if 'value' in added_attribute:
+                   value = added_attribute['value']
+               else:
+                   raise PLCInvalidArgument, "Must specify a value"
+               
+               if 'node_id' in added_attribute:
+                   node_id = added_attribute['node_id']
+               else:
+                   node_id = None
+
+               if 'nodegroup_id' in added_attribute:
+                   nodegroup_id = added_attribute['nodegroup_id']
+               else:
+                   nodegroup_id = None 
+               AddSliceAttribute.__call__(AddSliceAttribute(self.api), auth, self['slice_id'], type, value, node_id, nodegroup_id)
+           for updated_attribute in updated_attributes:
+               attribute_id = updated_attribute.pop('slice_attribute_id')
+               if attribute_id not in self['slice_attribute_ids']:
+                   raise PLCInvalidArgument, "Attribute doesnt belong to this slice" 
+               else:
+                   UpdateSliceAttribute.__call__(UpdateSliceAttribute(self.api), auth, attribute_id, updated_attribute)                 
+       
+    def sync(self, commit = True):
+        """
+        Add or update a slice.
+        """
+
+        # Before a new slice is added, delete expired slices
+        if 'slice_id' not in self:
+            expired = Slices(self.api, expires = -int(time.time()))
+            for slice in expired:
+                slice.delete(commit)
+
+        Row.sync(self, commit)
+
+    def delete(self, commit = True):
+        """
+        Delete existing slice.
+        """
+
+        assert 'slice_id' in self
+
+        # Clean up miscellaneous join tables
+        for table in self.join_tables:
+            self.api.db.do("DELETE FROM %s WHERE slice_id = %d" % \
+                           (table, self['slice_id']))
+
+        # Mark as deleted
+        self['is_deleted'] = True
+        self.sync(commit)
+
+
+class Slices(Table):
+    """
+    Representation of row(s) from the slices table in the
+    database.
+    """
+
+    def __init__(self, api, slice_filter = None, columns = None, expires = int(time.time())):
+        Table.__init__(self, api, Slice, columns)
+
+        sql = "SELECT %s FROM view_slices WHERE is_deleted IS False" % \
+              ", ".join(self.columns)
+
+        if expires is not None:
+            if expires >= 0:
+                sql += " AND expires > %d" % expires
+            else:
+                expires = -expires
+                sql += " AND expires < %d" % expires
+
+        if slice_filter is not None:
+            if isinstance(slice_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), slice_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), slice_filter)
+                slice_filter = Filter(Slice.fields, {'slice_id': ints, 'name': strs})
+                sql += " AND (%s) %s" % slice_filter.sql(api, "OR")
+            elif isinstance(slice_filter, dict):
+                slice_filter = Filter(Slice.fields, slice_filter)
+                sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
+            elif isinstance (slice_filter, StringTypes):
+                slice_filter = Filter(Slice.fields, {'name':[slice_filter]})
+                sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
+            elif isinstance (slice_filter, int):
+                slice_filter = Filter(Slice.fields, {'slice_id':[slice_filter]})
+                sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
+            else:
+                raise PLCInvalidArgument, "Wrong slice filter %r"%slice_filter
+
+        self.selectall(sql)
diff --git a/PLC/Table.py b/PLC/Table.py
new file mode 100644 (file)
index 0000000..07c3c09
--- /dev/null
@@ -0,0 +1,328 @@
+from types import StringTypes, IntType, LongType
+import time
+import calendar
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+
+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 sync().
+    """
+
+    # Set this to the name of the table that stores the row.
+    table_name = None
+
+    # Set this to the name of the primary key of the table. It is
+    # assumed that the this key is a sequence if it is not set when
+    # sync() is called.
+    primary_key = None
+
+    # Set this to the names of tables that reference this table's
+    # primary key.
+    join_tables = []
+
+    # Set this to a dict of the valid fields of this object and their
+    # types. Not all fields (e.g., joined fields) may be updated via
+    # sync().
+    fields = {}
+
+    def __init__(self, api, fields = {}):
+        dict.__init__(self, fields)
+        self.api = api
+
+    def validate(self):
+        """
+        Validates values. Will validate a value with a custom function
+        if a function named 'validate_[key]' exists.
+        """
+
+        # Warn about mandatory fields
+        mandatory_fields = self.api.db.fields(self.table_name, notnull = True, hasdef = False)
+        for field in mandatory_fields:
+            if not self.has_key(field) or self[field] is None:
+                raise PLCInvalidArgument, field + " must be specified and cannot be unset in class %s"%self.__class__.__name__
+
+        # Validate values before committing
+        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 separate_types(self, items):
+       """
+       Separate a list of different typed objects. 
+       Return a list for each type (ints, strs and dicts)
+       """
+       
+       if isinstance(items, (list, tuple, set)):
+           ints = filter(lambda x: isinstance(x, (int, long)), items)
+           strs = filter(lambda x: isinstance(x, StringTypes), items)
+           dicts = filter(lambda x: isinstance(x, dict), items)
+           return (ints, strs, dicts)          
+       else:
+           raise PLCInvalidArgument, "Can only separate list types" 
+               
+
+    def associate(self, *args):
+       """
+       Provides a means for high lvl api calls to associate objects
+        using low lvl calls.
+       """
+
+       if len(args) < 3:
+           raise PLCInvalidArgumentCount, "auth, field, value must be specified"
+       elif hasattr(self, 'associate_' + args[1]):
+           associate = getattr(self, 'associate_'+args[1])
+           associate(*args)
+       else:
+           raise PLCInvalidArguemnt, "No such associate function associate_%s" % args[1]
+
+    def validate_timestamp(self, timestamp, check_future = False):
+        """
+        Validates the specified GMT timestamp string (must be in
+        %Y-%m-%d %H:%M:%S format) or number (seconds since UNIX epoch,
+        i.e., 1970-01-01 00:00:00 GMT). If check_future is True,
+        raises an exception if timestamp is not in the future. Returns
+        a GMT timestamp string.
+        """
+
+        time_format = "%Y-%m-%d %H:%M:%S"
+
+       if isinstance(timestamp, StringTypes):
+           # calendar.timegm() is the inverse of time.gmtime()
+           timestamp = calendar.timegm(time.strptime(timestamp, time_format))
+
+        # Human readable timestamp string
+       human = time.strftime(time_format, time.gmtime(timestamp))
+
+       if check_future and timestamp < time.time():
+            raise PLCInvalidArgument, "'%s' not in the future" % human
+
+       return human
+
+    def add_object(self, classobj, join_table, columns = None):
+        """
+        Returns a function that can be used to associate this object
+        with another.
+        """
+
+        def add(self, obj, columns = None, commit = True):
+            """
+            Associate with the specified object.
+            """
+
+            # Various sanity checks
+            assert isinstance(self, Row)
+            assert self.primary_key in self
+            assert join_table in self.join_tables
+            assert isinstance(obj, classobj)
+            assert isinstance(obj, Row)
+            assert obj.primary_key in obj
+           assert join_table in obj.join_tables
+
+            # By default, just insert the primary keys of each object
+            # into the join table.
+            if columns is None:
+                columns = {self.primary_key: self[self.primary_key],
+                           obj.primary_key: obj[obj.primary_key]}
+
+            params = []
+            for name, value in columns.iteritems():
+                params.append(self.api.db.param(name, value))
+
+            self.api.db.do("INSERT INTO %s (%s) VALUES(%s)" % \
+                           (join_table, ", ".join(columns), ", ".join(params)),
+                           columns)
+
+            if commit:
+                self.api.db.commit()
+    
+        return add
+
+    add_object = classmethod(add_object)
+
+    def remove_object(self, classobj, join_table):
+        """
+        Returns a function that can be used to disassociate this
+        object with another.
+        """
+
+        def remove(self, obj, commit = True):
+            """
+            Disassociate from the specified object.
+            """
+    
+            assert isinstance(self, Row)
+            assert self.primary_key in self
+            assert join_table in self.join_tables
+            assert isinstance(obj, classobj)
+            assert isinstance(obj, Row)
+            assert obj.primary_key in obj
+            assert join_table in obj.join_tables
+    
+            self_id = self[self.primary_key]
+            obj_id = obj[obj.primary_key]
+    
+            self.api.db.do("DELETE FROM %s WHERE %s = %s AND %s = %s" % \
+                           (join_table,
+                            self.primary_key, self.api.db.param('self_id', self_id),
+                            obj.primary_key, self.api.db.param('obj_id', obj_id)),
+                           locals())
+
+            if commit:
+                self.api.db.commit()
+
+        return remove
+
+    remove_object = classmethod(remove_object)
+
+    def db_fields(self, obj = None):
+        """
+        Return only those fields that can be set or updated directly
+        (i.e., those fields that are in the primary table (table_name)
+        for this object, and are not marked as a read-only Parameter.
+        """
+
+        if obj is None:
+            obj = self
+
+        db_fields = self.api.db.fields(self.table_name)
+        return dict(filter(lambda (key, value): \
+                           key in db_fields and \
+                           (key not in self.fields or \
+                            not isinstance(self.fields[key], Parameter) or \
+                            not self.fields[key].ro),
+                           obj.items()))
+
+    def __eq__(self, y):
+        """
+        Compare two objects.
+        """
+
+        # Filter out fields that cannot be set or updated directly
+        # (and thus would not affect equality for the purposes of
+        # deciding if we should sync() or not).
+        x = self.db_fields()
+        y = self.db_fields(y)
+        return dict.__eq__(x, y)
+
+    def sync(self, commit = True, insert = None):
+        """
+        Flush changes back to the database.
+        """
+
+        # Validate all specified fields
+        self.validate()
+
+        # Filter out fields that cannot be set or updated directly
+        db_fields = self.db_fields()
+
+        # Parameterize for safety
+        keys = db_fields.keys()
+        values = [self.api.db.param(key, value) for (key, value) in db_fields.items()]
+
+        # If the primary key (usually an auto-incrementing serial
+        # identifier) has not been specified, or the primary key is the
+        # only field in the table, or insert has been forced.
+        if not self.has_key(self.primary_key) or \
+           keys == [self.primary_key] or \
+           insert is True:
+           
+           # If primary key id is a serial int and it isnt included, get next id
+           if self.fields[self.primary_key].type in (IntType, LongType) and \
+              self.primary_key not in self:
+               pk_id = self.api.db.next_id(self.table_name, self.primary_key)
+               self[self.primary_key] = pk_id
+               db_fields[self.primary_key] = pk_id
+               keys = db_fields.keys()
+               values = [self.api.db.param(key, value) for (key, value) in db_fields.items()]
+            # Insert new row
+            sql = "INSERT INTO %s (%s) VALUES (%s)" % \
+                  (self.table_name, ", ".join(keys), ", ".join(values))
+        else:
+            # Update existing row
+            columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
+            sql = "UPDATE %s SET " % self.table_name + \
+                  ", ".join(columns) + \
+                  " WHERE %s = %s" % \
+                  (self.primary_key,
+                   self.api.db.param(self.primary_key, self[self.primary_key]))
+
+        self.api.db.do(sql, db_fields)
+
+        if commit:
+            self.api.db.commit()
+
+    def delete(self, commit = True):
+        """
+        Delete row from its primary table, and from any tables that
+        reference it.
+        """
+
+        assert self.primary_key in self
+
+        for table in self.join_tables + [self.table_name]:
+            if isinstance(table, tuple):
+                key = table[1]
+                table = table[0]
+            else:
+                key = self.primary_key
+
+            sql = "DELETE FROM %s WHERE %s = %s" % \
+                  (table, key,
+                   self.api.db.param(self.primary_key, self[self.primary_key]))
+
+            self.api.db.do(sql, self)
+
+        if commit:
+            self.api.db.commit()
+
+class Table(list):
+    """
+    Representation of row(s) in a database table.
+    """
+
+    def __init__(self, api, classobj, columns = None):
+        self.api = api
+        self.classobj = classobj
+        self.rows = {}
+
+        if columns is None:
+            columns = classobj.fields
+        else:
+            columns = filter(lambda x: x in classobj.fields, columns)
+            if not columns:
+                raise PLCInvalidArgument, "No valid return fields specified"
+
+        self.columns = columns
+
+    def sync(self, commit = True):
+        """
+        Flush changes back to the database.
+        """
+
+        for row in self:
+            row.sync(commit)
+
+    def selectall(self, sql, params = None):
+        """
+        Given a list of rows from the database, fill ourselves with
+        Row objects.
+        """
+
+        for row in self.api.db.selectall(sql, params):
+            obj = self.classobj(self.api, row)
+            self.append(obj)
+
+    def dict(self, key_field = None):
+        """
+        Return ourself as a dict keyed on key_field.
+        """
+
+        if key_field is None:
+            key_field = self.classobj.primary_key
+
+        return dict([(obj[key_field], obj) for obj in self])
diff --git a/PLC/Test.py b/PLC/Test.py
new file mode 100644 (file)
index 0000000..4867119
--- /dev/null
@@ -0,0 +1,1460 @@
+#!/usr/bin/python
+#
+# Test script utility class
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Test.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from pprint import pprint
+from string import letters, digits, punctuation
+from traceback import print_exc
+from optparse import OptionParser
+import socket
+import base64
+import struct
+import os
+import xmlrpclib
+
+from PLC.Shell import Shell
+
+from random import Random
+random = Random()
+
+def randfloat(min = 0.0, max = 1.0):
+    return float(min) + (random.random() * (float(max) - float(min)))
+
+def randint(min = 0, max = 1):
+    return int(randfloat(min, max + 1))
+
+# See "2.2 Characters" in the XML specification:
+#
+# #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
+# avoiding
+# [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
+#
+
+ascii_xml_chars = map(unichr, [0x9, 0xA])
+# xmlrpclib uses xml.parsers.expat, which always converts either '\r'
+# (#xD) or '\n' (#xA) to '\n'. So avoid using '\r', too, if this is
+# still the case.
+if xmlrpclib.loads(xmlrpclib.dumps(('\r',)))[0][0] == '\r':
+    ascii_xml_chars.append('\r')
+ascii_xml_chars += map(unichr, xrange(0x20, 0x7F - 1))
+low_xml_chars = list(ascii_xml_chars)
+low_xml_chars += map(unichr, xrange(0x84 + 1, 0x86 - 1))
+low_xml_chars += map(unichr, xrange(0x9F + 1, 0xFF))
+valid_xml_chars = list(low_xml_chars)
+valid_xml_chars += map(unichr, xrange(0xFF + 1, 0xD7FF))
+valid_xml_chars += map(unichr, xrange(0xE000, 0xFDD0 - 1))
+valid_xml_chars += map(unichr, xrange(0xFDDF + 1, 0xFFFD))
+
+def randstr(length, pool = valid_xml_chars, encoding = "utf-8"):
+    sample = random.sample(pool, min(length, len(pool)))
+    while True:
+        s = u''.join(sample)
+        bytes = len(s.encode(encoding))
+        if bytes > length:
+            sample.pop()
+        elif bytes < length:
+            sample += random.sample(pool, min(length - bytes, len(pool)))
+            random.shuffle(sample)
+        else:
+            break
+    return s
+
+def randhostname():
+    # 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.
+    hostname = 'a' + randstr(61, letters + digits + '-') + '1.' + \
+               'b' + randstr(61, letters + digits + '-') + '2.' + \
+               'c' + randstr(5, letters)
+    return hostname
+
+def randpath(length):
+    parts = []
+    for i in range(randint(1, 10)):
+        parts.append(randstr(randint(1, 30), ascii_xml_chars))
+    return u'/'.join(parts)[0:length]
+
+def randemail():
+    return (randstr(100, letters + digits) + "@" + randhostname()).lower()
+
+def randkey(bits = 2048):
+    ssh_key_types = ["ssh-dss", "ssh-rsa"]
+    key_type = random.sample(ssh_key_types, 1)[0]
+    return ' '.join([key_type,
+                     base64.b64encode(''.join(randstr(bits / 8).encode("utf-8"))),
+                     randemail()])
+
+def random_site():
+    return {
+        'name': randstr(254),
+        'abbreviated_name': randstr(50),
+        'login_base': randstr(20, letters).lower(),
+        'latitude': int(randfloat(-90.0, 90.0) * 1000) / 1000.0,
+        'longitude': int(randfloat(-180.0, 180.0) * 1000) / 1000.0,
+        }
+            
+def random_address_type():
+    return {
+        'name': randstr(20),
+        'description': randstr(254),
+        }
+
+def random_address():
+    return {
+        'line1': randstr(254),
+        'line2': randstr(254),
+        'line3': randstr(254),
+        'city': randstr(254),
+        'state': randstr(254),
+        'postalcode': randstr(64),
+        'country': randstr(128),
+        }
+
+def random_person():
+    return {
+        'first_name': randstr(128),
+        'last_name': randstr(128),
+        'email': randemail(),
+        'bio': randstr(254),
+        # Accounts are disabled by default
+        'enabled': False,
+        'password': randstr(254),
+        }
+
+def random_key(key_types):
+    return {
+        'key_type': random.sample(key_types, 1)[0],
+        'key': randkey()
+        }
+
+def random_nodegroup():
+    return {
+        'name': randstr(50),
+        'description': randstr(200),
+        }
+
+def random_node(boot_states):
+    return {
+        'hostname': randhostname(),
+        'boot_state': random.sample(boot_states, 1)[0],
+        'model': randstr(255),
+        'version': randstr(64),
+        }
+
+def random_nodenetwork(method, type):
+    nodenetwork_fields = {
+        'method': method,
+        'type': type,
+        'bwlimit': randint(500000, 10000000),
+        }
+
+    if method != 'dhcp':
+        ip = randint(0, 0xffffffff)
+        netmask = (0xffffffff << randint(2, 31)) & 0xffffffff
+        network = ip & netmask
+        broadcast = ((ip & netmask) | ~netmask) & 0xffffffff
+        gateway = randint(network + 1, broadcast - 1)
+        dns1 = randint(0, 0xffffffff)
+
+        for field in 'ip', 'netmask', 'network', 'broadcast', 'gateway', 'dns1':
+            nodenetwork_fields[field] = socket.inet_ntoa(struct.pack('>L', locals()[field]))
+
+    return nodenetwork_fields
+
+def random_pcu():
+    return {
+        'hostname': randhostname(),
+        'ip': socket.inet_ntoa(struct.pack('>L', randint(0, 0xffffffff))),
+        'protocol': randstr(16),
+        'username': randstr(254),
+        'password': randstr(254),
+        'notes': randstr(254),
+        'model': randstr(32),
+        }
+
+def random_conf_file():
+    return {
+        'enabled': bool(randint()),
+        'source': randpath(255),
+        'dest': randpath(255),
+        'file_permissions': "%#o" % randint(0, 512),
+        'file_owner': randstr(32, letters + '_' + digits),
+        'file_group': randstr(32, letters + '_' + digits),
+        'preinstall_cmd': randpath(100),
+        'postinstall_cmd': randpath(100),
+        'error_cmd': randpath(100),
+        'ignore_cmd_errors': bool(randint()),
+        'always_update': bool(randint()),
+        }
+
+def random_attribute_type(role_ids):
+    return {
+        'name': randstr(100),
+        'description': randstr(254),
+        'min_role_id': random.sample(role_ids, 1)[0],
+        }
+
+def random_slice(login_base):
+    return {
+        'name': login_base + "_" + randstr(11, letters).lower(),
+        'url': "http://" + randhostname() + "/",
+        'description': randstr(2048),
+        }
+
+class Test:
+    tiny = {
+        'sites': 1,
+        'address_types': 1,
+        'addresses_per_site': 1,
+        'persons_per_site': 1,
+        'keys_per_person': 1,
+        'nodegroups': 1,
+        'nodes_per_site': 1,
+        'nodenetworks_per_node': 1,
+        'pcus_per_site': 1,
+        'conf_files': 1,
+        'attribute_types': 1,
+        'slices_per_site': 1,
+        'attributes_per_slice': 1,
+        }
+
+    default = {
+        'sites': 10,
+        'address_types': 2,
+        'addresses_per_site': 2,
+        'persons_per_site': 10,
+        'keys_per_person': 2,
+        'nodegroups': 10,
+        'nodes_per_site': 2,
+        'nodenetworks_per_node': 1,
+        'pcus_per_site': 1,
+        'conf_files': 10,
+        'attribute_types': 10,
+        'slices_per_site': 10,
+        'attributes_per_slice': 2,
+        }
+
+    def __init__(self, api, check = True, verbose = True):
+        self.api = api
+        self.check = check
+        self.verbose = verbose
+        
+        self.site_ids = []
+        self.address_type_ids = []
+        self.address_ids = []
+        self.person_ids = []
+        self.key_ids = []
+        self.nodegroup_ids = []
+        self.node_ids = []
+        self.nodenetwork_ids = []
+        self.pcu_ids = []
+        self.conf_file_ids = []
+        self.attribute_type_ids = []
+        self.slice_ids = []
+        self.slice_attribute_ids = []
+
+    def Run(self, **kwds):
+        """
+        Run a complete database and API consistency test. Populates
+        the database with a set of random entities, updates them, then
+        deletes them. Examples:
+
+        test.Run() # Defaults
+        test.Run(**Test.default) # Defaults
+        test.Run(**Test.tiny) # Tiny set
+        test.Run(sites = 123, slices_per_site = 4) # Defaults with overrides
+        """
+
+        try:
+            self.Add(**kwds)
+            self.Update()
+        finally:
+            self.Delete()
+
+    def Add(self, **kwds):
+        """
+        Populate the database with a set of random entities. Examples:
+
+        test.populate() # Defaults
+        test.populate(Test.tiny) # Tiny set
+        test.populate(sites = 123, slices_per_site = 4) # Defaults with overrides
+        """
+
+        params = self.default.copy()
+        params.update(kwds)
+
+        self.AddSites(params['sites'])
+        self.AddAddressTypes(params['address_types'])
+        self.AddAddresses(params['addresses_per_site'])
+        self.AddPersons(params['persons_per_site'])
+        self.AddKeys(params['keys_per_person'])
+        self.AddNodeGroups(params['nodegroups'])
+        self.AddNodes(params['nodes_per_site'])
+        self.AddNodeNetworks(params['nodenetworks_per_node'])
+        self.AddPCUs(params['pcus_per_site'])
+        self.AddConfFiles(params['conf_files'])
+        self.AddSliceAttributeTypes(params['attribute_types'])
+        self.AddSlices(params['slices_per_site'])
+        self.AddSliceAttributes(params['attributes_per_slice'])
+
+    def Update(self):
+        self.UpdateSites()
+        self.UpdateAddressTypes()
+        self.UpdateAddresses()
+        self.UpdatePersons()
+        self.UpdateKeys()
+        self.UpdateNodeGroups()
+        self.UpdateNodes()
+        self.UpdateNodeNetworks()
+        self.UpdatePCUs()
+        self.UpdateConfFiles()
+        self.UpdateSliceAttributeTypes()
+        self.UpdateSlices()
+        self.UpdateSliceAttributes()
+
+    def Delete(self):
+        self.DeleteSliceAttributes()
+        self.DeleteSlices()
+        self.DeleteSliceAttributeTypes()
+        self.DeleteKeys()
+        self.DeleteConfFiles()
+        self.DeletePCUs()
+        self.DeleteNodeNetworks()
+        self.DeleteNodes()
+        self.DeletePersons()
+        self.DeleteNodeGroups()
+        self.DeleteAddresses()
+        self.DeleteAddressTypes()
+        self.DeleteSites()
+
+    def AddSites(self, n = 10):
+        """
+        Add a number of random sites.
+        """
+
+        for i in range(n):
+            # Add site
+            site_fields = random_site()
+            site_id = self.api.AddSite(site_fields)
+
+            # Should return a unique site_id
+            assert site_id not in self.site_ids
+            self.site_ids.append(site_id)
+
+            # Enable slice creation
+            site_fields['max_slices'] = randint(1, 10)
+            self.api.UpdateSite(site_id, site_fields)
+
+            if self.check:
+                # Check site
+                site = self.api.GetSites([site_id])[0]
+                for field in site_fields:
+                    assert site[field] == site_fields[field]
+
+            if self.verbose:
+                print "Added site", site_id
+
+    def UpdateSites(self):
+        """
+        Make random changes to any sites we may have added.
+        """
+
+        for site_id in self.site_ids:
+            # Update site
+            site_fields = random_site()
+            # Do not change login_base
+           if 'login_base' in site_fields:
+               del site_fields['login_base']
+            self.api.UpdateSite(site_id, site_fields)
+
+            if self.check:
+                # Check site
+                site = self.api.GetSites([site_id])[0]
+                for field in site_fields:
+                    assert site[field] == site_fields[field]
+
+            if self.verbose:
+                print "Updated site", site_id
+
+    def DeleteSites(self):
+        """
+        Delete any random sites we may have added.
+        """
+
+        for site_id in self.site_ids:
+            self.api.DeleteSite(site_id)
+
+            if self.check:
+                assert not self.api.GetSites([site_id])
+
+            if self.verbose:
+                print "Deleted site", site_id
+
+        if self.check:
+            assert not self.api.GetSites(self.site_ids)
+
+        self.site_ids = []
+
+    def AddAddressTypes(self, n = 2):
+        """
+        Add a number of random address types.
+        """
+
+        for i in range(n):
+            address_type_fields = random_address_type()
+            address_type_id = self.api.AddAddressType(address_type_fields)
+
+            # Should return a unique address_type_id
+            assert address_type_id not in self.address_type_ids
+            self.address_type_ids.append(address_type_id)
+
+            if self.check:
+                # Check address type
+                address_type = self.api.GetAddressTypes([address_type_id])[0]
+                for field in address_type_fields:
+                    assert address_type[field] == address_type_fields[field]
+
+            if self.verbose:
+                print "Added address type", address_type_id
+
+    def UpdateAddressTypes(self):
+        """
+        Make random changes to any address types we may have added.
+        """
+
+        for address_type_id in self.address_type_ids:
+            # Update address_type
+            address_type_fields = random_address_type()
+            self.api.UpdateAddressType(address_type_id, address_type_fields)
+
+            if self.check:
+                # Check address type
+                address_type = self.api.GetAddressTypes([address_type_id])[0]
+                for field in address_type_fields:
+                    assert address_type[field] == address_type_fields[field]
+
+            if self.verbose:
+                print "Updated address_type", address_type_id
+
+    def DeleteAddressTypes(self):
+        """
+        Delete any random address types we may have added.
+        """
+
+        for address_type_id in self.address_type_ids:
+            self.api.DeleteAddressType(address_type_id)
+
+            if self.check:
+                assert not self.api.GetAddressTypes([address_type_id])
+
+            if self.verbose:
+                print "Deleted address type", address_type_id
+
+        if self.check:
+            assert not self.api.GetAddressTypes(self.address_type_ids)
+
+        self.address_type_ids = []
+
+    def AddAddresses(self, per_site = 2):
+        """
+        Add a number of random addresses to each site.
+        """
+
+        for site_id in self.site_ids:
+            for i in range(per_site):
+                address_fields = random_address()
+                address_id = self.api.AddSiteAddress(site_id, address_fields)
+
+                # Should return a unique address_id
+                assert address_id not in self.address_ids
+                self.address_ids.append(address_id)
+
+                # Add random address type
+                if self.address_type_ids:
+                    for address_type_id in random.sample(self.address_type_ids, 1):
+                        self.api.AddAddressTypeToAddress(address_type_id, address_id)
+
+                if self.check:
+                    # Check address
+                    address = self.api.GetAddresses([address_id])[0]
+                    for field in address_fields:
+                        assert address[field] == address_fields[field]
+
+                if self.verbose:
+                    print "Added address", address_id, "to site", site_id
+
+    def UpdateAddresses(self):
+        """
+        Make random changes to any addresses we may have added.
+        """
+
+        for address_id in self.address_ids:
+            # Update address
+            address_fields = random_address()
+            self.api.UpdateAddress(address_id, address_fields)
+
+            if self.check:
+                # Check address
+                address = self.api.GetAddresses([address_id])[0]
+                for field in address_fields:
+                    assert address[field] == address_fields[field]
+
+            if self.verbose:
+                print "Updated address", address_id
+
+    def DeleteAddresses(self):
+        """
+        Delete any random addresses we may have added.
+        """
+
+        for address_id in self.address_ids:
+            # Remove address types
+            address = self.api.GetAddresses([address_id])[0]
+            for address_type_id in address['address_type_ids']:
+                self.api.DeleteAddressTypeFromAddress(address_type_id, address_id)
+
+            if self.check:
+                address = self.api.GetAddresses([address_id])[0]
+                assert not address['address_type_ids']
+
+            self.api.DeleteAddress(address_id)
+
+            if self.check:
+                assert not self.api.GetAddresses([address_id])
+
+            if self.verbose:
+                print "Deleted address", address_id
+
+        if self.check:
+            assert not self.api.GetAddresses(self.address_ids)
+
+        self.address_ids = []
+
+    def AddPersons(self, per_site = 10):
+        """
+        Add a number of random users to each site.
+        """
+
+        for site_id in self.site_ids:
+            for i in range(per_site):
+                # Add user
+                person_fields = random_person()
+                person_id = self.api.AddPerson(person_fields)
+
+                # Should return a unique person_id
+                assert person_id not in self.person_ids
+                self.person_ids.append(person_id)
+
+                if self.check:
+                    # Check user
+                    person = self.api.GetPersons([person_id])[0]
+                    for field in person_fields:
+                        if field != 'password':
+                            assert person[field] == person_fields[field]
+
+                auth = {'AuthMethod': "password",
+                        'Username': person_fields['email'],
+                        'AuthString': person_fields['password']}
+
+                if self.check:
+                    # Check that user is disabled
+                    try:
+                        assert not self.api.AuthCheck(auth)
+                    except:
+                        pass
+
+                # Add random set of roles
+                role_ids = random.sample([20, 30, 40], randint(1, 3))
+                for role_id in role_ids:
+                    self.api.AddRoleToPerson(role_id, person_id)
+
+                if self.check:
+                    person = self.api.GetPersons([person_id])[0]
+                    assert set(role_ids) == set(person['role_ids'])
+
+                # Enable user
+                self.api.UpdatePerson(person_id, {'enabled': True})
+
+                if self.check:
+                    # Check that user is enabled
+                    assert self.api.AuthCheck(auth)
+
+                # Associate user with site
+                self.api.AddPersonToSite(person_id, site_id)
+                self.api.SetPersonPrimarySite(person_id, site_id)
+
+                if self.check:
+                    person = self.api.GetPersons([person_id])[0]
+                    assert person['site_ids'][0] == site_id
+
+                if self.verbose:
+                    print "Added user", person_id, "to site", site_id
+
+    def UpdatePersons(self):
+        """
+        Make random changes to any users we may have added.
+        """
+
+        for person_id in self.person_ids:
+            # Update user
+            person_fields = random_person()
+            # Keep them enabled
+            person_fields['enabled'] = True
+            self.api.UpdatePerson(person_id, person_fields)
+
+            if self.check:
+                # Check user
+                person = self.api.GetPersons([person_id])[0]
+                for field in person_fields:
+                    if field != 'password':
+                        assert person[field] == person_fields[field]
+
+            if self.verbose:
+                print "Updated person", person_id
+
+            person = self.api.GetPersons([person_id])[0]
+
+            # Associate user with a random set of sites
+            site_ids = random.sample(self.site_ids, randint(0, len(self.site_ids)))
+            for site_id in (set(site_ids) - set(person['site_ids'])):
+                self.api.AddPersonToSite(person_id, site_id)
+            for site_id in (set(person['site_ids']) - set(site_ids)):
+                self.api.DeletePersonFromSite(person_id, site_id)
+
+            if site_ids:
+                self.api.SetPersonPrimarySite(person_id, site_ids[0])
+
+            if self.check:
+                person = self.api.GetPersons([person_id])[0]
+                assert set(site_ids) == set(person['site_ids'])
+
+            if self.verbose:
+                print "Updated person", person_id, "to sites", site_ids
+
+    def DeletePersons(self):
+        """
+        Delete any random users we may have added.
+        """
+
+        for person_id in self.person_ids:
+            # Remove from site
+            person = self.api.GetPersons([person_id])[0]
+            for site_id in person['site_ids']:
+                self.api.DeletePersonFromSite(person_id, site_id)
+
+            if self.check:
+                person = self.api.GetPersons([person_id])[0]
+                assert not person['site_ids']
+
+            # Revoke roles
+            for role_id in person['role_ids']:
+                self.api.DeleteRoleFromPerson(role_id, person_id)
+
+            if self.check:
+                person = self.api.GetPersons([person_id])[0]
+                assert not person['role_ids']
+
+            # Disable account
+            self.api.UpdatePerson(person_id, {'enabled': False})
+
+            if self.check:
+                person = self.api.GetPersons([person_id])[0]
+                assert not person['enabled']
+
+            # Delete account
+            self.api.DeletePerson(person_id)
+
+            if self.check:
+                assert not self.api.GetPersons([person_id])                         
+
+            if self.verbose:
+                print "Deleted user", person_id
+
+        if self.check:
+            assert not self.api.GetPersons(self.person_ids)
+
+        self.person_ids = []
+
+    def AddKeys(self, per_person = 2):
+        """
+        Add a number of random keys to each user.
+        """
+
+        key_types = self.api.GetKeyTypes()
+        if not key_types:
+            raise Exception, "No key types"
+
+        for person_id in self.person_ids:
+            for i in range(per_person):
+                # Add key
+                key_fields = random_key(key_types)
+                key_id = self.api.AddPersonKey(person_id, key_fields)
+
+                # Should return a unique key_id
+                assert key_id not in self.key_ids
+                self.key_ids.append(key_id)
+
+                if self.check:
+                    # Check key
+                    key = self.api.GetKeys([key_id])[0]
+                    for field in key_fields:
+                        assert key[field] == key_fields[field]
+
+                    # Add and immediately blacklist a key
+                    key_fields = random_key(key_types)
+                    key_id = self.api.AddPersonKey(person_id, key_fields)
+
+                    self.api.BlacklistKey(key_id)
+
+                    # Is effectively deleted
+                    assert not self.api.GetKeys([key_id])
+
+                    # Cannot be added again
+                    try:
+                        key_id = self.api.AddPersonKey(person_id, key_fields)
+                        assert False
+                    except Exception, e:
+                        pass
+
+                if self.verbose:
+                    print "Added key", key_id, "to user", person_id
+
+    def UpdateKeys(self):
+        """
+        Make random changes to any keys we may have added.
+        """
+
+        key_types = self.api.GetKeyTypes()
+        if not key_types:
+            raise Exception, "No key types"
+
+        for key_id in self.key_ids:
+            # Update key
+            key_fields = random_key(key_types)
+            self.api.UpdateKey(key_id, key_fields)
+
+            if self.check:
+                # Check key
+                key = self.api.GetKeys([key_id])[0]
+                for field in key_fields:
+                    assert key[field] == key_fields[field]
+
+            if self.verbose:
+                print "Updated key", key_id
+
+    def DeleteKeys(self):
+        """
+        Delete any random keys we may have added.
+        """
+
+        for key_id in self.key_ids:
+            self.api.DeleteKey(key_id)
+
+            if self.check:
+                assert not self.api.GetKeys([key_id])
+
+            if self.verbose:
+                print "Deleted key", key_id
+
+        if self.check:
+            assert not self.api.GetKeys(self.key_ids)
+
+        self.key_ids = []
+
+    def AddNodeGroups(self, n = 10):
+        """
+        Add a number of random node groups.
+        """
+
+        for i in range(n):
+            # Add node group
+            nodegroup_fields = random_nodegroup()
+            nodegroup_id = self.api.AddNodeGroup(nodegroup_fields)
+
+            # Should return a unique nodegroup_id
+            assert nodegroup_id not in self.nodegroup_ids
+            self.nodegroup_ids.append(nodegroup_id)
+
+            if self.check:
+                # Check node group
+                nodegroup = self.api.GetNodeGroups([nodegroup_id])[0]
+                for field in nodegroup_fields:
+                    assert nodegroup[field] == nodegroup_fields[field]
+
+            if self.verbose:
+                print "Added node group", nodegroup_id
+
+    def UpdateNodeGroups(self):
+        """
+        Make random changes to any node groups we may have added.
+        """
+
+        for nodegroup_id in self.nodegroup_ids:
+            # Update nodegroup
+            nodegroup_fields = random_nodegroup()
+            self.api.UpdateNodeGroup(nodegroup_id, nodegroup_fields)
+
+            if self.check:
+                # Check nodegroup
+                nodegroup = self.api.GetNodeGroups([nodegroup_id])[0]
+                for field in nodegroup_fields:
+                    assert nodegroup[field] == nodegroup_fields[field]
+
+            if self.verbose:
+                print "Updated node group", nodegroup_id
+
+    def DeleteNodeGroups(self):
+        """
+        Delete any random node groups we may have added.
+        """
+
+        for nodegroup_id in self.nodegroup_ids:
+            self.api.DeleteNodeGroup(nodegroup_id)
+
+            if self.check:
+                assert not self.api.GetNodeGroups([nodegroup_id])
+
+            if self.verbose:
+                print "Deleted node group", nodegroup_id
+
+        if self.check:
+            assert not self.api.GetNodeGroups(self.nodegroup_ids)
+
+        self.nodegroup_ids = []
+
+    def AddNodes(self, per_site = 2):
+        """
+        Add a number of random nodes to each site. Each node will also
+        be added to a random node group if AddNodeGroups() was
+        previously run.
+        """
+        
+        boot_states = self.api.GetBootStates()
+        if not boot_states:
+            raise Exception, "No boot states"
+
+        for site_id in self.site_ids:
+            for i in range(per_site):
+                # Add node
+                node_fields = random_node(boot_states)
+                node_id = self.api.AddNode(site_id, node_fields)
+
+                # Should return a unique node_id
+                assert node_id not in self.node_ids
+                self.node_ids.append(node_id)
+
+                # Add to a random set of node groups
+                nodegroup_ids = random.sample(self.nodegroup_ids, randint(0, len(self.nodegroup_ids)))
+                for nodegroup_id in nodegroup_ids:
+                    self.api.AddNodeToNodeGroup(node_id, nodegroup_id)
+
+                if self.check:
+                    # Check node
+                    node = self.api.GetNodes([node_id])[0]
+                    for field in node_fields:
+                        assert node[field] == node_fields[field]
+
+                if self.verbose:
+                    print "Added node", node_id
+
+    def UpdateNodes(self):
+        """
+        Make random changes to any nodes we may have added.
+        """
+
+        boot_states = self.api.GetBootStates()
+        if not boot_states:
+            raise Exception, "No boot states"
+
+        for node_id in self.node_ids:
+            # Update node
+            node_fields = random_node(boot_states)
+            self.api.UpdateNode(node_id, node_fields)
+
+            node = self.api.GetNodes([node_id])[0]
+
+            # Add to a random set of node groups
+            nodegroup_ids = random.sample(self.nodegroup_ids, randint(0, len(self.nodegroup_ids)))
+            for nodegroup_id in (set(nodegroup_ids) - set(node['nodegroup_ids'])):
+                self.api.AddNodeToNodeGroup(node_id, nodegroup_id)
+            for nodegroup_id in (set(node['nodegroup_ids']) - set(nodegroup_ids)):
+                self.api.DeleteNodeFromNodeGroup(node_id, nodegroup_id)
+
+            if self.check:
+                # Check node
+                node = self.api.GetNodes([node_id])[0]
+                for field in node_fields:
+                    assert node[field] == node_fields[field]
+                assert set(nodegroup_ids) == set(node['nodegroup_ids'])
+
+            if self.verbose:
+                print "Updated node", node_id
+                print "Added node", node_id, "to node groups", nodegroup_ids
+
+    def DeleteNodes(self):
+        """
+        Delete any random nodes we may have added.
+        """
+
+        for node_id in self.node_ids:
+            # Remove from node groups
+            node = self.api.GetNodes([node_id])[0]
+            for nodegroup_id in node['nodegroup_ids']:
+                self.api.DeleteNodeFromNodeGroup(node_id, nodegroup_id)
+
+            if self.check:
+                node = self.api.GetNodes([node_id])[0]
+                assert not node['nodegroup_ids']
+
+            self.api.DeleteNode(node_id)
+
+            if self.check:
+                assert not self.api.GetNodes([node_id])
+
+            if self.verbose:
+                print "Deleted node", node_id
+
+        if self.check:
+            assert not self.api.GetNodes(self.node_ids)
+
+        self.node_ids = []
+
+    def AddNodeNetworks(self, per_node = 1):
+        """
+        Add a number of random network interfaces to each node.
+        """
+
+        network_methods = self.api.GetNetworkMethods()
+        if not network_methods:
+            raise Exception, "No network methods"
+        
+        network_types = self.api.GetNetworkTypes()
+        if not network_types:
+            raise Exception, "No network types"
+
+        for node_id in self.node_ids:
+            for i in range(per_node):
+                method = random.sample(network_methods, 1)[0]
+                type = random.sample(network_types, 1)[0]
+
+                # Add node network
+                nodenetwork_fields = random_nodenetwork(method, type)
+                nodenetwork_id = self.api.AddNodeNetwork(node_id, nodenetwork_fields)
+
+                # Should return a unique nodenetwork_id
+                assert nodenetwork_id not in self.nodenetwork_ids
+                self.nodenetwork_ids.append(nodenetwork_id)
+
+                if self.check:
+                    # Check node network
+                    nodenetwork = self.api.GetNodeNetworks([nodenetwork_id])[0]
+                    for field in nodenetwork_fields:
+                        assert nodenetwork[field] == nodenetwork_fields[field]
+
+                if self.verbose:
+                    print "Added node network", nodenetwork_id, "to node", node_id
+
+    def UpdateNodeNetworks(self):
+        """
+        Make random changes to any network interfaces we may have added.
+        """
+
+        network_methods = self.api.GetNetworkMethods()
+        if not network_methods:
+            raise Exception, "No network methods"
+        
+        network_types = self.api.GetNetworkTypes()
+        if not network_types:
+            raise Exception, "No network types"
+
+        for nodenetwork_id in self.nodenetwork_ids:
+            method = random.sample(network_methods, 1)[0]
+            type = random.sample(network_types, 1)[0]
+
+            # Update nodenetwork
+            nodenetwork_fields = random_nodenetwork(method, type)
+            self.api.UpdateNodeNetwork(nodenetwork_id, nodenetwork_fields)
+
+            if self.check:
+                # Check nodenetwork
+                nodenetwork = self.api.GetNodeNetworks([nodenetwork_id])[0]
+                for field in nodenetwork_fields:
+                    assert nodenetwork[field] == nodenetwork_fields[field]
+
+            if self.verbose:
+                print "Updated node network", nodenetwork_id
+
+    def DeleteNodeNetworks(self):
+        """
+        Delete any random network interfaces we may have added.
+        """
+
+        for nodenetwork_id in self.nodenetwork_ids:
+            self.api.DeleteNodeNetwork(nodenetwork_id)
+
+            if self.check:
+                assert not self.api.GetNodeNetworks([nodenetwork_id])
+
+            if self.verbose:
+                print "Deleted node network", nodenetwork_id
+
+        if self.check:
+            assert not self.api.GetNodeNetworks(self.nodenetwork_ids)
+
+        self.nodenetwork_ids = []
+
+    def AddPCUs(self, per_site = 1):
+        """
+        Add a number of random PCUs to each site. Each node at the
+        site will be added to a port on the PCU if AddNodes() was
+        previously run.
+        """
+
+        for site_id in self.site_ids:
+            for i in range(per_site):
+                # Add PCU
+                pcu_fields = random_pcu()
+                pcu_id = self.api.AddPCU(site_id, pcu_fields)
+
+                # Should return a unique pcu_id
+                assert pcu_id not in self.pcu_ids
+                self.pcu_ids.append(pcu_id)
+
+                # Add each node at this site to a different port on this PCU
+                site = self.api.GetSites([site_id])[0]
+                port = randint(1, 10)
+                for node_id in site['node_ids']:
+                    self.api.AddNodeToPCU(node_id, pcu_id, port)
+                    port += 1
+
+                if self.check:
+                    # Check PCU
+                    pcu = self.api.GetPCUs([pcu_id])[0]
+                    for field in pcu_fields:
+                        assert pcu[field] == pcu_fields[field]
+
+                if self.verbose:
+                    print "Added PCU", pcu_id, "to site", site_id
+
+    def UpdatePCUs(self):
+        """
+        Make random changes to any PCUs we may have added.
+        """
+
+        for pcu_id in self.pcu_ids:
+            # Update PCU
+            pcu_fields = random_pcu()
+            self.api.UpdatePCU(pcu_id, pcu_fields)
+
+            if self.check:
+                # Check PCU
+                pcu = self.api.GetPCUs([pcu_id])[0]
+                for field in pcu_fields:
+                    assert pcu[field] == pcu_fields[field]
+
+            if self.verbose:
+                print "Updated PCU", pcu_id
+
+    def DeletePCUs(self):
+        """
+        Delete any random nodes we may have added.
+        """
+
+        for pcu_id in self.pcu_ids:
+            # Remove nodes from PCU
+            pcu = self.api.GetPCUs([pcu_id])[0]
+            for node_id in pcu['node_ids']:
+                self.api.DeleteNodeFromPCU(node_id, pcu_id)
+
+            if self.check:
+                pcu = self.api.GetPCUs([pcu_id])[0]
+                assert not pcu['node_ids']
+
+            self.api.DeletePCU(pcu_id)
+
+            if self.check:
+                assert not self.api.GetPCUs([pcu_id])
+
+            if self.verbose:
+                print "Deleted PCU", pcu_id
+
+        if self.check:
+            assert not self.api.GetPCUs(self.pcu_ids)
+
+        self.pcu_ids = []
+
+    def AddConfFiles(self, n = 10):
+        """
+        Add a number of random global configuration files.
+        """
+
+        conf_files = []
+
+        for i in range(n):
+            # Add a random configuration file
+            conf_files.append(random_conf_file())
+
+        if n:
+            # Add a nodegroup override file
+            nodegroup_conf_file = conf_files[0].copy()
+            nodegroup_conf_file['source'] = randpath(255)
+            conf_files.append(nodegroup_conf_file)
+
+            # Add a node override file
+            node_conf_file = conf_files[0].copy()
+            node_conf_file['source'] = randpath(255)
+            conf_files.append(node_conf_file)
+
+        for conf_file_fields in conf_files:
+            conf_file_id = self.api.AddConfFile(conf_file_fields)
+
+            # Should return a unique conf_file_id
+            assert conf_file_id not in self.conf_file_ids
+            self.conf_file_ids.append(conf_file_id)
+
+            # Add to nodegroup
+            if conf_file_fields == nodegroup_conf_file and self.nodegroup_ids:
+                nodegroup_id = random.sample(self.nodegroup_ids, 1)[0]
+                self.api.AddConfFileToNodeGroup(conf_file_id, nodegroup_id)
+            else:
+                nodegroup_id = None
+
+            # Add to node
+            if conf_file_fields == node_conf_file and self.node_ids:
+                node_id = random.sample(self.node_ids, 1)[0]
+                self.api.AddConfFileToNode(conf_file_id, node_id)
+            else:
+                node_id = None
+
+            if self.check:
+                # Check configuration file
+                conf_file = self.api.GetConfFiles([conf_file_id])[0]
+                for field in conf_file_fields:
+                    assert conf_file[field] == conf_file_fields[field]
+
+            if self.verbose:
+                print "Added configuration file", conf_file_id,
+                if nodegroup_id is not None:
+                    print "to node group", nodegroup_id,
+                elif node_id is not None:
+                    print "to node", node_id,
+                print
+
+    def UpdateConfFiles(self):
+        """
+        Make random changes to any configuration files we may have added.
+        """
+
+        for conf_file_id in self.conf_file_ids:
+            # Update configuration file
+            conf_file_fields = random_conf_file()
+            # Do not update dest so that it remains an override if set
+           if 'dest' in conf_file_fields:
+               del conf_file_fields['dest']
+            self.api.UpdateConfFile(conf_file_id, conf_file_fields)
+
+            if self.check:
+                # Check configuration file
+                conf_file = self.api.GetConfFiles([conf_file_id])[0]
+                for field in conf_file_fields:
+                    assert conf_file[field] == conf_file_fields[field]
+
+            if self.verbose:
+                print "Updated configuration file", conf_file_id
+
+    def DeleteConfFiles(self):
+        """
+        Delete any random configuration files we may have added.
+        """
+
+        for conf_file_id in self.conf_file_ids:
+            self.api.DeleteConfFile(conf_file_id)
+
+            if self.check:
+                assert not self.api.GetConfFiles([conf_file_id])
+
+            if self.verbose:
+                print "Deleted configuration file", conf_file_id
+
+        if self.check:
+            assert not self.api.GetConfFiles(self.conf_file_ids)
+
+        self.conf_file_ids = []
+
+    def AddSliceAttributeTypes(self, n = 10):
+        """
+        Add a number of random slice attribute types.
+        """
+
+        roles = self.api.GetRoles()
+        if not roles:
+            raise Exception, "No roles"
+        role_ids = [role['role_id'] for role in roles]
+
+        for i in range(n):
+            attribute_type_fields = random_attribute_type(role_ids)
+            attribute_type_id = self.api.AddSliceAttributeType(attribute_type_fields)
+
+            # Should return a unique attribute_type_id
+            assert attribute_type_id not in self.attribute_type_ids
+            self.attribute_type_ids.append(attribute_type_id)
+
+            if self.check:
+                # Check slice attribute type
+                attribute_type = self.api.GetSliceAttributeTypes([attribute_type_id])[0]
+                for field in attribute_type_fields:
+                    assert attribute_type[field] == attribute_type_fields[field]
+
+            if self.verbose:
+                print "Added slice attribute type", attribute_type_id
+
+    def UpdateSliceAttributeTypes(self):
+        """
+        Make random changes to any slice attribute types we may have added.
+        """
+
+        roles = self.api.GetRoles()
+        if not roles:
+            raise Exception, "No roles"
+        role_ids = [role['role_id'] for role in roles]
+
+        for attribute_type_id in self.attribute_type_ids:
+            # Update slice attribute type
+            attribute_type_fields = random_attribute_type(role_ids)
+            self.api.UpdateSliceAttributeType(attribute_type_id, attribute_type_fields)
+
+            if self.check:
+                # Check slice attribute type
+                attribute_type = self.api.GetSliceAttributeTypes([attribute_type_id])[0]
+                for field in attribute_type_fields:
+                    assert attribute_type[field] == attribute_type_fields[field]
+
+            if self.verbose:
+                print "Updated slice attribute type", attribute_type_id
+
+    def DeleteSliceAttributeTypes(self):
+        """
+        Delete any random slice attribute types we may have added.
+        """
+
+        for attribute_type_id in self.attribute_type_ids:
+            self.api.DeleteSliceAttributeType(attribute_type_id)
+
+            if self.check:
+                assert not self.api.GetSliceAttributeTypes([attribute_type_id])
+
+            if self.verbose:
+                print "Deleted slice attribute type", attribute_type_id
+
+        if self.check:
+            assert not self.api.GetSliceAttributeTypes(self.attribute_type_ids)
+
+        self.attribute_type_ids = []
+
+    def AddSlices(self, per_site = 10):
+        """
+        Add a number of random slices per site.
+        """
+
+        for site in self.api.GetSites(self.site_ids):
+            for i in range(min(per_site, site['max_slices'])):
+                # Add slice
+                slice_fields = random_slice(site['login_base'])
+                slice_id = self.api.AddSlice(slice_fields)
+
+                # Should return a unique slice_id
+                assert slice_id not in self.slice_ids
+                self.slice_ids.append(slice_id)
+
+                # Add slice to a random set of nodes
+                node_ids = random.sample(self.node_ids, randint(0, len(self.node_ids)))
+                if node_ids:
+                    self.api.AddSliceToNodes(slice_id, node_ids)
+
+                # Add random set of site users to slice
+                person_ids = random.sample(site['person_ids'], randint(0, len(site['person_ids'])))
+                for person_id in person_ids:
+                    self.api.AddPersonToSlice(person_id, slice_id)
+
+                if self.check:
+                    # Check slice
+                    slice = self.api.GetSlices([slice_id])[0]
+                    for field in slice_fields:
+                        assert slice[field] == slice_fields[field]
+
+                    assert set(node_ids) == set(slice['node_ids'])
+                    assert set(person_ids) == set(slice['person_ids'])
+
+                if self.verbose:
+                    print "Added slice", slice_id, "to site", site['site_id'],
+                    if node_ids:
+                        print "and nodes", node_ids,
+                    print
+                    if person_ids:
+                        print "Added users", site['person_ids'], "to slice", slice_id
+
+    def UpdateSlices(self):
+        """
+        Make random changes to any slices we may have added.
+        """
+
+        for slice_id in self.slice_ids:
+            # Update slice
+            slice_fields = random_slice("unused")
+            # Cannot change slice name
+           if 'name' in slice_fields:
+               del slice_fields['name']
+            self.api.UpdateSlice(slice_id, slice_fields)
+
+            slice = self.api.GetSlices([slice_id])[0]
+
+            # Add slice to a random set of nodes
+            node_ids = random.sample(self.node_ids, randint(0, len(self.node_ids)))
+            self.api.AddSliceToNodes(slice_id, list(set(node_ids) - set(slice['node_ids'])))
+            self.api.DeleteSliceFromNodes(slice_id, list(set(slice['node_ids']) - set(node_ids)))
+
+            # Add random set of users to slice
+            person_ids = random.sample(self.person_ids, randint(0, len(self.person_ids)))
+            for person_id in (set(person_ids) - set(slice['person_ids'])):
+                self.api.AddPersonToSlice(person_id, slice_id)
+            for person_id in (set(slice['person_ids']) - set(person_ids)):
+                self.api.DeletePersonFromSlice(person_id, slice_id)
+
+            if self.check:
+                slice = self.api.GetSlices([slice_id])[0]
+                for field in slice_fields:
+                    assert slice[field] == slice_fields[field]
+                assert set(node_ids) == set(slice['node_ids'])
+                assert set(person_ids) == set(slice['person_ids'])
+
+            if self.verbose:
+                print "Updated slice", slice_id
+                print "Added nodes", node_ids, "to slice", slice_id
+                print "Added persons", person_ids, "to slice", slice_id
+
+    def DeleteSlices(self):
+        """
+        Delete any random slices we may have added.
+        """
+
+        for slice_id in self.slice_ids:
+            self.api.DeleteSlice(slice_id)
+
+            if self.check:
+                assert not self.api.GetSlices([slice_id])
+
+            if self.verbose:
+                print "Deleted slice", slice_id
+
+        if self.check:
+            assert not self.api.GetSlices(self.slice_ids)
+
+        self.slice_ids = []
+
+    def AddSliceAttributes(self, per_slice = 2):
+        """
+        Add a number of random slices per site.
+        """
+
+        if not self.attribute_type_ids:
+            return
+
+        for slice_id in self.slice_ids:
+            slice = self.api.GetSlices([slice_id])[0]
+
+            for i in range(per_slice):
+                # Set a random slice/sliver attribute
+                for attribute_type_id in random.sample(self.attribute_type_ids, 1):
+                    value = randstr(16, letters + '_' + digits)
+                    # Make it a sliver attribute with 50% probability
+                    if slice['node_ids']:
+                        node_id = random.sample(slice['node_ids'] + [None] * len(slice['node_ids']), 1)[0]
+                    else:
+                        node_id = None
+
+                    # Add slice attribute
+                    if node_id is None:
+                        slice_attribute_id = self.api.AddSliceAttribute(slice_id, attribute_type_id, value)
+                    else:
+                        slice_attribute_id = self.api.AddSliceAttribute(slice_id, attribute_type_id, value, node_id)
+
+                    # Should return a unique slice_attribute_id
+                    assert slice_attribute_id not in self.slice_attribute_ids
+                    self.slice_attribute_ids.append(slice_attribute_id)
+
+                    if self.check:
+                        # Check slice attribute
+                        slice_attribute = self.api.GetSliceAttributes([slice_attribute_id])[0]
+                        for field in 'attribute_type_id', 'slice_id', 'node_id', 'slice_attribute_id', 'value':
+                            assert slice_attribute[field] == locals()[field]
+
+                    if self.verbose:
+                        print "Added slice attribute", slice_attribute_id, "of type", attribute_type_id,
+                        if node_id is not None:
+                            print "to node", node_id,
+                        print
+                        
+    def UpdateSliceAttributes(self):
+        """
+        Make random changes to any slice attributes we may have added.
+        """
+
+        for slice_attribute_id in self.slice_attribute_ids:
+            # Update slice attribute
+            value = randstr(16, letters + '_' + digits)
+            self.api.UpdateSliceAttribute(slice_attribute_id, value)
+
+            # Check slice attribute again
+            slice_attribute = self.api.GetSliceAttributes([slice_attribute_id])[0]
+            assert slice_attribute['value'] == value
+
+            if self.verbose:
+                print "Updated slice attribute", slice_attribute_id
+
+    def DeleteSliceAttributes(self):
+        """
+        Delete any random slice attributes we may have added.
+        """
+
+        for slice_attribute_id in self.slice_attribute_ids:
+            self.api.DeleteSliceAttribute(slice_attribute_id)
+
+            if self.check:
+                assert not self.api.GetSliceAttributes([slice_attribute_id])
+
+            if self.verbose:
+                print "Deleted slice attribute", slice_attribute_id
+
+        if self.check:
+            assert not self.api.GetSliceAttributes(self.slice_attribute_ids)
+
+        self.slice_attribute_ids = []
+
+def main():
+    parser = OptionParser()
+    parser.add_option("-c", "--check", action = "store_true", default = False, help = "Check most actions (default: %default)")
+    parser.add_option("-q", "--quiet", action = "store_true", default = False, help = "Be quiet (default: %default)")
+    parser.add_option("-t", "--tiny", action = "store_true", default = False, help = "Run a tiny test (default: %default)")
+    (options, args) = parser.parse_args()
+
+    test = Test(api = Shell(),
+                check = options.check,
+                verbose = not options.quiet)
+
+    if options.tiny:
+        params = Test.tiny
+    else:
+        params = Test.default
+
+    test.Run(**params)
+
+if __name__ == "__main__":
+    main()
diff --git a/PLC/__init__.py b/PLC/__init__.py
new file mode 100644 (file)
index 0000000..d5ddda8
--- /dev/null
@@ -0,0 +1,48 @@
+all = """
+Addresses
+AddressTypes
+API
+Auth
+Boot
+BootStates
+ConfFiles
+Config
+Debug
+EventObjects
+Events
+Faults
+Filter
+GPG
+InitScripts
+Keys
+KeyTypes
+Messages
+Method
+NetworkMethods
+NetworkTypes
+NodeGroups
+NodeNetworkSettings
+NodeNetworkSettingTypes
+NodeNetworks
+Nodes
+Parameter
+PCUProtocolTypes
+PCUs
+PCUTypes
+Peers
+Persons
+POD
+PostgreSQL
+PyCurl
+Roles
+sendmail
+Sessions
+Shell
+Sites
+SliceAttributes
+SliceAttributeTypes
+SliceInstantiations
+Slices
+Table
+Test
+""".split()
diff --git a/PLC/sendmail.py b/PLC/sendmail.py
new file mode 100644 (file)
index 0000000..cab0ae6
--- /dev/null
@@ -0,0 +1,98 @@
+import os
+import sys
+import pprint
+from types import StringTypes
+from email.MIMEText import MIMEText
+from email.Header import Header
+from smtplib import SMTP
+
+from PLC.Debug import log
+from PLC.Faults import *
+
+def sendmail(api, To, Subject, Body, From = None, Cc = None, Bcc = None):
+    """
+    Uses sendmail (must be installed and running locally) to send a
+    message to the specified recipients. If the API is running under
+    mod_python, the apache user must be listed in e.g.,
+    /etc/mail/trusted-users.
+
+    To, Cc, and Bcc may be addresses or lists of addresses. Each
+    address may be either a plain text address or a tuple of (name,
+    address).
+    """
+
+    # Fix up defaults
+    if not isinstance(To, list):
+        To = [To]
+    if Cc is not None and not isinstance(Cc, list):
+        Cc = [Cc]
+    if Bcc is not None and not isinstance(Bcc, list):
+        Bcc = [Bcc]
+    if From is None:
+        From = ("%s Support" % api.config.PLC_NAME,
+                api.config.PLC_MAIL_SUPPORT_ADDRESS)
+
+    # Create a MIME-encoded UTF-8 message
+    msg = MIMEText(Body.encode(api.encoding), _charset = api.encoding)
+
+    # Unicode subject headers are automatically encoded correctly
+    msg['Subject'] = Subject
+
+    def encode_addresses(addresses, header_name = None):
+        """
+        Unicode address headers are automatically encoded by
+        email.Header, but not correctly. The correct way is to put the
+        textual name inside quotes and the address inside brackets:
+
+        To: "=?utf-8?b?encoded" <recipient@domain>
+
+        Each address in addrs may be a tuple of (name, address) or
+        just an address. Returns a tuple of (header, addrlist)
+        representing the encoded header text and the list of plain
+        text addresses.
+        """
+
+        header = []
+        addrs = []
+
+        for addr in addresses:
+            if isinstance(addr, tuple):
+                (name, addr) = addr
+                try:
+                    name = name.encode('ascii')
+                    header.append('%s <%s>' % (name, addr))
+                except:
+                    h = Header(name, charset = api.encoding, header_name = header_name)
+                    header.append('"%s" <%s>' % (h.encode(), addr))
+            else:
+                header.append(addr)
+            addrs.append(addr)
+
+        return (", ".join(header), addrs)
+
+    (msg['From'], from_addrs) = encode_addresses([From], 'From')
+    (msg['To'], to_addrs) = encode_addresses(To, 'To')
+
+    if Cc is not None:
+        (msg['Cc'], cc_addrs) = encode_addresses(Cc, 'Cc')
+        to_addrs += cc_addrs
+
+    if Bcc is not None:
+        (unused, bcc_addrs) = encode_addresses(Bcc, 'Bcc')
+        to_addrs += bcc_addrs
+
+    # Needed to pass some spam filters
+    msg['Reply-To'] = msg['From']
+    msg['X-Mailer'] = "Python/" + sys.version.split(" ")[0]
+
+    if not api.config.PLC_MAIL_ENABLED:
+        print >> log, "From: %(From)s, To: %(To)s, Subject: %(Subject)s" % msg
+        return
+
+    s = SMTP()
+    s.connect()
+    rejected = s.sendmail(from_addrs[0], to_addrs, msg.as_string(), rcpt_options = ["NOTIFY=NEVER"])
+    s.close()
+
+    if rejected:
+        raise PLCAPIError, "Error sending message to " + ", ".join(rejected.keys())