From 3d2ecaa0c2da3f5848d4d0ed51a30238cb2201ff Mon Sep 17 00:00:00 2001 From: Tony Mack Date: Fri, 10 Oct 2008 19:10:15 +0000 Subject: [PATCH] starting new development to support slice conf files --- PLC/.cvsignore | 1 + PLC/API.py | 174 ++ PLC/AddressTypes.py | 66 + PLC/Addresses.py | 99 ++ PLC/Auth.py | 332 ++++ PLC/Boot.py | 61 + PLC/BootStates.py | 53 + PLC/ConfFiles.py | 155 ++ PLC/Config.py | 96 ++ PLC/Debug.py | 54 + PLC/EventObjects.py | 63 + PLC/Events.py | 79 + PLC/Faults.py | 67 + PLC/Filter.py | 203 +++ PLC/GPG.py | 183 +++ PLC/InitScripts.py | 66 + PLC/KeyTypes.py | 53 + PLC/Keys.py | 122 ++ PLC/Messages.py | 50 + PLC/Method.py | 372 +++++ PLC/Methods/.cvsignore | 1 + PLC/Methods/AddAddressType.py | 36 + PLC/Methods/AddAddressTypeToAddress.py | 47 + PLC/Methods/AddBootState.py | 29 + PLC/Methods/AddConfFile.py | 37 + PLC/Methods/AddConfFileToNode.py | 51 + PLC/Methods/AddConfFileToNodeGroup.py | 50 + PLC/Methods/AddInitScript.py | 37 + PLC/Methods/AddKeyType.py | 29 + PLC/Methods/AddMessage.py | 29 + PLC/Methods/AddNetworkMethod.py | 29 + PLC/Methods/AddNetworkType.py | 29 + PLC/Methods/AddNode.py | 66 + PLC/Methods/AddNodeGroup.py | 39 + PLC/Methods/AddNodeNetwork.py | 73 + PLC/Methods/AddNodeNetworkSetting.py | 89 + PLC/Methods/AddNodeNetworkSettingType.py | 45 + PLC/Methods/AddNodeToNodeGroup.py | 55 + PLC/Methods/AddNodeToPCU.py | 74 + PLC/Methods/AddPCU.py | 61 + PLC/Methods/AddPCUProtocolType.py | 55 + PLC/Methods/AddPCUType.py | 35 + PLC/Methods/AddPeer.py | 36 + PLC/Methods/AddPerson.py | 43 + PLC/Methods/AddPersonKey.py | 59 + PLC/Methods/AddPersonToSite.py | 56 + PLC/Methods/AddPersonToSlice.py | 61 + PLC/Methods/AddRole.py | 32 + PLC/Methods/AddRoleToPerson.py | 66 + PLC/Methods/AddSession.py | 37 + PLC/Methods/AddSite.py | 41 + PLC/Methods/AddSiteAddress.py | 58 + PLC/Methods/AddSlice.py | 80 + PLC/Methods/AddSliceAttribute.py | 113 ++ PLC/Methods/AddSliceAttributeType.py | 38 + PLC/Methods/AddSliceInstantiation.py | 29 + PLC/Methods/AddSliceToNodes.py | 69 + PLC/Methods/AddSliceToNodesWhitelist.py | 54 + PLC/Methods/AdmAddAddressType.py | 21 + PLC/Methods/AdmAddNode.py | 33 + PLC/Methods/AdmAddNodeGroup.py | 22 + PLC/Methods/AdmAddNodeNetwork.py | 31 + PLC/Methods/AdmAddNodeToNodeGroup.py | 8 + PLC/Methods/AdmAddPerson.py | 30 + PLC/Methods/AdmAddPersonKey.py | 28 + PLC/Methods/AdmAddPersonToSite.py | 8 + PLC/Methods/AdmAddSite.py | 32 + PLC/Methods/AdmAddSitePowerControlUnit.py | 8 + .../AdmAssociateNodeToPowerControlUnitPort.py | 29 + PLC/Methods/AdmAuthCheck.py | 8 + PLC/Methods/AdmDeleteAddressType.py | 8 + PLC/Methods/AdmDeleteAllPersonKeys.py | 55 + PLC/Methods/AdmDeleteNode.py | 9 + PLC/Methods/AdmDeleteNodeGroup.py | 8 + PLC/Methods/AdmDeleteNodeNetwork.py | 24 + PLC/Methods/AdmDeletePerson.py | 8 + PLC/Methods/AdmDeletePersonKeys.py | 56 + PLC/Methods/AdmDeleteSite.py | 8 + PLC/Methods/AdmDeleteSitePowerControlUnit.py | 8 + .../AdmDisassociatePowerControlUnitPort.py | 37 + PLC/Methods/AdmGenerateNodeConfFile.py | 110 ++ PLC/Methods/AdmGetAllAddressTypes.py | 8 + PLC/Methods/AdmGetAllKeyTypes.py | 8 + PLC/Methods/AdmGetAllNodeNetworks.py | 37 + PLC/Methods/AdmGetAllRoles.py | 32 + PLC/Methods/AdmGetNodeGroupNodes.py | 36 + PLC/Methods/AdmGetNodeGroups.py | 8 + PLC/Methods/AdmGetNodes.py | 11 + PLC/Methods/AdmGetPersonKeys.py | 40 + PLC/Methods/AdmGetPersonRoles.py | 55 + PLC/Methods/AdmGetPersonSites.py | 47 + PLC/Methods/AdmGetPersons.py | 11 + PLC/Methods/AdmGetPowerControlUnitNodes.py | 41 + PLC/Methods/AdmGetPowerControlUnits.py | 8 + PLC/Methods/AdmGetSiteNodes.py | 44 + PLC/Methods/AdmGetSitePIs.py | 44 + PLC/Methods/AdmGetSitePersons.py | 44 + PLC/Methods/AdmGetSitePowerControlUnits.py | 35 + PLC/Methods/AdmGetSiteTechContacts.py | 45 + PLC/Methods/AdmGetSites.py | 11 + PLC/Methods/AdmGrantRoleToPerson.py | 11 + PLC/Methods/AdmIsPersonInRole.py | 67 + PLC/Methods/AdmQueryConfFile.py | 35 + PLC/Methods/AdmQueryNode.py | 67 + PLC/Methods/AdmQueryPerson.py | 29 + PLC/Methods/AdmQueryPowerControlUnit.py | 59 + PLC/Methods/AdmQuerySite.py | 87 + PLC/Methods/AdmRebootNode.py | 8 + PLC/Methods/AdmRemoveNodeFromNodeGroup.py | 8 + PLC/Methods/AdmRemovePersonFromSite.py | 8 + PLC/Methods/AdmRevokeRoleFromPerson.py | 11 + PLC/Methods/AdmSetPersonEnabled.py | 23 + PLC/Methods/AdmSetPersonPrimarySite.py | 8 + PLC/Methods/AdmUpdateNode.py | 8 + PLC/Methods/AdmUpdateNodeGroup.py | 27 + PLC/Methods/AdmUpdateNodeNetwork.py | 8 + PLC/Methods/AdmUpdatePerson.py | 8 + PLC/Methods/AdmUpdateSite.py | 8 + PLC/Methods/AdmUpdateSitePowerControlUnit.py | 8 + PLC/Methods/AnonAdmGetNodeGroups.py | 11 + PLC/Methods/AuthCheck.py | 16 + PLC/Methods/BlacklistKey.py | 42 + PLC/Methods/BootCheckAuthentication.py | 8 + PLC/Methods/BootGetNodeDetails.py | 55 + PLC/Methods/BootNotifyOwners.py | 32 + PLC/Methods/BootUpdateNode.py | 64 + PLC/Methods/DeleteAddress.py | 43 + PLC/Methods/DeleteAddressType.py | 33 + PLC/Methods/DeleteAddressTypeFromAddress.py | 48 + PLC/Methods/DeleteBootState.py | 35 + PLC/Methods/DeleteConfFile.py | 33 + PLC/Methods/DeleteConfFileFromNode.py | 48 + PLC/Methods/DeleteConfFileFromNodeGroup.py | 49 + PLC/Methods/DeleteInitScript.py | 33 + PLC/Methods/DeleteKey.py | 46 + PLC/Methods/DeleteKeyType.py | 34 + PLC/Methods/DeleteMessage.py | 34 + PLC/Methods/DeleteNetworkMethod.py | 35 + PLC/Methods/DeleteNetworkType.py | 35 + PLC/Methods/DeleteNode.py | 52 + PLC/Methods/DeleteNodeFromNodeGroup.py | 53 + PLC/Methods/DeleteNodeFromPCU.py | 65 + PLC/Methods/DeleteNodeGroup.py | 41 + PLC/Methods/DeleteNodeNetwork.py | 57 + PLC/Methods/DeleteNodeNetworkSetting.py | 73 + PLC/Methods/DeleteNodeNetworkSettingType.py | 39 + PLC/Methods/DeletePCU.py | 43 + PLC/Methods/DeletePCUProtocolType.py | 33 + PLC/Methods/DeletePCUType.py | 33 + PLC/Methods/DeletePeer.py | 38 + PLC/Methods/DeletePerson.py | 51 + PLC/Methods/DeletePersonFromSite.py | 56 + PLC/Methods/DeletePersonFromSlice.py | 59 + PLC/Methods/DeleteRole.py | 38 + PLC/Methods/DeleteRoleFromPerson.py | 67 + PLC/Methods/DeleteSession.py | 30 + PLC/Methods/DeleteSite.py | 46 + PLC/Methods/DeleteSlice.py | 48 + PLC/Methods/DeleteSliceAttribute.py | 59 + PLC/Methods/DeleteSliceAttributeType.py | 34 + PLC/Methods/DeleteSliceFromNodes.py | 58 + PLC/Methods/DeleteSliceFromNodesWhitelist.py | 54 + PLC/Methods/DeleteSliceInstantiation.py | 34 + PLC/Methods/GenerateNodeConfFile.py | 107 ++ PLC/Methods/GetAddressTypes.py | 32 + PLC/Methods/GetAddresses.py | 30 + PLC/Methods/GetBootMedium.py | 470 ++++++ PLC/Methods/GetBootStates.py | 22 + PLC/Methods/GetConfFiles.py | 31 + PLC/Methods/GetEventObjects.py | 29 + PLC/Methods/GetEvents.py | 30 + PLC/Methods/GetInitScripts.py | 31 + PLC/Methods/GetKeyTypes.py | 22 + PLC/Methods/GetKeys.py | 41 + PLC/Methods/GetMessages.py | 31 + PLC/Methods/GetNetworkMethods.py | 22 + PLC/Methods/GetNetworkTypes.py | 22 + PLC/Methods/GetNodeGroups.py | 30 + PLC/Methods/GetNodeNetworkSettingTypes.py | 33 + PLC/Methods/GetNodeNetworkSettings.py | 45 + PLC/Methods/GetNodeNetworks.py | 32 + PLC/Methods/GetNodes.py | 83 + PLC/Methods/GetPCUProtocolTypes.py | 40 + PLC/Methods/GetPCUTypes.py | 50 + PLC/Methods/GetPCUs.py | 73 + PLC/Methods/GetPeerData.py | 87 + PLC/Methods/GetPeerName.py | 19 + PLC/Methods/GetPeers.py | 47 + PLC/Methods/GetPersons.py | 87 + PLC/Methods/GetPlcRelease.py | 58 + PLC/Methods/GetRoles.py | 21 + PLC/Methods/GetSession.py | 39 + PLC/Methods/GetSessions.py | 35 + PLC/Methods/GetSites.py | 31 + PLC/Methods/GetSliceAttributeTypes.py | 30 + PLC/Methods/GetSliceAttributes.py | 88 + PLC/Methods/GetSliceInstantiations.py | 21 + PLC/Methods/GetSliceKeys.py | 134 ++ PLC/Methods/GetSliceTicket.py | 77 + PLC/Methods/GetSlices.py | 75 + PLC/Methods/GetSlicesMD5.py | 30 + PLC/Methods/GetSlivers.py | 227 +++ PLC/Methods/GetWhitelist.py | 73 + PLC/Methods/NotifyPersons.py | 48 + PLC/Methods/NotifySupport.py | 36 + PLC/Methods/RebootNode.py | 73 + PLC/Methods/RefreshPeer.py | 476 ++++++ PLC/Methods/ResetPassword.py | 128 ++ PLC/Methods/SetPersonPrimarySite.py | 62 + PLC/Methods/SliceCreate.py | 25 + PLC/Methods/SliceDelete.py | 29 + PLC/Methods/SliceExtendedInfo.py | 84 + PLC/Methods/SliceGetTicket.py | 249 +++ PLC/Methods/SliceInfo.py | 75 + PLC/Methods/SliceListNames.py | 45 + PLC/Methods/SliceListUserSlices.py | 47 + PLC/Methods/SliceNodesAdd.py | 29 + PLC/Methods/SliceNodesDel.py | 29 + PLC/Methods/SliceNodesList.py | 40 + PLC/Methods/SliceRenew.py | 34 + PLC/Methods/SliceTicketGet.py | 13 + PLC/Methods/SliceUpdate.py | 37 + PLC/Methods/SliceUserAdd.py | 32 + PLC/Methods/SliceUserDel.py | 35 + PLC/Methods/SliceUsersList.py | 45 + PLC/Methods/UpdateAddress.py | 54 + PLC/Methods/UpdateAddressType.py | 42 + PLC/Methods/UpdateConfFile.py | 42 + PLC/Methods/UpdateInitScript.py | 42 + PLC/Methods/UpdateKey.py | 55 + PLC/Methods/UpdateMessage.py | 43 + PLC/Methods/UpdateNode.py | 81 + PLC/Methods/UpdateNodeGroup.py | 54 + PLC/Methods/UpdateNodeNetwork.py | 69 + PLC/Methods/UpdateNodeNetworkSetting.py | 72 + PLC/Methods/UpdateNodeNetworkSettingType.py | 48 + PLC/Methods/UpdatePCU.py | 52 + PLC/Methods/UpdatePCUProtocolType.py | 41 + PLC/Methods/UpdatePCUType.py | 42 + PLC/Methods/UpdatePeer.py | 50 + PLC/Methods/UpdatePerson.py | 90 + PLC/Methods/UpdateSite.py | 79 + PLC/Methods/UpdateSlice.py | 107 ++ PLC/Methods/UpdateSliceAttribute.py | 65 + PLC/Methods/UpdateSliceAttributeType.py | 43 + PLC/Methods/VerifyPerson.py | 156 ++ PLC/Methods/__init__.py | 231 +++ PLC/Methods/system/.cvsignore | 1 + PLC/Methods/system/__init__.py | 0 PLC/Methods/system/listMethods.py | 20 + PLC/Methods/system/methodHelp.py | 20 + PLC/Methods/system/methodSignature.py | 60 + PLC/Methods/system/multicall.py | 54 + PLC/NetworkMethods.py | 53 + PLC/NetworkTypes.py | 53 + PLC/NodeGroups.py | 182 ++ PLC/NodeNetworkSettingTypes.py | 83 + PLC/NodeNetworkSettings.py | 57 + PLC/NodeNetworks.py | 231 +++ PLC/Nodes.py | 326 ++++ PLC/PCUProtocolTypes.py | 75 + PLC/PCUTypes.py | 104 ++ PLC/PCUs.py | 116 ++ PLC/POD.py | 90 + PLC/Parameter.py | 105 ++ PLC/Peers.py | 235 +++ PLC/Persons.py | 501 ++++++ PLC/PostgreSQL.py | 263 +++ PLC/PyCurl.py | 82 + PLC/Roles.py | 72 + PLC/Sessions.py | 91 + PLC/Shell.py | 260 +++ PLC/Sites.py | 271 +++ PLC/SliceAttributeTypes.py | 72 + PLC/SliceAttributes.py | 46 + PLC/SliceInstantiations.py | 53 + PLC/Slices.py | 296 ++++ PLC/Table.py | 328 ++++ PLC/Test.py | 1460 +++++++++++++++++ PLC/__init__.py | 48 + PLC/sendmail.py | 98 ++ 281 files changed, 19274 insertions(+) create mode 100644 PLC/.cvsignore create mode 100644 PLC/API.py create mode 100644 PLC/AddressTypes.py create mode 100644 PLC/Addresses.py create mode 100644 PLC/Auth.py create mode 100644 PLC/Boot.py create mode 100644 PLC/BootStates.py create mode 100644 PLC/ConfFiles.py create mode 100644 PLC/Config.py create mode 100644 PLC/Debug.py create mode 100644 PLC/EventObjects.py create mode 100644 PLC/Events.py create mode 100644 PLC/Faults.py create mode 100644 PLC/Filter.py create mode 100644 PLC/GPG.py create mode 100644 PLC/InitScripts.py create mode 100644 PLC/KeyTypes.py create mode 100644 PLC/Keys.py create mode 100644 PLC/Messages.py create mode 100644 PLC/Method.py create mode 100644 PLC/Methods/.cvsignore create mode 100644 PLC/Methods/AddAddressType.py create mode 100644 PLC/Methods/AddAddressTypeToAddress.py create mode 100644 PLC/Methods/AddBootState.py create mode 100644 PLC/Methods/AddConfFile.py create mode 100644 PLC/Methods/AddConfFileToNode.py create mode 100644 PLC/Methods/AddConfFileToNodeGroup.py create mode 100644 PLC/Methods/AddInitScript.py create mode 100644 PLC/Methods/AddKeyType.py create mode 100644 PLC/Methods/AddMessage.py create mode 100644 PLC/Methods/AddNetworkMethod.py create mode 100644 PLC/Methods/AddNetworkType.py create mode 100644 PLC/Methods/AddNode.py create mode 100644 PLC/Methods/AddNodeGroup.py create mode 100644 PLC/Methods/AddNodeNetwork.py create mode 100644 PLC/Methods/AddNodeNetworkSetting.py create mode 100644 PLC/Methods/AddNodeNetworkSettingType.py create mode 100644 PLC/Methods/AddNodeToNodeGroup.py create mode 100644 PLC/Methods/AddNodeToPCU.py create mode 100644 PLC/Methods/AddPCU.py create mode 100644 PLC/Methods/AddPCUProtocolType.py create mode 100644 PLC/Methods/AddPCUType.py create mode 100644 PLC/Methods/AddPeer.py create mode 100644 PLC/Methods/AddPerson.py create mode 100644 PLC/Methods/AddPersonKey.py create mode 100644 PLC/Methods/AddPersonToSite.py create mode 100644 PLC/Methods/AddPersonToSlice.py create mode 100644 PLC/Methods/AddRole.py create mode 100644 PLC/Methods/AddRoleToPerson.py create mode 100644 PLC/Methods/AddSession.py create mode 100644 PLC/Methods/AddSite.py create mode 100644 PLC/Methods/AddSiteAddress.py create mode 100644 PLC/Methods/AddSlice.py create mode 100644 PLC/Methods/AddSliceAttribute.py create mode 100644 PLC/Methods/AddSliceAttributeType.py create mode 100644 PLC/Methods/AddSliceInstantiation.py create mode 100644 PLC/Methods/AddSliceToNodes.py create mode 100644 PLC/Methods/AddSliceToNodesWhitelist.py create mode 100644 PLC/Methods/AdmAddAddressType.py create mode 100644 PLC/Methods/AdmAddNode.py create mode 100644 PLC/Methods/AdmAddNodeGroup.py create mode 100644 PLC/Methods/AdmAddNodeNetwork.py create mode 100644 PLC/Methods/AdmAddNodeToNodeGroup.py create mode 100644 PLC/Methods/AdmAddPerson.py create mode 100644 PLC/Methods/AdmAddPersonKey.py create mode 100644 PLC/Methods/AdmAddPersonToSite.py create mode 100644 PLC/Methods/AdmAddSite.py create mode 100644 PLC/Methods/AdmAddSitePowerControlUnit.py create mode 100644 PLC/Methods/AdmAssociateNodeToPowerControlUnitPort.py create mode 100644 PLC/Methods/AdmAuthCheck.py create mode 100644 PLC/Methods/AdmDeleteAddressType.py create mode 100644 PLC/Methods/AdmDeleteAllPersonKeys.py create mode 100644 PLC/Methods/AdmDeleteNode.py create mode 100644 PLC/Methods/AdmDeleteNodeGroup.py create mode 100644 PLC/Methods/AdmDeleteNodeNetwork.py create mode 100644 PLC/Methods/AdmDeletePerson.py create mode 100644 PLC/Methods/AdmDeletePersonKeys.py create mode 100644 PLC/Methods/AdmDeleteSite.py create mode 100644 PLC/Methods/AdmDeleteSitePowerControlUnit.py create mode 100644 PLC/Methods/AdmDisassociatePowerControlUnitPort.py create mode 100644 PLC/Methods/AdmGenerateNodeConfFile.py create mode 100644 PLC/Methods/AdmGetAllAddressTypes.py create mode 100644 PLC/Methods/AdmGetAllKeyTypes.py create mode 100644 PLC/Methods/AdmGetAllNodeNetworks.py create mode 100644 PLC/Methods/AdmGetAllRoles.py create mode 100644 PLC/Methods/AdmGetNodeGroupNodes.py create mode 100644 PLC/Methods/AdmGetNodeGroups.py create mode 100644 PLC/Methods/AdmGetNodes.py create mode 100644 PLC/Methods/AdmGetPersonKeys.py create mode 100644 PLC/Methods/AdmGetPersonRoles.py create mode 100644 PLC/Methods/AdmGetPersonSites.py create mode 100644 PLC/Methods/AdmGetPersons.py create mode 100644 PLC/Methods/AdmGetPowerControlUnitNodes.py create mode 100644 PLC/Methods/AdmGetPowerControlUnits.py create mode 100644 PLC/Methods/AdmGetSiteNodes.py create mode 100644 PLC/Methods/AdmGetSitePIs.py create mode 100644 PLC/Methods/AdmGetSitePersons.py create mode 100644 PLC/Methods/AdmGetSitePowerControlUnits.py create mode 100644 PLC/Methods/AdmGetSiteTechContacts.py create mode 100644 PLC/Methods/AdmGetSites.py create mode 100644 PLC/Methods/AdmGrantRoleToPerson.py create mode 100644 PLC/Methods/AdmIsPersonInRole.py create mode 100644 PLC/Methods/AdmQueryConfFile.py create mode 100644 PLC/Methods/AdmQueryNode.py create mode 100644 PLC/Methods/AdmQueryPerson.py create mode 100644 PLC/Methods/AdmQueryPowerControlUnit.py create mode 100644 PLC/Methods/AdmQuerySite.py create mode 100644 PLC/Methods/AdmRebootNode.py create mode 100644 PLC/Methods/AdmRemoveNodeFromNodeGroup.py create mode 100644 PLC/Methods/AdmRemovePersonFromSite.py create mode 100644 PLC/Methods/AdmRevokeRoleFromPerson.py create mode 100644 PLC/Methods/AdmSetPersonEnabled.py create mode 100644 PLC/Methods/AdmSetPersonPrimarySite.py create mode 100644 PLC/Methods/AdmUpdateNode.py create mode 100644 PLC/Methods/AdmUpdateNodeGroup.py create mode 100644 PLC/Methods/AdmUpdateNodeNetwork.py create mode 100644 PLC/Methods/AdmUpdatePerson.py create mode 100644 PLC/Methods/AdmUpdateSite.py create mode 100644 PLC/Methods/AdmUpdateSitePowerControlUnit.py create mode 100644 PLC/Methods/AnonAdmGetNodeGroups.py create mode 100644 PLC/Methods/AuthCheck.py create mode 100644 PLC/Methods/BlacklistKey.py create mode 100644 PLC/Methods/BootCheckAuthentication.py create mode 100644 PLC/Methods/BootGetNodeDetails.py create mode 100644 PLC/Methods/BootNotifyOwners.py create mode 100644 PLC/Methods/BootUpdateNode.py create mode 100644 PLC/Methods/DeleteAddress.py create mode 100644 PLC/Methods/DeleteAddressType.py create mode 100644 PLC/Methods/DeleteAddressTypeFromAddress.py create mode 100644 PLC/Methods/DeleteBootState.py create mode 100644 PLC/Methods/DeleteConfFile.py create mode 100644 PLC/Methods/DeleteConfFileFromNode.py create mode 100644 PLC/Methods/DeleteConfFileFromNodeGroup.py create mode 100644 PLC/Methods/DeleteInitScript.py create mode 100644 PLC/Methods/DeleteKey.py create mode 100644 PLC/Methods/DeleteKeyType.py create mode 100644 PLC/Methods/DeleteMessage.py create mode 100644 PLC/Methods/DeleteNetworkMethod.py create mode 100644 PLC/Methods/DeleteNetworkType.py create mode 100644 PLC/Methods/DeleteNode.py create mode 100644 PLC/Methods/DeleteNodeFromNodeGroup.py create mode 100644 PLC/Methods/DeleteNodeFromPCU.py create mode 100644 PLC/Methods/DeleteNodeGroup.py create mode 100644 PLC/Methods/DeleteNodeNetwork.py create mode 100644 PLC/Methods/DeleteNodeNetworkSetting.py create mode 100644 PLC/Methods/DeleteNodeNetworkSettingType.py create mode 100644 PLC/Methods/DeletePCU.py create mode 100644 PLC/Methods/DeletePCUProtocolType.py create mode 100644 PLC/Methods/DeletePCUType.py create mode 100644 PLC/Methods/DeletePeer.py create mode 100644 PLC/Methods/DeletePerson.py create mode 100644 PLC/Methods/DeletePersonFromSite.py create mode 100644 PLC/Methods/DeletePersonFromSlice.py create mode 100644 PLC/Methods/DeleteRole.py create mode 100644 PLC/Methods/DeleteRoleFromPerson.py create mode 100644 PLC/Methods/DeleteSession.py create mode 100644 PLC/Methods/DeleteSite.py create mode 100644 PLC/Methods/DeleteSlice.py create mode 100644 PLC/Methods/DeleteSliceAttribute.py create mode 100644 PLC/Methods/DeleteSliceAttributeType.py create mode 100644 PLC/Methods/DeleteSliceFromNodes.py create mode 100644 PLC/Methods/DeleteSliceFromNodesWhitelist.py create mode 100644 PLC/Methods/DeleteSliceInstantiation.py create mode 100644 PLC/Methods/GenerateNodeConfFile.py create mode 100644 PLC/Methods/GetAddressTypes.py create mode 100644 PLC/Methods/GetAddresses.py create mode 100644 PLC/Methods/GetBootMedium.py create mode 100644 PLC/Methods/GetBootStates.py create mode 100644 PLC/Methods/GetConfFiles.py create mode 100644 PLC/Methods/GetEventObjects.py create mode 100644 PLC/Methods/GetEvents.py create mode 100644 PLC/Methods/GetInitScripts.py create mode 100644 PLC/Methods/GetKeyTypes.py create mode 100644 PLC/Methods/GetKeys.py create mode 100644 PLC/Methods/GetMessages.py create mode 100644 PLC/Methods/GetNetworkMethods.py create mode 100644 PLC/Methods/GetNetworkTypes.py create mode 100644 PLC/Methods/GetNodeGroups.py create mode 100644 PLC/Methods/GetNodeNetworkSettingTypes.py create mode 100644 PLC/Methods/GetNodeNetworkSettings.py create mode 100644 PLC/Methods/GetNodeNetworks.py create mode 100644 PLC/Methods/GetNodes.py create mode 100644 PLC/Methods/GetPCUProtocolTypes.py create mode 100644 PLC/Methods/GetPCUTypes.py create mode 100644 PLC/Methods/GetPCUs.py create mode 100644 PLC/Methods/GetPeerData.py create mode 100644 PLC/Methods/GetPeerName.py create mode 100644 PLC/Methods/GetPeers.py create mode 100644 PLC/Methods/GetPersons.py create mode 100644 PLC/Methods/GetPlcRelease.py create mode 100644 PLC/Methods/GetRoles.py create mode 100644 PLC/Methods/GetSession.py create mode 100644 PLC/Methods/GetSessions.py create mode 100644 PLC/Methods/GetSites.py create mode 100644 PLC/Methods/GetSliceAttributeTypes.py create mode 100644 PLC/Methods/GetSliceAttributes.py create mode 100644 PLC/Methods/GetSliceInstantiations.py create mode 100644 PLC/Methods/GetSliceKeys.py create mode 100644 PLC/Methods/GetSliceTicket.py create mode 100644 PLC/Methods/GetSlices.py create mode 100644 PLC/Methods/GetSlicesMD5.py create mode 100644 PLC/Methods/GetSlivers.py create mode 100644 PLC/Methods/GetWhitelist.py create mode 100644 PLC/Methods/NotifyPersons.py create mode 100644 PLC/Methods/NotifySupport.py create mode 100644 PLC/Methods/RebootNode.py create mode 100644 PLC/Methods/RefreshPeer.py create mode 100644 PLC/Methods/ResetPassword.py create mode 100644 PLC/Methods/SetPersonPrimarySite.py create mode 100644 PLC/Methods/SliceCreate.py create mode 100644 PLC/Methods/SliceDelete.py create mode 100644 PLC/Methods/SliceExtendedInfo.py create mode 100644 PLC/Methods/SliceGetTicket.py create mode 100644 PLC/Methods/SliceInfo.py create mode 100644 PLC/Methods/SliceListNames.py create mode 100644 PLC/Methods/SliceListUserSlices.py create mode 100644 PLC/Methods/SliceNodesAdd.py create mode 100644 PLC/Methods/SliceNodesDel.py create mode 100644 PLC/Methods/SliceNodesList.py create mode 100644 PLC/Methods/SliceRenew.py create mode 100644 PLC/Methods/SliceTicketGet.py create mode 100644 PLC/Methods/SliceUpdate.py create mode 100644 PLC/Methods/SliceUserAdd.py create mode 100644 PLC/Methods/SliceUserDel.py create mode 100644 PLC/Methods/SliceUsersList.py create mode 100644 PLC/Methods/UpdateAddress.py create mode 100644 PLC/Methods/UpdateAddressType.py create mode 100644 PLC/Methods/UpdateConfFile.py create mode 100644 PLC/Methods/UpdateInitScript.py create mode 100644 PLC/Methods/UpdateKey.py create mode 100644 PLC/Methods/UpdateMessage.py create mode 100644 PLC/Methods/UpdateNode.py create mode 100644 PLC/Methods/UpdateNodeGroup.py create mode 100644 PLC/Methods/UpdateNodeNetwork.py create mode 100644 PLC/Methods/UpdateNodeNetworkSetting.py create mode 100644 PLC/Methods/UpdateNodeNetworkSettingType.py create mode 100644 PLC/Methods/UpdatePCU.py create mode 100644 PLC/Methods/UpdatePCUProtocolType.py create mode 100644 PLC/Methods/UpdatePCUType.py create mode 100644 PLC/Methods/UpdatePeer.py create mode 100644 PLC/Methods/UpdatePerson.py create mode 100644 PLC/Methods/UpdateSite.py create mode 100644 PLC/Methods/UpdateSlice.py create mode 100644 PLC/Methods/UpdateSliceAttribute.py create mode 100644 PLC/Methods/UpdateSliceAttributeType.py create mode 100644 PLC/Methods/VerifyPerson.py create mode 100644 PLC/Methods/__init__.py create mode 100644 PLC/Methods/system/.cvsignore create mode 100644 PLC/Methods/system/__init__.py create mode 100644 PLC/Methods/system/listMethods.py create mode 100644 PLC/Methods/system/methodHelp.py create mode 100644 PLC/Methods/system/methodSignature.py create mode 100644 PLC/Methods/system/multicall.py create mode 100644 PLC/NetworkMethods.py create mode 100644 PLC/NetworkTypes.py create mode 100644 PLC/NodeGroups.py create mode 100644 PLC/NodeNetworkSettingTypes.py create mode 100644 PLC/NodeNetworkSettings.py create mode 100644 PLC/NodeNetworks.py create mode 100644 PLC/Nodes.py create mode 100644 PLC/PCUProtocolTypes.py create mode 100644 PLC/PCUTypes.py create mode 100644 PLC/PCUs.py create mode 100644 PLC/POD.py create mode 100644 PLC/Parameter.py create mode 100644 PLC/Peers.py create mode 100644 PLC/Persons.py create mode 100644 PLC/PostgreSQL.py create mode 100644 PLC/PyCurl.py create mode 100644 PLC/Roles.py create mode 100644 PLC/Sessions.py create mode 100644 PLC/Shell.py create mode 100644 PLC/Sites.py create mode 100644 PLC/SliceAttributeTypes.py create mode 100644 PLC/SliceAttributes.py create mode 100644 PLC/SliceInstantiations.py create mode 100644 PLC/Slices.py create mode 100644 PLC/Table.py create mode 100644 PLC/Test.py create mode 100644 PLC/__init__.py create mode 100644 PLC/sendmail.py diff --git a/PLC/.cvsignore b/PLC/.cvsignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/PLC/.cvsignore @@ -0,0 +1 @@ +*.pyc diff --git a/PLC/API.py b/PLC/API.py new file mode 100644 index 00000000..5c4cb9d5 --- /dev/null +++ b/PLC/API.py @@ -0,0 +1,174 @@ +# +# PLCAPI XML-RPC and SOAP interfaces +# +# Aaron Klingaman +# Mark Huang +# +# 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, "&", "&") + s = replace(s, "<", "<") + s = replace(s, ">", ">",) + + # 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 index 00000000..7156c00b --- /dev/null +++ b/PLC/AddressTypes.py @@ -0,0 +1,66 @@ +# +# Functions for interacting with the address_types table in the database +# +# Mark Huang +# 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 index 00000000..36c0d1dc --- /dev/null +++ b/PLC/Addresses.py @@ -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 index 00000000..498acc56 --- /dev/null +++ b/PLC/Auth.py @@ -0,0 +1,332 @@ +# +# PLCAPI authentication parameters +# +# Mark Huang +# 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 index 00000000..5f253547 --- /dev/null +++ b/PLC/Boot.py @@ -0,0 +1,61 @@ +# +# Boot Manager support +# +# Mark Huang +# 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 index 00000000..014b61a8 --- /dev/null +++ b/PLC/BootStates.py @@ -0,0 +1,53 @@ +# +# Functions for interacting with the boot_states table in the database +# +# Mark Huang +# 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 index 00000000..f15a574a --- /dev/null +++ b/PLC/ConfFiles.py @@ -0,0 +1,155 @@ +# +# Functions for interacting with the conf_files table in the database +# +# Mark Huang +# 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 index 00000000..693ba221 --- /dev/null +++ b/PLC/Config.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# +# PLCAPI configuration store. Supports XML-based configuration file +# format exported by MyPLC. +# +# Mark Huang +# 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 index 00000000..b8dac85e --- /dev/null +++ b/PLC/Debug.py @@ -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 index 00000000..52b44c72 --- /dev/null +++ b/PLC/EventObjects.py @@ -0,0 +1,63 @@ +# +# Functions for interacting with the events table in the database +# +# Tony Mack +# 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 index 00000000..0d319cf2 --- /dev/null +++ b/PLC/Events.py @@ -0,0 +1,79 @@ +# +# Functions for interacting with the events table in the database +# +# Tony Mack +# 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 index 00000000..e3d46e5b --- /dev/null +++ b/PLC/Faults.py @@ -0,0 +1,67 @@ +# +# PLCAPI XML-RPC faults +# +# Aaron Klingaman +# Mark Huang +# +# 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 index 00000000..195c4e67 --- /dev/null +++ b/PLC/Filter.py @@ -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 index 00000000..457c32bb --- /dev/null +++ b/PLC/GPG.py @@ -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 +# 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 index 00000000..9f864d2a --- /dev/null +++ b/PLC/InitScripts.py @@ -0,0 +1,66 @@ +# +# Functions for interacting with the initscripts table in the database +# +# Tony Mack +# 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 index 00000000..920662b0 --- /dev/null +++ b/PLC/KeyTypes.py @@ -0,0 +1,53 @@ +# +# Functions for interacting with the key_types table in the database +# +# Mark Huang +# 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 index 00000000..8d22dc2e --- /dev/null +++ b/PLC/Keys.py @@ -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 index 00000000..0620ac08 --- /dev/null +++ b/PLC/Messages.py @@ -0,0 +1,50 @@ +# +# Functions for interacting with the messages table in the database +# +# Tony Mack +# 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 index 00000000..5e7d09a1 --- /dev/null +++ b/PLC/Method.py @@ -0,0 +1,372 @@ +# +# Base class for all PLCAPI functions +# +# Mark Huang +# 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 index 00000000..0d20b648 --- /dev/null +++ b/PLC/Methods/.cvsignore @@ -0,0 +1 @@ +*.pyc diff --git a/PLC/Methods/AddAddressType.py b/PLC/Methods/AddAddressType.py new file mode 100644 index 00000000..9fc771bd --- /dev/null +++ b/PLC/Methods/AddAddressType.py @@ -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 index 00000000..d69e6276 --- /dev/null +++ b/PLC/Methods/AddAddressTypeToAddress.py @@ -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 index 00000000..fc752545 --- /dev/null +++ b/PLC/Methods/AddBootState.py @@ -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 index 00000000..5604ceff --- /dev/null +++ b/PLC/Methods/AddConfFile.py @@ -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 index 00000000..2d1542c2 --- /dev/null +++ b/PLC/Methods/AddConfFileToNode.py @@ -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 index 00000000..6ff642cf --- /dev/null +++ b/PLC/Methods/AddConfFileToNodeGroup.py @@ -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 index 00000000..8c247cb7 --- /dev/null +++ b/PLC/Methods/AddInitScript.py @@ -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 index 00000000..b3690a86 --- /dev/null +++ b/PLC/Methods/AddKeyType.py @@ -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 index 00000000..62a2da7a --- /dev/null +++ b/PLC/Methods/AddMessage.py @@ -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 index 00000000..11f38456 --- /dev/null +++ b/PLC/Methods/AddNetworkMethod.py @@ -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 index 00000000..65330535 --- /dev/null +++ b/PLC/Methods/AddNetworkType.py @@ -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 index 00000000..8d253df2 --- /dev/null +++ b/PLC/Methods/AddNode.py @@ -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 index 00000000..34f5f972 --- /dev/null +++ b/PLC/Methods/AddNodeGroup.py @@ -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 index 00000000..6e24bce7 --- /dev/null +++ b/PLC/Methods/AddNodeNetwork.py @@ -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 index 00000000..f02670d8 --- /dev/null +++ b/PLC/Methods/AddNodeNetworkSetting.py @@ -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 index 00000000..1c3cc2cd --- /dev/null +++ b/PLC/Methods/AddNodeNetworkSettingType.py @@ -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 index 00000000..a552b112 --- /dev/null +++ b/PLC/Methods/AddNodeToNodeGroup.py @@ -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 index 00000000..c0d5eff9 --- /dev/null +++ b/PLC/Methods/AddNodeToPCU.py @@ -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 index 00000000..3c461946 --- /dev/null +++ b/PLC/Methods/AddPCU.py @@ -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 index 00000000..76dad3bc --- /dev/null +++ b/PLC/Methods/AddPCUProtocolType.py @@ -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 index 00000000..106791af --- /dev/null +++ b/PLC/Methods/AddPCUType.py @@ -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 index 00000000..d6dc5764 --- /dev/null +++ b/PLC/Methods/AddPeer.py @@ -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 index 00000000..cb0aa71c --- /dev/null +++ b/PLC/Methods/AddPerson.py @@ -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 index 00000000..aa4ed00b --- /dev/null +++ b/PLC/Methods/AddPersonKey.py @@ -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 index 00000000..7d5ac100 --- /dev/null +++ b/PLC/Methods/AddPersonToSite.py @@ -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 index 00000000..e3392eb9 --- /dev/null +++ b/PLC/Methods/AddPersonToSlice.py @@ -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 index 00000000..7266180b --- /dev/null +++ b/PLC/Methods/AddRole.py @@ -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 index 00000000..5a8e241e --- /dev/null +++ b/PLC/Methods/AddRoleToPerson.py @@ -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 index 00000000..6f5bc88e --- /dev/null +++ b/PLC/Methods/AddSession.py @@ -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 index 00000000..475c8b13 --- /dev/null +++ b/PLC/Methods/AddSite.py @@ -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 index 00000000..a3a4eff6 --- /dev/null +++ b/PLC/Methods/AddSiteAddress.py @@ -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 index 00000000..6cc056ca --- /dev/null +++ b/PLC/Methods/AddSlice.py @@ -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 index 00000000..ad324374 --- /dev/null +++ b/PLC/Methods/AddSliceAttribute.py @@ -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 index 00000000..095ae83b --- /dev/null +++ b/PLC/Methods/AddSliceAttributeType.py @@ -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 index 00000000..0374957e --- /dev/null +++ b/PLC/Methods/AddSliceInstantiation.py @@ -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 index 00000000..d5a2c8c2 --- /dev/null +++ b/PLC/Methods/AddSliceToNodes.py @@ -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 index 00000000..a6b4bd12 --- /dev/null +++ b/PLC/Methods/AddSliceToNodesWhitelist.py @@ -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 index 00000000..e0cd09dc --- /dev/null +++ b/PLC/Methods/AdmAddAddressType.py @@ -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 index 00000000..dda5c99d --- /dev/null +++ b/PLC/Methods/AdmAddNode.py @@ -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 index 00000000..6bbb59e4 --- /dev/null +++ b/PLC/Methods/AdmAddNodeGroup.py @@ -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 index 00000000..c309a772 --- /dev/null +++ b/PLC/Methods/AdmAddNodeNetwork.py @@ -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 index 00000000..dc7eab4e --- /dev/null +++ b/PLC/Methods/AdmAddNodeToNodeGroup.py @@ -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 index 00000000..2b90f61a --- /dev/null +++ b/PLC/Methods/AdmAddPerson.py @@ -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 index 00000000..05d0a69d --- /dev/null +++ b/PLC/Methods/AdmAddPersonKey.py @@ -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 index 00000000..948b06ff --- /dev/null +++ b/PLC/Methods/AdmAddPersonToSite.py @@ -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 index 00000000..929c0a73 --- /dev/null +++ b/PLC/Methods/AdmAddSite.py @@ -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 index 00000000..e9e452ef --- /dev/null +++ b/PLC/Methods/AdmAddSitePowerControlUnit.py @@ -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 index 00000000..9e955be3 --- /dev/null +++ b/PLC/Methods/AdmAssociateNodeToPowerControlUnitPort.py @@ -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 index 00000000..63defa57 --- /dev/null +++ b/PLC/Methods/AdmAuthCheck.py @@ -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 index 00000000..12f0625f --- /dev/null +++ b/PLC/Methods/AdmDeleteAddressType.py @@ -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 index 00000000..9f038f93 --- /dev/null +++ b/PLC/Methods/AdmDeleteAllPersonKeys.py @@ -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 index 00000000..2ec9ff1a --- /dev/null +++ b/PLC/Methods/AdmDeleteNode.py @@ -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 index 00000000..b5b2cb6c --- /dev/null +++ b/PLC/Methods/AdmDeleteNodeGroup.py @@ -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 index 00000000..d5665047 --- /dev/null +++ b/PLC/Methods/AdmDeleteNodeNetwork.py @@ -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 index 00000000..ff29e8bd --- /dev/null +++ b/PLC/Methods/AdmDeletePerson.py @@ -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 index 00000000..fd24eefc --- /dev/null +++ b/PLC/Methods/AdmDeletePersonKeys.py @@ -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 index 00000000..7501ad5a --- /dev/null +++ b/PLC/Methods/AdmDeleteSite.py @@ -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 index 00000000..2865224a --- /dev/null +++ b/PLC/Methods/AdmDeleteSitePowerControlUnit.py @@ -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 index 00000000..5f7c4484 --- /dev/null +++ b/PLC/Methods/AdmDisassociatePowerControlUnitPort.py @@ -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 index 00000000..85789bd7 --- /dev/null +++ b/PLC/Methods/AdmGenerateNodeConfFile.py @@ -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 index 00000000..ca4748b2 --- /dev/null +++ b/PLC/Methods/AdmGetAllAddressTypes.py @@ -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 index 00000000..4383f847 --- /dev/null +++ b/PLC/Methods/AdmGetAllKeyTypes.py @@ -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 index 00000000..c00bdec4 --- /dev/null +++ b/PLC/Methods/AdmGetAllNodeNetworks.py @@ -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 index 00000000..2b887145 --- /dev/null +++ b/PLC/Methods/AdmGetAllRoles.py @@ -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 index 00000000..51c392a0 --- /dev/null +++ b/PLC/Methods/AdmGetNodeGroupNodes.py @@ -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 index 00000000..fa1ad599 --- /dev/null +++ b/PLC/Methods/AdmGetNodeGroups.py @@ -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 index 00000000..74d84892 --- /dev/null +++ b/PLC/Methods/AdmGetNodes.py @@ -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 index 00000000..946230a3 --- /dev/null +++ b/PLC/Methods/AdmGetPersonKeys.py @@ -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 index 00000000..024b93ce --- /dev/null +++ b/PLC/Methods/AdmGetPersonRoles.py @@ -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 index 00000000..79324f8b --- /dev/null +++ b/PLC/Methods/AdmGetPersonSites.py @@ -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 index 00000000..35e94a0e --- /dev/null +++ b/PLC/Methods/AdmGetPersons.py @@ -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 index 00000000..af298ee0 --- /dev/null +++ b/PLC/Methods/AdmGetPowerControlUnitNodes.py @@ -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 index 00000000..8f7e0c79 --- /dev/null +++ b/PLC/Methods/AdmGetPowerControlUnits.py @@ -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 index 00000000..b366c80c --- /dev/null +++ b/PLC/Methods/AdmGetSiteNodes.py @@ -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 index 00000000..d35ee880 --- /dev/null +++ b/PLC/Methods/AdmGetSitePIs.py @@ -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 index 00000000..81225288 --- /dev/null +++ b/PLC/Methods/AdmGetSitePersons.py @@ -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 index 00000000..b95f2987 --- /dev/null +++ b/PLC/Methods/AdmGetSitePowerControlUnits.py @@ -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 index 00000000..f531db5e --- /dev/null +++ b/PLC/Methods/AdmGetSiteTechContacts.py @@ -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 index 00000000..cf5b0cd6 --- /dev/null +++ b/PLC/Methods/AdmGetSites.py @@ -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 index 00000000..36e2e250 --- /dev/null +++ b/PLC/Methods/AdmGrantRoleToPerson.py @@ -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 index 00000000..b32ab035 --- /dev/null +++ b/PLC/Methods/AdmIsPersonInRole.py @@ -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 index 00000000..6cf5d99d --- /dev/null +++ b/PLC/Methods/AdmQueryConfFile.py @@ -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 index 00000000..f41d04f6 --- /dev/null +++ b/PLC/Methods/AdmQueryNode.py @@ -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 index 00000000..b41d0a5a --- /dev/null +++ b/PLC/Methods/AdmQueryPerson.py @@ -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 index 00000000..8fc2f429 --- /dev/null +++ b/PLC/Methods/AdmQueryPowerControlUnit.py @@ -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 index 00000000..cad6b8ca --- /dev/null +++ b/PLC/Methods/AdmQuerySite.py @@ -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 index 00000000..c8368e32 --- /dev/null +++ b/PLC/Methods/AdmRebootNode.py @@ -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 index 00000000..f905a162 --- /dev/null +++ b/PLC/Methods/AdmRemoveNodeFromNodeGroup.py @@ -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 index 00000000..54d3f1de --- /dev/null +++ b/PLC/Methods/AdmRemovePersonFromSite.py @@ -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 index 00000000..2631a3a2 --- /dev/null +++ b/PLC/Methods/AdmRevokeRoleFromPerson.py @@ -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 index 00000000..2009f006 --- /dev/null +++ b/PLC/Methods/AdmSetPersonEnabled.py @@ -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 index 00000000..c631a951 --- /dev/null +++ b/PLC/Methods/AdmSetPersonPrimarySite.py @@ -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 index 00000000..ba4b3f1d --- /dev/null +++ b/PLC/Methods/AdmUpdateNode.py @@ -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 index 00000000..b53198ac --- /dev/null +++ b/PLC/Methods/AdmUpdateNodeGroup.py @@ -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 index 00000000..a85d62a1 --- /dev/null +++ b/PLC/Methods/AdmUpdateNodeNetwork.py @@ -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 index 00000000..066fe6d7 --- /dev/null +++ b/PLC/Methods/AdmUpdatePerson.py @@ -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 index 00000000..0b6c26ae --- /dev/null +++ b/PLC/Methods/AdmUpdateSite.py @@ -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 index 00000000..ed564fbc --- /dev/null +++ b/PLC/Methods/AdmUpdateSitePowerControlUnit.py @@ -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 index 00000000..b223e1e1 --- /dev/null +++ b/PLC/Methods/AnonAdmGetNodeGroups.py @@ -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 index 00000000..0a4c2601 --- /dev/null +++ b/PLC/Methods/AuthCheck.py @@ -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 index 00000000..7953e7a0 --- /dev/null +++ b/PLC/Methods/BlacklistKey.py @@ -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 index 00000000..ea9b0989 --- /dev/null +++ b/PLC/Methods/BootCheckAuthentication.py @@ -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 index 00000000..2f5056d9 --- /dev/null +++ b/PLC/Methods/BootGetNodeDetails.py @@ -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 index 00000000..81a7cb01 --- /dev/null +++ b/PLC/Methods/BootNotifyOwners.py @@ -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 index 00000000..52381cbe --- /dev/null +++ b/PLC/Methods/BootUpdateNode.py @@ -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 index 00000000..d4f98bc7 --- /dev/null +++ b/PLC/Methods/DeleteAddress.py @@ -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 index 00000000..4fd1d9b6 --- /dev/null +++ b/PLC/Methods/DeleteAddressType.py @@ -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 index 00000000..d4ea928b --- /dev/null +++ b/PLC/Methods/DeleteAddressTypeFromAddress.py @@ -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 index 00000000..507fc7b7 --- /dev/null +++ b/PLC/Methods/DeleteBootState.py @@ -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 index 00000000..f05ae43f --- /dev/null +++ b/PLC/Methods/DeleteConfFile.py @@ -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 index 00000000..50b08e6f --- /dev/null +++ b/PLC/Methods/DeleteConfFileFromNode.py @@ -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 index 00000000..5504b0fc --- /dev/null +++ b/PLC/Methods/DeleteConfFileFromNodeGroup.py @@ -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 index 00000000..47a99930 --- /dev/null +++ b/PLC/Methods/DeleteInitScript.py @@ -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 index 00000000..86c16a50 --- /dev/null +++ b/PLC/Methods/DeleteKey.py @@ -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 index 00000000..e09e5c51 --- /dev/null +++ b/PLC/Methods/DeleteKeyType.py @@ -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 index 00000000..49899421 --- /dev/null +++ b/PLC/Methods/DeleteMessage.py @@ -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 index 00000000..d0f982e3 --- /dev/null +++ b/PLC/Methods/DeleteNetworkMethod.py @@ -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 index 00000000..a02f6e0c --- /dev/null +++ b/PLC/Methods/DeleteNetworkType.py @@ -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 index 00000000..bc92718c --- /dev/null +++ b/PLC/Methods/DeleteNode.py @@ -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 index 00000000..2bc67704 --- /dev/null +++ b/PLC/Methods/DeleteNodeFromNodeGroup.py @@ -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 index 00000000..8e728efb --- /dev/null +++ b/PLC/Methods/DeleteNodeFromPCU.py @@ -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 index 00000000..76501504 --- /dev/null +++ b/PLC/Methods/DeleteNodeGroup.py @@ -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 index 00000000..4bdda21c --- /dev/null +++ b/PLC/Methods/DeleteNodeNetwork.py @@ -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 index 00000000..e092f37b --- /dev/null +++ b/PLC/Methods/DeleteNodeNetworkSetting.py @@ -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 index 00000000..c1ae76b2 --- /dev/null +++ b/PLC/Methods/DeleteNodeNetworkSettingType.py @@ -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 index 00000000..944882bc --- /dev/null +++ b/PLC/Methods/DeletePCU.py @@ -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 index 00000000..ab66520c --- /dev/null +++ b/PLC/Methods/DeletePCUProtocolType.py @@ -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 index 00000000..d73c204d --- /dev/null +++ b/PLC/Methods/DeletePCUType.py @@ -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 index 00000000..4260d259 --- /dev/null +++ b/PLC/Methods/DeletePeer.py @@ -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 index 00000000..448f8085 --- /dev/null +++ b/PLC/Methods/DeletePerson.py @@ -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 index 00000000..db2af2df --- /dev/null +++ b/PLC/Methods/DeletePersonFromSite.py @@ -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 index 00000000..c990ccd3 --- /dev/null +++ b/PLC/Methods/DeletePersonFromSlice.py @@ -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 index 00000000..f7072809 --- /dev/null +++ b/PLC/Methods/DeleteRole.py @@ -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 index 00000000..151ba258 --- /dev/null +++ b/PLC/Methods/DeleteRoleFromPerson.py @@ -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 index 00000000..3898f515 --- /dev/null +++ b/PLC/Methods/DeleteSession.py @@ -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 index 00000000..c23fff45 --- /dev/null +++ b/PLC/Methods/DeleteSite.py @@ -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 index 00000000..297f8a94 --- /dev/null +++ b/PLC/Methods/DeleteSlice.py @@ -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 index 00000000..06a99f3e --- /dev/null +++ b/PLC/Methods/DeleteSliceAttribute.py @@ -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 index 00000000..e6c1a8a7 --- /dev/null +++ b/PLC/Methods/DeleteSliceAttributeType.py @@ -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 index 00000000..2390be52 --- /dev/null +++ b/PLC/Methods/DeleteSliceFromNodes.py @@ -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 index 00000000..8899d884 --- /dev/null +++ b/PLC/Methods/DeleteSliceFromNodesWhitelist.py @@ -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 index 00000000..5098a9de --- /dev/null +++ b/PLC/Methods/DeleteSliceInstantiation.py @@ -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 index 00000000..0b5cf8e7 --- /dev/null +++ b/PLC/Methods/GenerateNodeConfFile.py @@ -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 index 00000000..d10be739 --- /dev/null +++ b/PLC/Methods/GetAddressTypes.py @@ -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 index 00000000..b299295e --- /dev/null +++ b/PLC/Methods/GetAddresses.py @@ -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 index 00000000..fcd0957a --- /dev/null +++ b/PLC/Methods/GetBootMedium.py @@ -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 (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 index 00000000..4cd31bea --- /dev/null +++ b/PLC/Methods/GetBootStates.py @@ -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 index 00000000..89d5250d --- /dev/null +++ b/PLC/Methods/GetConfFiles.py @@ -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 index 00000000..02bcd68a --- /dev/null +++ b/PLC/Methods/GetEventObjects.py @@ -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 index 00000000..2bc989c1 --- /dev/null +++ b/PLC/Methods/GetEvents.py @@ -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 index 00000000..d8bb0f53 --- /dev/null +++ b/PLC/Methods/GetInitScripts.py @@ -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 index 00000000..32bb6580 --- /dev/null +++ b/PLC/Methods/GetKeyTypes.py @@ -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 index 00000000..2d7550ca --- /dev/null +++ b/PLC/Methods/GetKeys.py @@ -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 index 00000000..b0eb44e9 --- /dev/null +++ b/PLC/Methods/GetMessages.py @@ -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 index 00000000..cee914ac --- /dev/null +++ b/PLC/Methods/GetNetworkMethods.py @@ -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 index 00000000..dbddd9fc --- /dev/null +++ b/PLC/Methods/GetNetworkTypes.py @@ -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 index 00000000..f4927efc --- /dev/null +++ b/PLC/Methods/GetNodeGroups.py @@ -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 index 00000000..462cfbc9 --- /dev/null +++ b/PLC/Methods/GetNodeNetworkSettingTypes.py @@ -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 index 00000000..0003be9c --- /dev/null +++ b/PLC/Methods/GetNodeNetworkSettings.py @@ -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 index 00000000..150f87de --- /dev/null +++ b/PLC/Methods/GetNodeNetworks.py @@ -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 index 00000000..f4b577d3 --- /dev/null +++ b/PLC/Methods/GetNodes.py @@ -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 index 00000000..44f9380f --- /dev/null +++ b/PLC/Methods/GetPCUProtocolTypes.py @@ -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 index 00000000..2a81508c --- /dev/null +++ b/PLC/Methods/GetPCUTypes.py @@ -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 index 00000000..ee9ab4de --- /dev/null +++ b/PLC/Methods/GetPCUs.py @@ -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 index 00000000..e68cf55a --- /dev/null +++ b/PLC/Methods/GetPeerData.py @@ -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 index 00000000..30fbd945 --- /dev/null +++ b/PLC/Methods/GetPeerName.py @@ -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 index 00000000..e93fe363 --- /dev/null +++ b/PLC/Methods/GetPeers.py @@ -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 index 00000000..52289334 --- /dev/null +++ b/PLC/Methods/GetPersons.py @@ -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 index 00000000..d35df13c --- /dev/null +++ b/PLC/Methods/GetPlcRelease.py @@ -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[^:]+)\s*:\s*(?P.*)\Z', + 'tags' : '\A(?P[^:]+)\s*:=\s*(?P.*)\Z', + 'rpms' : '\A(?P[^:]+)\s*::\s*(?P.*)\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 index 00000000..04566623 --- /dev/null +++ b/PLC/Methods/GetRoles.py @@ -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 index 00000000..ae752198 --- /dev/null +++ b/PLC/Methods/GetSession.py @@ -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 index 00000000..a72553c2 --- /dev/null +++ b/PLC/Methods/GetSessions.py @@ -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 index 00000000..c0f198e1 --- /dev/null +++ b/PLC/Methods/GetSites.py @@ -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 index 00000000..bc8f1ed1 --- /dev/null +++ b/PLC/Methods/GetSliceAttributeTypes.py @@ -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 index 00000000..b8a0a11b --- /dev/null +++ b/PLC/Methods/GetSliceAttributes.py @@ -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 index 00000000..174c2095 --- /dev/null +++ b/PLC/Methods/GetSliceInstantiations.py @@ -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 index 00000000..4029c83c --- /dev/null +++ b/PLC/Methods/GetSliceKeys.py @@ -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 index 00000000..cd73f7b6 --- /dev/null +++ b/PLC/Methods/GetSliceTicket.py @@ -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 index 00000000..63dc0b40 --- /dev/null +++ b/PLC/Methods/GetSlices.py @@ -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 index 00000000..b7e4cde8 --- /dev/null +++ b/PLC/Methods/GetSlicesMD5.py @@ -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 index 00000000..e23e5fce --- /dev/null +++ b/PLC/Methods/GetSlivers.py @@ -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 index 00000000..11251f8c --- /dev/null +++ b/PLC/Methods/GetWhitelist.py @@ -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 index 00000000..70c273d6 --- /dev/null +++ b/PLC/Methods/NotifyPersons.py @@ -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 index 00000000..99ec318e --- /dev/null +++ b/PLC/Methods/NotifySupport.py @@ -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 index 00000000..bea6b894 --- /dev/null +++ b/PLC/Methods/RebootNode.py @@ -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 index 00000000..a45f8bd8 --- /dev/null +++ b/PLC/Methods/RefreshPeer.py @@ -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 index 00000000..0e2d2a95 --- /dev/null +++ b/PLC/Methods/ResetPassword.py @@ -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 index 00000000..644826b6 --- /dev/null +++ b/PLC/Methods/SetPersonPrimarySite.py @@ -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 index 00000000..cc30b520 --- /dev/null +++ b/PLC/Methods/SliceCreate.py @@ -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 index 00000000..70f6696d --- /dev/null +++ b/PLC/Methods/SliceDelete.py @@ -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 index 00000000..12116104 --- /dev/null +++ b/PLC/Methods/SliceExtendedInfo.py @@ -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 index 00000000..64413c9a --- /dev/null +++ b/PLC/Methods/SliceGetTicket.py @@ -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 " + # ' to ' + self.write(escape(content, { + '"': '"', + "'": ''', + })) + + 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() + + # + xml.startElement('ticket', {}) + + # + xml.startElement('slice', + {'id': str(slice['slice_id']), + 'name': unicode(slice['name']), + 'expiry': unicode(int(slice['expires']))}) + + # + xml.startElement('nodes', {}) + for node_id in slice['node_ids']: + if not nodes.has_key(node_id): + continue + node = nodes[node_id] + # + xml.simpleElement('node', + {'id': str(node['node_id']), + 'hostname': unicode(node['hostname'])}) + # + xml.endElement('nodes') + + # + xml.startElement('users', {}) + for person_id in slice['person_ids']: + if not persons.has_key(person_id): + continue + user = persons[person_id] + # + xml.simpleElement('user', + {'person_id': unicode(user['person_id']), + 'email': unicode(user['email'])}) + # + xml.endElement('users') + + # + 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" + + # + xml.startElement('resource', {'name': unicode(attribute_name)}) + + # + xml.startElement('value', + {'name': unicode(value_name), + 'type': type}, + newl = False) + # element value + xml.characters(unicode(value)) + # + xml.endElement('value', indent = False) + + # + xml.endElement('resource') + # + xml.endElement('rspec') + + # + 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 index 00000000..9645f996 --- /dev/null +++ b/PLC/Methods/SliceInfo.py @@ -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 index 00000000..4d94933d --- /dev/null +++ b/PLC/Methods/SliceListNames.py @@ -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 index 00000000..9e054eda --- /dev/null +++ b/PLC/Methods/SliceListUserSlices.py @@ -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 index 00000000..35ccabef --- /dev/null +++ b/PLC/Methods/SliceNodesAdd.py @@ -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 index 00000000..66c0ed23 --- /dev/null +++ b/PLC/Methods/SliceNodesDel.py @@ -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 index 00000000..0c44f66e --- /dev/null +++ b/PLC/Methods/SliceNodesList.py @@ -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 index 00000000..4ac6f895 --- /dev/null +++ b/PLC/Methods/SliceRenew.py @@ -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 index 00000000..5b2b786a --- /dev/null +++ b/PLC/Methods/SliceTicketGet.py @@ -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 index 00000000..9e82d3ad --- /dev/null +++ b/PLC/Methods/SliceUpdate.py @@ -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 index 00000000..560a66a5 --- /dev/null +++ b/PLC/Methods/SliceUserAdd.py @@ -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 index 00000000..0b41b158 --- /dev/null +++ b/PLC/Methods/SliceUserDel.py @@ -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 index 00000000..e3eb1e42 --- /dev/null +++ b/PLC/Methods/SliceUsersList.py @@ -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 index 00000000..ed2fd435 --- /dev/null +++ b/PLC/Methods/UpdateAddress.py @@ -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 index 00000000..922c9409 --- /dev/null +++ b/PLC/Methods/UpdateAddressType.py @@ -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 index 00000000..6fd0e2a9 --- /dev/null +++ b/PLC/Methods/UpdateConfFile.py @@ -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 index 00000000..bb0f1f05 --- /dev/null +++ b/PLC/Methods/UpdateInitScript.py @@ -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 index 00000000..0fb560bd --- /dev/null +++ b/PLC/Methods/UpdateKey.py @@ -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 index 00000000..e44dca47 --- /dev/null +++ b/PLC/Methods/UpdateMessage.py @@ -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 index 00000000..d5cdb0d4 --- /dev/null +++ b/PLC/Methods/UpdateNode.py @@ -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 index 00000000..c84c7f18 --- /dev/null +++ b/PLC/Methods/UpdateNodeGroup.py @@ -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 index 00000000..dc1e65ad --- /dev/null +++ b/PLC/Methods/UpdateNodeNetwork.py @@ -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 index 00000000..e8f74972 --- /dev/null +++ b/PLC/Methods/UpdateNodeNetworkSetting.py @@ -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 index 00000000..b18079b6 --- /dev/null +++ b/PLC/Methods/UpdateNodeNetworkSettingType.py @@ -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 index 00000000..89166843 --- /dev/null +++ b/PLC/Methods/UpdatePCU.py @@ -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 index 00000000..b1a30bca --- /dev/null +++ b/PLC/Methods/UpdatePCUProtocolType.py @@ -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 index 00000000..fc4e8869 --- /dev/null +++ b/PLC/Methods/UpdatePCUType.py @@ -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 index 00000000..8586a48a --- /dev/null +++ b/PLC/Methods/UpdatePeer.py @@ -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 index 00000000..20436049 --- /dev/null +++ b/PLC/Methods/UpdatePerson.py @@ -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 index 00000000..6a33c5e2 --- /dev/null +++ b/PLC/Methods/UpdateSite.py @@ -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 index 00000000..901ecbc4 --- /dev/null +++ b/PLC/Methods/UpdateSlice.py @@ -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 index 00000000..94512919 --- /dev/null +++ b/PLC/Methods/UpdateSliceAttribute.py @@ -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 index 00000000..145a51da --- /dev/null +++ b/PLC/Methods/UpdateSliceAttributeType.py @@ -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 index 00000000..9dd784b7 --- /dev/null +++ b/PLC/Methods/VerifyPerson.py @@ -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 index 00000000..45c92284 --- /dev/null +++ b/PLC/Methods/__init__.py @@ -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 index 00000000..0d20b648 --- /dev/null +++ b/PLC/Methods/system/.cvsignore @@ -0,0 +1 @@ +*.pyc diff --git a/PLC/Methods/system/__init__.py b/PLC/Methods/system/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PLC/Methods/system/listMethods.py b/PLC/Methods/system/listMethods.py new file mode 100644 index 00000000..c8cfa37a --- /dev/null +++ b/PLC/Methods/system/listMethods.py @@ -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 index 00000000..22a0dc1c --- /dev/null +++ b/PLC/Methods/system/methodHelp.py @@ -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 index 00000000..4b049a1d --- /dev/null +++ b/PLC/Methods/system/methodSignature.py @@ -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 index 00000000..64563ef5 --- /dev/null +++ b/PLC/Methods/system/multicall.py @@ -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 index 00000000..d6b6a637 --- /dev/null +++ b/PLC/NetworkMethods.py @@ -0,0 +1,53 @@ +# +# Functions for interacting with the network_methods table in the database +# +# Mark Huang +# 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 index 00000000..b42b42ee --- /dev/null +++ b/PLC/NetworkTypes.py @@ -0,0 +1,53 @@ +# +# Functions for interacting with the network_types table in the database +# +# Mark Huang +# 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 index 00000000..65b4a41c --- /dev/null +++ b/PLC/NodeGroups.py @@ -0,0 +1,182 @@ +# +# Functions for interacting with the nodegroups table in the database +# +# Mark Huang +# 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 index 00000000..69f36b75 --- /dev/null +++ b/PLC/NodeNetworkSettingTypes.py @@ -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 index 00000000..bcf25065 --- /dev/null +++ b/PLC/NodeNetworkSettings.py @@ -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 index 00000000..1d072a66 --- /dev/null +++ b/PLC/NodeNetworks.py @@ -0,0 +1,231 @@ +# +# Functions for interacting with the nodenetworks table in the database +# +# Mark Huang +# 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 index 00000000..ac7b1266 --- /dev/null +++ b/PLC/Nodes.py @@ -0,0 +1,326 @@ +# +# Functions for interacting with the nodes table in the database +# +# Mark Huang +# 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 index 00000000..9475d314 --- /dev/null +++ b/PLC/PCUProtocolTypes.py @@ -0,0 +1,75 @@ +# +# Functions for interacting with the pcu_type_port table in the database +# +# Mark Huang +# 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 index 00000000..f0e73768 --- /dev/null +++ b/PLC/PCUTypes.py @@ -0,0 +1,104 @@ +# +# Functions for interacting with the pcu_types table in the database +# +# Mark Huang +# 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 index 00000000..0ab56cc7 --- /dev/null +++ b/PLC/PCUs.py @@ -0,0 +1,116 @@ +# +# Functions for interacting with the pcus table in the database +# +# Mark Huang +# 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 index 00000000..01a7ddd1 --- /dev/null +++ b/PLC/POD.py @@ -0,0 +1,90 @@ +# Marc E. Fiuczynski +# 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 index 00000000..474ad78a --- /dev/null +++ b/PLC/Parameter.py @@ -0,0 +1,105 @@ +# +# Shared type definitions +# +# Mark Huang +# 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 index 00000000..0973c3d4 --- /dev/null +++ b/PLC/Peers.py @@ -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 index 00000000..d2bb510d --- /dev/null +++ b/PLC/Persons.py @@ -0,0 +1,501 @@ +# +# Functions for interacting with the persons table in the database +# +# Mark Huang +# 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 index 00000000..23baa997 --- /dev/null +++ b/PLC/PostgreSQL.py @@ -0,0 +1,263 @@ +# +# PostgreSQL database interface. Sort of like DBI(3) (Database +# independent interface for Perl). +# +# Mark Huang +# 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 index 00000000..548e290d --- /dev/null +++ b/PLC/PyCurl.py @@ -0,0 +1,82 @@ +# +# Replacement for xmlrpclib.SafeTransport, which does not validate +# SSL certificates. Requires PyCurl. +# +# Mark Huang +# 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="" + 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 index 00000000..53d68d01 --- /dev/null +++ b/PLC/Roles.py @@ -0,0 +1,72 @@ +# +# Functions for interacting with the roles table in the database +# +# Mark Huang +# 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 index 00000000..e0a57b32 --- /dev/null +++ b/PLC/Sessions.py @@ -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 index 00000000..43317a02 --- /dev/null +++ b/PLC/Shell.py @@ -0,0 +1,260 @@ +#!/usr/bin/python +# +# Interactive shell for testing PLCAPI +# +# Mark Huang +# 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 index 00000000..6035bb78 --- /dev/null +++ b/PLC/Sites.py @@ -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 index 00000000..5884fe0a --- /dev/null +++ b/PLC/SliceAttributeTypes.py @@ -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 index 00000000..9f0b1fbe --- /dev/null +++ b/PLC/SliceAttributes.py @@ -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 index 00000000..db118388 --- /dev/null +++ b/PLC/SliceInstantiations.py @@ -0,0 +1,53 @@ +# +# Functions for interacting with the slice_instantiations table in the database +# +# Mark Huang +# 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 index 00000000..1a1786c8 --- /dev/null +++ b/PLC/Slices.py @@ -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 index 00000000..07c3c09d --- /dev/null +++ b/PLC/Table.py @@ -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 index 00000000..48671194 --- /dev/null +++ b/PLC/Test.py @@ -0,0 +1,1460 @@ +#!/usr/bin/python +# +# Test script utility class +# +# Mark Huang +# 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 index 00000000..d5ddda8a --- /dev/null +++ b/PLC/__init__.py @@ -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 index 00000000..cab0ae62 --- /dev/null +++ b/PLC/sendmail.py @@ -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" + + 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()) -- 2.47.0