wmglobalqueue 2.4.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Utils/CPMetrics.py +270 -0
- Utils/CertTools.py +100 -0
- Utils/EmailAlert.py +50 -0
- Utils/ExtendedUnitTestCase.py +62 -0
- Utils/FileTools.py +182 -0
- Utils/IteratorTools.py +80 -0
- Utils/MathUtils.py +31 -0
- Utils/MemoryCache.py +119 -0
- Utils/Patterns.py +24 -0
- Utils/Pipeline.py +137 -0
- Utils/PortForward.py +97 -0
- Utils/ProcFS.py +112 -0
- Utils/ProcessStats.py +194 -0
- Utils/PythonVersion.py +17 -0
- Utils/Signals.py +36 -0
- Utils/TemporaryEnvironment.py +27 -0
- Utils/Throttled.py +227 -0
- Utils/Timers.py +130 -0
- Utils/Timestamps.py +86 -0
- Utils/TokenManager.py +143 -0
- Utils/Tracing.py +60 -0
- Utils/TwPrint.py +98 -0
- Utils/Utilities.py +318 -0
- Utils/__init__.py +11 -0
- Utils/wmcoreDTools.py +707 -0
- WMCore/ACDC/Collection.py +57 -0
- WMCore/ACDC/CollectionTypes.py +12 -0
- WMCore/ACDC/CouchCollection.py +67 -0
- WMCore/ACDC/CouchFileset.py +238 -0
- WMCore/ACDC/CouchService.py +73 -0
- WMCore/ACDC/DataCollectionService.py +485 -0
- WMCore/ACDC/Fileset.py +94 -0
- WMCore/ACDC/__init__.py +11 -0
- WMCore/Algorithms/Alarm.py +39 -0
- WMCore/Algorithms/MathAlgos.py +274 -0
- WMCore/Algorithms/MiscAlgos.py +67 -0
- WMCore/Algorithms/ParseXMLFile.py +115 -0
- WMCore/Algorithms/Permissions.py +27 -0
- WMCore/Algorithms/Singleton.py +58 -0
- WMCore/Algorithms/SubprocessAlgos.py +129 -0
- WMCore/Algorithms/__init__.py +7 -0
- WMCore/Cache/GenericDataCache.py +98 -0
- WMCore/Cache/WMConfigCache.py +572 -0
- WMCore/Cache/__init__.py +0 -0
- WMCore/Configuration.py +659 -0
- WMCore/DAOFactory.py +47 -0
- WMCore/DataStructs/File.py +177 -0
- WMCore/DataStructs/Fileset.py +140 -0
- WMCore/DataStructs/Job.py +182 -0
- WMCore/DataStructs/JobGroup.py +142 -0
- WMCore/DataStructs/JobPackage.py +49 -0
- WMCore/DataStructs/LumiList.py +734 -0
- WMCore/DataStructs/Mask.py +219 -0
- WMCore/DataStructs/MathStructs/ContinuousSummaryHistogram.py +197 -0
- WMCore/DataStructs/MathStructs/DiscreteSummaryHistogram.py +92 -0
- WMCore/DataStructs/MathStructs/SummaryHistogram.py +117 -0
- WMCore/DataStructs/MathStructs/__init__.py +0 -0
- WMCore/DataStructs/Pickleable.py +24 -0
- WMCore/DataStructs/Run.py +256 -0
- WMCore/DataStructs/Subscription.py +175 -0
- WMCore/DataStructs/WMObject.py +47 -0
- WMCore/DataStructs/WorkUnit.py +112 -0
- WMCore/DataStructs/Workflow.py +60 -0
- WMCore/DataStructs/__init__.py +8 -0
- WMCore/Database/CMSCouch.py +1430 -0
- WMCore/Database/ConfigDBMap.py +29 -0
- WMCore/Database/CouchMonitoring.py +450 -0
- WMCore/Database/CouchUtils.py +118 -0
- WMCore/Database/DBCore.py +198 -0
- WMCore/Database/DBCreator.py +113 -0
- WMCore/Database/DBExceptionHandler.py +59 -0
- WMCore/Database/DBFactory.py +117 -0
- WMCore/Database/DBFormatter.py +177 -0
- WMCore/Database/Dialects.py +13 -0
- WMCore/Database/ExecuteDAO.py +327 -0
- WMCore/Database/MongoDB.py +241 -0
- WMCore/Database/MySQL/Destroy.py +42 -0
- WMCore/Database/MySQL/ListUserContent.py +20 -0
- WMCore/Database/MySQL/__init__.py +9 -0
- WMCore/Database/MySQLCore.py +132 -0
- WMCore/Database/Oracle/Destroy.py +56 -0
- WMCore/Database/Oracle/ListUserContent.py +19 -0
- WMCore/Database/Oracle/__init__.py +9 -0
- WMCore/Database/ResultSet.py +44 -0
- WMCore/Database/Transaction.py +91 -0
- WMCore/Database/__init__.py +9 -0
- WMCore/Database/ipy_profile_couch.py +438 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/CleanUpTask.py +29 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/HeartbeatMonitor.py +105 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/LocationUpdateTask.py +28 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/ReqMgrInteractionTask.py +35 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/__init__.py +0 -0
- WMCore/GlobalWorkQueue/__init__.py +0 -0
- WMCore/GroupUser/CouchObject.py +127 -0
- WMCore/GroupUser/Decorators.py +51 -0
- WMCore/GroupUser/Group.py +33 -0
- WMCore/GroupUser/Interface.py +73 -0
- WMCore/GroupUser/User.py +96 -0
- WMCore/GroupUser/__init__.py +11 -0
- WMCore/Lexicon.py +836 -0
- WMCore/REST/Auth.py +202 -0
- WMCore/REST/CherryPyPeriodicTask.py +166 -0
- WMCore/REST/Error.py +333 -0
- WMCore/REST/Format.py +642 -0
- WMCore/REST/HeartbeatMonitorBase.py +90 -0
- WMCore/REST/Main.py +636 -0
- WMCore/REST/Server.py +2435 -0
- WMCore/REST/Services.py +24 -0
- WMCore/REST/Test.py +120 -0
- WMCore/REST/Tools.py +38 -0
- WMCore/REST/Validation.py +250 -0
- WMCore/REST/__init__.py +1 -0
- WMCore/ReqMgr/DataStructs/RequestStatus.py +209 -0
- WMCore/ReqMgr/DataStructs/RequestType.py +13 -0
- WMCore/ReqMgr/DataStructs/__init__.py +0 -0
- WMCore/ReqMgr/__init__.py +1 -0
- WMCore/Services/AlertManager/AlertManagerAPI.py +111 -0
- WMCore/Services/AlertManager/__init__.py +0 -0
- WMCore/Services/CRIC/CRIC.py +238 -0
- WMCore/Services/CRIC/__init__.py +0 -0
- WMCore/Services/DBS/DBS3Reader.py +1044 -0
- WMCore/Services/DBS/DBSConcurrency.py +44 -0
- WMCore/Services/DBS/DBSErrors.py +112 -0
- WMCore/Services/DBS/DBSReader.py +23 -0
- WMCore/Services/DBS/DBSUtils.py +166 -0
- WMCore/Services/DBS/DBSWriterObjects.py +381 -0
- WMCore/Services/DBS/ProdException.py +133 -0
- WMCore/Services/DBS/__init__.py +8 -0
- WMCore/Services/FWJRDB/FWJRDBAPI.py +118 -0
- WMCore/Services/FWJRDB/__init__.py +0 -0
- WMCore/Services/HTTPS/HTTPSAuthHandler.py +66 -0
- WMCore/Services/HTTPS/__init__.py +0 -0
- WMCore/Services/LogDB/LogDB.py +201 -0
- WMCore/Services/LogDB/LogDBBackend.py +191 -0
- WMCore/Services/LogDB/LogDBExceptions.py +11 -0
- WMCore/Services/LogDB/LogDBReport.py +85 -0
- WMCore/Services/LogDB/__init__.py +0 -0
- WMCore/Services/MSPileup/__init__.py +0 -0
- WMCore/Services/MSUtils/MSUtils.py +54 -0
- WMCore/Services/MSUtils/__init__.py +0 -0
- WMCore/Services/McM/McM.py +173 -0
- WMCore/Services/McM/__init__.py +8 -0
- WMCore/Services/MonIT/Grafana.py +133 -0
- WMCore/Services/MonIT/__init__.py +0 -0
- WMCore/Services/PyCondor/PyCondorAPI.py +154 -0
- WMCore/Services/PyCondor/__init__.py +0 -0
- WMCore/Services/ReqMgr/ReqMgr.py +261 -0
- WMCore/Services/ReqMgr/__init__.py +0 -0
- WMCore/Services/ReqMgrAux/ReqMgrAux.py +419 -0
- WMCore/Services/ReqMgrAux/__init__.py +0 -0
- WMCore/Services/RequestDB/RequestDBReader.py +267 -0
- WMCore/Services/RequestDB/RequestDBWriter.py +39 -0
- WMCore/Services/RequestDB/__init__.py +0 -0
- WMCore/Services/Requests.py +624 -0
- WMCore/Services/Rucio/Rucio.py +1290 -0
- WMCore/Services/Rucio/RucioUtils.py +74 -0
- WMCore/Services/Rucio/__init__.py +0 -0
- WMCore/Services/RucioConMon/RucioConMon.py +121 -0
- WMCore/Services/RucioConMon/__init__.py +0 -0
- WMCore/Services/Service.py +400 -0
- WMCore/Services/StompAMQ/__init__.py +0 -0
- WMCore/Services/TagCollector/TagCollector.py +155 -0
- WMCore/Services/TagCollector/XMLUtils.py +98 -0
- WMCore/Services/TagCollector/__init__.py +0 -0
- WMCore/Services/UUIDLib.py +13 -0
- WMCore/Services/UserFileCache/UserFileCache.py +160 -0
- WMCore/Services/UserFileCache/__init__.py +8 -0
- WMCore/Services/WMAgent/WMAgent.py +63 -0
- WMCore/Services/WMAgent/__init__.py +0 -0
- WMCore/Services/WMArchive/CMSSWMetrics.py +526 -0
- WMCore/Services/WMArchive/DataMap.py +463 -0
- WMCore/Services/WMArchive/WMArchive.py +33 -0
- WMCore/Services/WMArchive/__init__.py +0 -0
- WMCore/Services/WMBS/WMBS.py +97 -0
- WMCore/Services/WMBS/__init__.py +0 -0
- WMCore/Services/WMStats/DataStruct/RequestInfoCollection.py +300 -0
- WMCore/Services/WMStats/DataStruct/__init__.py +0 -0
- WMCore/Services/WMStats/WMStatsPycurl.py +145 -0
- WMCore/Services/WMStats/WMStatsReader.py +445 -0
- WMCore/Services/WMStats/WMStatsWriter.py +273 -0
- WMCore/Services/WMStats/__init__.py +0 -0
- WMCore/Services/WMStatsServer/WMStatsServer.py +134 -0
- WMCore/Services/WMStatsServer/__init__.py +0 -0
- WMCore/Services/WorkQueue/WorkQueue.py +492 -0
- WMCore/Services/WorkQueue/__init__.py +0 -0
- WMCore/Services/__init__.py +8 -0
- WMCore/Services/pycurl_manager.py +574 -0
- WMCore/WMBase.py +50 -0
- WMCore/WMConnectionBase.py +164 -0
- WMCore/WMException.py +183 -0
- WMCore/WMExceptions.py +269 -0
- WMCore/WMFactory.py +76 -0
- WMCore/WMInit.py +377 -0
- WMCore/WMLogging.py +104 -0
- WMCore/WMSpec/ConfigSectionTree.py +442 -0
- WMCore/WMSpec/Persistency.py +135 -0
- WMCore/WMSpec/Steps/BuildMaster.py +87 -0
- WMCore/WMSpec/Steps/BuildTools.py +201 -0
- WMCore/WMSpec/Steps/Builder.py +97 -0
- WMCore/WMSpec/Steps/Diagnostic.py +89 -0
- WMCore/WMSpec/Steps/Emulator.py +62 -0
- WMCore/WMSpec/Steps/ExecuteMaster.py +208 -0
- WMCore/WMSpec/Steps/Executor.py +210 -0
- WMCore/WMSpec/Steps/StepFactory.py +213 -0
- WMCore/WMSpec/Steps/TaskEmulator.py +75 -0
- WMCore/WMSpec/Steps/Template.py +204 -0
- WMCore/WMSpec/Steps/Templates/AlcaHarvest.py +76 -0
- WMCore/WMSpec/Steps/Templates/CMSSW.py +613 -0
- WMCore/WMSpec/Steps/Templates/DQMUpload.py +59 -0
- WMCore/WMSpec/Steps/Templates/DeleteFiles.py +70 -0
- WMCore/WMSpec/Steps/Templates/LogArchive.py +84 -0
- WMCore/WMSpec/Steps/Templates/LogCollect.py +105 -0
- WMCore/WMSpec/Steps/Templates/StageOut.py +105 -0
- WMCore/WMSpec/Steps/Templates/__init__.py +10 -0
- WMCore/WMSpec/Steps/WMExecutionFailure.py +21 -0
- WMCore/WMSpec/Steps/__init__.py +8 -0
- WMCore/WMSpec/Utilities.py +63 -0
- WMCore/WMSpec/WMSpecErrors.py +12 -0
- WMCore/WMSpec/WMStep.py +347 -0
- WMCore/WMSpec/WMTask.py +1997 -0
- WMCore/WMSpec/WMWorkload.py +2288 -0
- WMCore/WMSpec/WMWorkloadTools.py +382 -0
- WMCore/WMSpec/__init__.py +9 -0
- WMCore/WorkQueue/DataLocationMapper.py +273 -0
- WMCore/WorkQueue/DataStructs/ACDCBlock.py +47 -0
- WMCore/WorkQueue/DataStructs/Block.py +48 -0
- WMCore/WorkQueue/DataStructs/CouchWorkQueueElement.py +148 -0
- WMCore/WorkQueue/DataStructs/WorkQueueElement.py +274 -0
- WMCore/WorkQueue/DataStructs/WorkQueueElementResult.py +152 -0
- WMCore/WorkQueue/DataStructs/WorkQueueElementsSummary.py +185 -0
- WMCore/WorkQueue/DataStructs/__init__.py +0 -0
- WMCore/WorkQueue/Policy/End/EndPolicyInterface.py +44 -0
- WMCore/WorkQueue/Policy/End/SingleShot.py +22 -0
- WMCore/WorkQueue/Policy/End/__init__.py +32 -0
- WMCore/WorkQueue/Policy/PolicyInterface.py +17 -0
- WMCore/WorkQueue/Policy/Start/Block.py +258 -0
- WMCore/WorkQueue/Policy/Start/Dataset.py +180 -0
- WMCore/WorkQueue/Policy/Start/MonteCarlo.py +131 -0
- WMCore/WorkQueue/Policy/Start/ResubmitBlock.py +171 -0
- WMCore/WorkQueue/Policy/Start/StartPolicyInterface.py +316 -0
- WMCore/WorkQueue/Policy/Start/__init__.py +34 -0
- WMCore/WorkQueue/Policy/__init__.py +57 -0
- WMCore/WorkQueue/WMBSHelper.py +772 -0
- WMCore/WorkQueue/WorkQueue.py +1237 -0
- WMCore/WorkQueue/WorkQueueBackend.py +750 -0
- WMCore/WorkQueue/WorkQueueBase.py +39 -0
- WMCore/WorkQueue/WorkQueueExceptions.py +44 -0
- WMCore/WorkQueue/WorkQueueReqMgrInterface.py +278 -0
- WMCore/WorkQueue/WorkQueueUtils.py +130 -0
- WMCore/WorkQueue/__init__.py +13 -0
- WMCore/Wrappers/JsonWrapper/JSONThunker.py +342 -0
- WMCore/Wrappers/JsonWrapper/__init__.py +7 -0
- WMCore/Wrappers/__init__.py +6 -0
- WMCore/__init__.py +10 -0
- wmglobalqueue-2.4.5.1.data/data/bin/wmc-dist-patch +15 -0
- wmglobalqueue-2.4.5.1.data/data/bin/wmc-dist-unpatch +8 -0
- wmglobalqueue-2.4.5.1.data/data/bin/wmc-httpd +3 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/.couchapprc +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/README.md +40 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/index.html +264 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/ElementInfoByWorkflow.js +96 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/StuckElementInfo.js +57 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/WorkloadInfoTable.js +80 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/dataTable.js +70 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/namespace.js +23 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/style/main.css +75 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/couchapp.json +4 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/childQueueFilter.js +13 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/filterDeletedDocs.js +3 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/queueFilter.js +11 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/language +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/mustache.js +333 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/validate.js +27 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/workqueue_utils.js +61 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/elementsDetail.js +28 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/filter.js +86 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/stuckElements.js +38 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/workRestrictions.js +153 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/workflowSummary.js +28 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/rewrites.json +73 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/shows/redirect.js +23 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/shows/status.js +40 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/ElementSummaryByWorkflow.html +27 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/StuckElementSummary.html +26 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/TaskStatus.html +23 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/WorkflowSummary.html +27 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/workqueue-common-lib.html +2 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib-remote.html +16 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib.html +18 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/updates/in-place.js +50 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/validate_doc_update.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.couch.app.js +235 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.pathbinder.js +173 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeData/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeData/reduce.js +2 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeParentData/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeParentData/reduce.js +2 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activePileupData/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activePileupData/reduce.js +2 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/analyticsData/map.js +11 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/analyticsData/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/availableByPriority/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/conflicts/map.js +5 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elements/map.js +5 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByData/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByParent/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByParentData/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByPileupData/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByStatus/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsBySubscription/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/map.js +8 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/reduce.js +3 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsDetailByWorkflowAndStatus/map.js +26 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/map.js +10 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByRequest/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByRequest/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatus/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatus/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/openRequests/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/recent-items/map.js +5 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/reduce.js +1 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/specsByWorkflow/map.js +5 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/stuckElements/map.js +38 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/map.js +12 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/reduce.js +3 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrl/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrl/reduce.js +2 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/map.js +6 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/reduce.js +2 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/workflowSummary/map.js +9 -0
- wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/workflowSummary/reduce.js +10 -0
- wmglobalqueue-2.4.5.1.dist-info/METADATA +26 -0
- wmglobalqueue-2.4.5.1.dist-info/RECORD +347 -0
- wmglobalqueue-2.4.5.1.dist-info/WHEEL +5 -0
- wmglobalqueue-2.4.5.1.dist-info/licenses/LICENSE +202 -0
- wmglobalqueue-2.4.5.1.dist-info/licenses/NOTICE +16 -0
- wmglobalqueue-2.4.5.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
_CMSCouch_
|
|
4
|
+
|
|
5
|
+
A simple API to CouchDB that sends HTTP requests to the REST interface.
|
|
6
|
+
|
|
7
|
+
http://wiki.apache.org/couchdb/API_Cheatsheet
|
|
8
|
+
|
|
9
|
+
NOT A THREAD SAFE CLASS.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import print_function, division
|
|
12
|
+
from builtins import str as newstr, bytes as newbytes, object
|
|
13
|
+
from Utils.Utilities import decodeBytesToUnicode, encodeUnicodeToBytes, decodeBytesToUnicodeConditional
|
|
14
|
+
from Utils.PythonVersion import PY3
|
|
15
|
+
|
|
16
|
+
from future import standard_library
|
|
17
|
+
standard_library.install_aliases()
|
|
18
|
+
from future.utils import viewitems
|
|
19
|
+
import urllib.request, urllib.parse, urllib.error
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import hashlib
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
import time
|
|
27
|
+
import sys
|
|
28
|
+
from pprint import pformat
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from http.client import HTTPException
|
|
31
|
+
|
|
32
|
+
from Utils.IteratorTools import grouper, nestedDictUpdate
|
|
33
|
+
from WMCore.Lexicon import sanitizeURL
|
|
34
|
+
from WMCore.Services.Requests import JSONRequests
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_name(dbname):
|
|
38
|
+
match = re.match("^[a-z0-9_$()+-/]+$", urllib.parse.unquote_plus(dbname))
|
|
39
|
+
if not match:
|
|
40
|
+
msg = '%s is not a valid database name'
|
|
41
|
+
raise ValueError(msg % urllib.parse.unquote_plus(dbname))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_server_url(srvurl):
|
|
45
|
+
good_name = srvurl.startswith('http://') or srvurl.startswith('https://')
|
|
46
|
+
if not good_name:
|
|
47
|
+
raise ValueError('You must include http(s):// in your servers address')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
PY3_STR_DECODER = lambda x: decodeBytesToUnicodeConditional(x, condition=PY3)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Document(dict):
|
|
54
|
+
"""
|
|
55
|
+
Document class is the instantiation of one document in the CouchDB
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, id=None, inputDict=None):
|
|
59
|
+
"""
|
|
60
|
+
Initialise our Document object - a dictionary which has an id field
|
|
61
|
+
inputDict - input dictionary to initialise this instance
|
|
62
|
+
"""
|
|
63
|
+
inputDict = inputDict or {}
|
|
64
|
+
dict.__init__(self)
|
|
65
|
+
self.update(inputDict)
|
|
66
|
+
if id:
|
|
67
|
+
self.setdefault("_id", id)
|
|
68
|
+
|
|
69
|
+
def delete(self):
|
|
70
|
+
"""
|
|
71
|
+
Mark the document as deleted
|
|
72
|
+
"""
|
|
73
|
+
# https://issues.apache.org/jira/browse/COUCHDB-1141
|
|
74
|
+
deletedDict = {'_id': self['_id'], '_rev': self['_rev'], '_deleted': True}
|
|
75
|
+
self.update(deletedDict)
|
|
76
|
+
for key in list(self.keys()):
|
|
77
|
+
if key not in deletedDict:
|
|
78
|
+
del self[key]
|
|
79
|
+
|
|
80
|
+
def __to_json__(self, thunker):
|
|
81
|
+
"""
|
|
82
|
+
__to_json__
|
|
83
|
+
|
|
84
|
+
This is here to prevent the serializer from attempting to serialize
|
|
85
|
+
this object and adding a bunch of keys that couch won't understand.
|
|
86
|
+
"""
|
|
87
|
+
jsonDict = {}
|
|
88
|
+
for key in self.keys():
|
|
89
|
+
jsonDict[key] = self[key]
|
|
90
|
+
|
|
91
|
+
return jsonDict
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CouchDBRequests(JSONRequests):
|
|
95
|
+
"""
|
|
96
|
+
CouchDB has two non-standard HTTP calls, implement them here for
|
|
97
|
+
completeness, and talks to the CouchDB port
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, url='http://localhost:5984', usePYCurl=True, ckey=None, cert=None, capath=None):
|
|
101
|
+
"""
|
|
102
|
+
Initialise requests
|
|
103
|
+
"""
|
|
104
|
+
JSONRequests.__init__(self, url,
|
|
105
|
+
{"cachepath": None, "pycurl": usePYCurl, "key": ckey, "cert": cert, "capath": capath})
|
|
106
|
+
self.accept_type = "application/json"
|
|
107
|
+
self["timeout"] = 600
|
|
108
|
+
|
|
109
|
+
def move(self, uri=None, data=None):
|
|
110
|
+
"""
|
|
111
|
+
MOVE some data
|
|
112
|
+
"""
|
|
113
|
+
return self.makeRequest(uri, data, 'MOVE')
|
|
114
|
+
|
|
115
|
+
def copy(self, uri=None, data=None):
|
|
116
|
+
"""
|
|
117
|
+
COPY some data
|
|
118
|
+
"""
|
|
119
|
+
return self.makeRequest(uri, data, 'COPY')
|
|
120
|
+
|
|
121
|
+
def makeRequest(self, uri=None, data=None, type='GET', incoming_headers=None,
|
|
122
|
+
encode=True, decode=True, contentType=None, cache=False):
|
|
123
|
+
"""
|
|
124
|
+
Make the request, handle any failed status, return just the data (for
|
|
125
|
+
compatibility). By default do not cache the response.
|
|
126
|
+
|
|
127
|
+
TODO: set caching in the calling methods.
|
|
128
|
+
"""
|
|
129
|
+
incoming_headers = incoming_headers or {}
|
|
130
|
+
incoming_headers.update(self.additionalHeaders)
|
|
131
|
+
try:
|
|
132
|
+
if not cache:
|
|
133
|
+
incoming_headers.update({'Cache-Control': 'no-cache'})
|
|
134
|
+
result, status, reason, cached = JSONRequests.makeRequest(
|
|
135
|
+
self, uri, data, type, incoming_headers,
|
|
136
|
+
encode, decode, contentType)
|
|
137
|
+
except HTTPException as e:
|
|
138
|
+
self.checkForCouchError(getattr(e, "status", None),
|
|
139
|
+
getattr(e, "reason", None),
|
|
140
|
+
data,
|
|
141
|
+
getattr(e, "result", None))
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
def checkForCouchError(self, status, reason, data=None, result=None):
|
|
146
|
+
"""
|
|
147
|
+
_checkForCouchError_
|
|
148
|
+
|
|
149
|
+
Check the HTTP status and raise an appropriate exception.
|
|
150
|
+
"""
|
|
151
|
+
if status == 400:
|
|
152
|
+
raise CouchBadRequestError(reason, data, result, status)
|
|
153
|
+
elif status == 401:
|
|
154
|
+
raise CouchUnauthorisedError(reason, data, result, status)
|
|
155
|
+
elif status == 403:
|
|
156
|
+
raise CouchForbidden(reason, data, result, status)
|
|
157
|
+
elif status == 404:
|
|
158
|
+
raise CouchNotFoundError(reason, data, result, status)
|
|
159
|
+
elif status == 405:
|
|
160
|
+
raise CouchNotAllowedError(reason, data, result, status)
|
|
161
|
+
elif status == 406:
|
|
162
|
+
raise CouchNotAcceptableError(reason, data, result, status)
|
|
163
|
+
elif status == 409:
|
|
164
|
+
raise CouchConflictError(reason, data, result, status)
|
|
165
|
+
elif status == 410:
|
|
166
|
+
raise CouchFeatureGone(reason, data, result, status)
|
|
167
|
+
elif status == 412:
|
|
168
|
+
raise CouchPreconditionFailedError(reason, data, result, status)
|
|
169
|
+
elif status == 413:
|
|
170
|
+
raise CouchRequestTooLargeError(reason, data, result, status)
|
|
171
|
+
elif status == 416:
|
|
172
|
+
raise CouchRequestedRangeNotSatisfiableError(reason, data, result, status)
|
|
173
|
+
elif status == 417:
|
|
174
|
+
raise CouchExpectationFailedError(reason, data, result, status)
|
|
175
|
+
elif status == 500:
|
|
176
|
+
raise CouchInternalServerError(reason, data, result, status)
|
|
177
|
+
elif status in [502, 503, 504]:
|
|
178
|
+
# There are HTTP errors that CouchDB doesn't raise but can appear
|
|
179
|
+
# in our environment, e.g. behind a proxy. Reraise the HTTPException
|
|
180
|
+
raise CouchError(reason, data, result, status)
|
|
181
|
+
else:
|
|
182
|
+
# We have a new error status, log it
|
|
183
|
+
raise CouchError(reason, data, result, status)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Database(CouchDBRequests):
|
|
188
|
+
"""
|
|
189
|
+
Object representing a connection to a CouchDB Database instance.
|
|
190
|
+
TODO: implement COPY and MOVE calls.
|
|
191
|
+
TODO: remove leading whitespace when committing a view
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def __init__(self, dbname='database', url='http://localhost:5984', size=1000, ckey=None, cert=None):
|
|
195
|
+
"""
|
|
196
|
+
A set of queries against a CouchDB database
|
|
197
|
+
"""
|
|
198
|
+
check_name(dbname)
|
|
199
|
+
|
|
200
|
+
self.name = urllib.parse.quote_plus(dbname)
|
|
201
|
+
|
|
202
|
+
CouchDBRequests.__init__(self, url=url, ckey=ckey, cert=cert)
|
|
203
|
+
self._reset_queue()
|
|
204
|
+
|
|
205
|
+
self._queue_size = size
|
|
206
|
+
self.threads = []
|
|
207
|
+
self.last_seq = 0
|
|
208
|
+
|
|
209
|
+
def _reset_queue(self):
|
|
210
|
+
"""
|
|
211
|
+
Set the queue to an empty list, e.g. after a commit
|
|
212
|
+
"""
|
|
213
|
+
self._queue = []
|
|
214
|
+
|
|
215
|
+
def timestamp(self, data, label=''):
|
|
216
|
+
"""
|
|
217
|
+
Time stamp each doc in a list
|
|
218
|
+
"""
|
|
219
|
+
if label is True:
|
|
220
|
+
label = 'timestamp'
|
|
221
|
+
|
|
222
|
+
if isinstance(data, type({})):
|
|
223
|
+
data[label] = int(time.time())
|
|
224
|
+
else:
|
|
225
|
+
for doc in data:
|
|
226
|
+
if label not in doc:
|
|
227
|
+
doc[label] = int(time.time())
|
|
228
|
+
return data
|
|
229
|
+
|
|
230
|
+
def getQueueSize(self):
|
|
231
|
+
"""
|
|
232
|
+
Return the current size of the queue, i.e., how
|
|
233
|
+
many documents are already queued up
|
|
234
|
+
"""
|
|
235
|
+
return len(self._queue)
|
|
236
|
+
|
|
237
|
+
def queue(self, doc, timestamp=False, viewlist=None, callback=None):
|
|
238
|
+
"""
|
|
239
|
+
Queue up a doc for bulk insert. If timestamp = True add a timestamp
|
|
240
|
+
field if one doesn't exist. Use this over commit(timestamp=True) if you
|
|
241
|
+
want to timestamp when a document was added to the queue instead of when
|
|
242
|
+
it was committed
|
|
243
|
+
If a callback is specified then pass it to the commit function if a
|
|
244
|
+
commit is triggered
|
|
245
|
+
"""
|
|
246
|
+
viewlist = viewlist or []
|
|
247
|
+
if timestamp:
|
|
248
|
+
self.timestamp(doc, timestamp)
|
|
249
|
+
# TODO: Thread this off so that it's non blocking...
|
|
250
|
+
if self.getQueueSize() >= self._queue_size:
|
|
251
|
+
logging.warning('queue larger than %s records, committing', self._queue_size)
|
|
252
|
+
self.commit(viewlist=viewlist, callback=callback)
|
|
253
|
+
self._queue.append(doc)
|
|
254
|
+
|
|
255
|
+
def queueDelete(self, doc):
|
|
256
|
+
"""
|
|
257
|
+
Queue up a document for deletion
|
|
258
|
+
"""
|
|
259
|
+
assert isinstance(doc, type({})), "document not a dictionary"
|
|
260
|
+
# https://issues.apache.org/jira/browse/COUCHDB-1141
|
|
261
|
+
doc = {'_id': doc['_id'], '_rev': doc['_rev'], '_deleted': True}
|
|
262
|
+
self.queue(doc)
|
|
263
|
+
|
|
264
|
+
def commitOne(self, doc, timestamp=False, viewlist=None):
|
|
265
|
+
"""
|
|
266
|
+
Helper function for when you know you only want to insert one doc
|
|
267
|
+
additionally keeps from having to rewrite ConfigCache to handle the
|
|
268
|
+
new commit function's semantics
|
|
269
|
+
"""
|
|
270
|
+
viewlist = viewlist or []
|
|
271
|
+
uri = '/%s/_bulk_docs/' % self.name
|
|
272
|
+
if timestamp:
|
|
273
|
+
self.timestamp(doc, timestamp)
|
|
274
|
+
|
|
275
|
+
data = {'docs': [doc]}
|
|
276
|
+
retval = self.post(uri, data)
|
|
277
|
+
for v in viewlist:
|
|
278
|
+
design, view = v.split('/')
|
|
279
|
+
self.loadView(design, view, {'limit': 0})
|
|
280
|
+
return retval
|
|
281
|
+
|
|
282
|
+
def commit(self, doc=None, returndocs=False, timestamp=False,
|
|
283
|
+
viewlist=None, callback=None, **data):
|
|
284
|
+
"""
|
|
285
|
+
Add doc and/or the contents of self._queue to the database.
|
|
286
|
+
If timestamp is true timestamp all documents with a unix style
|
|
287
|
+
timestamp - this will be the timestamp of when the commit was called, it
|
|
288
|
+
will not override an existing timestamp field. If timestamp is a string
|
|
289
|
+
that string will be used as the label for the timestamp.
|
|
290
|
+
|
|
291
|
+
The callback function will be called with the documents that trigger a
|
|
292
|
+
conflict when doing the bulk post of the documents in the queue,
|
|
293
|
+
callback functions must accept the database object, the data posted and a row in the
|
|
294
|
+
result from the bulk commit. The callback updates the retval with
|
|
295
|
+
its internal retval
|
|
296
|
+
|
|
297
|
+
key, value pairs can be used to pass extra parameters to the bulk doc api
|
|
298
|
+
See https://docs.couchdb.org/en/latest/api/database/bulk-api.html#db-bulk-docs
|
|
299
|
+
|
|
300
|
+
TODO: restore support for returndocs and viewlist
|
|
301
|
+
|
|
302
|
+
Returns a list of good documents
|
|
303
|
+
throws an exception otherwise
|
|
304
|
+
"""
|
|
305
|
+
viewlist = viewlist or []
|
|
306
|
+
if doc:
|
|
307
|
+
self.queue(doc, timestamp, viewlist)
|
|
308
|
+
|
|
309
|
+
if not self._queue:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
if timestamp:
|
|
313
|
+
self.timestamp(self._queue, timestamp)
|
|
314
|
+
# TODO: commit in thread to avoid blocking others
|
|
315
|
+
uri = '/%s/_bulk_docs/' % self.name
|
|
316
|
+
|
|
317
|
+
data['docs'] = list(self._queue)
|
|
318
|
+
retval = self.post(uri, data)
|
|
319
|
+
self._reset_queue()
|
|
320
|
+
for v in viewlist:
|
|
321
|
+
design, view = v.split('/')
|
|
322
|
+
self.loadView(design, view, {'limit': 0})
|
|
323
|
+
if callback:
|
|
324
|
+
for idx, result in enumerate(retval):
|
|
325
|
+
if result.get('error', None) == 'conflict':
|
|
326
|
+
retval[idx] = callback(self, data, result)
|
|
327
|
+
|
|
328
|
+
return retval
|
|
329
|
+
|
|
330
|
+
def document(self, id, rev=None):
|
|
331
|
+
"""
|
|
332
|
+
Load a document identified by id. You can specify a rev to see an older revision
|
|
333
|
+
of the document. This **should only** be used when resolving conflicts, relying
|
|
334
|
+
on CouchDB revisions for document history is not safe, as any compaction will
|
|
335
|
+
remove the older revisions.
|
|
336
|
+
"""
|
|
337
|
+
uri = '/%s/%s' % (self.name, urllib.parse.quote_plus(id))
|
|
338
|
+
if rev:
|
|
339
|
+
uri += '?' + urllib.parse.urlencode({'rev': rev})
|
|
340
|
+
return Document(id=id, inputDict=self.get(uri))
|
|
341
|
+
|
|
342
|
+
def updateDocument(self, doc_id, design, update_func, fields=None, useBody=False):
|
|
343
|
+
"""
|
|
344
|
+
Call the update function update_func defined in the design document
|
|
345
|
+
design for the document doc_id with a query string built from fields.
|
|
346
|
+
|
|
347
|
+
http://wiki.apache.org/couchdb/Document_Update_Handlers
|
|
348
|
+
"""
|
|
349
|
+
fields = fields or {}
|
|
350
|
+
# Clean up /'s in the name etc.
|
|
351
|
+
doc_id = urllib.parse.quote_plus(doc_id)
|
|
352
|
+
|
|
353
|
+
if not useBody:
|
|
354
|
+
updateUri = '/%s/_design/%s/_update/%s/%s?%s' % \
|
|
355
|
+
(self.name, design, update_func, doc_id, urllib.parse.urlencode(fields))
|
|
356
|
+
|
|
357
|
+
return self.put(uri=updateUri, decode=PY3_STR_DECODER)
|
|
358
|
+
else:
|
|
359
|
+
updateUri = '/%s/_design/%s/_update/%s/%s' % \
|
|
360
|
+
(self.name, design, update_func, doc_id)
|
|
361
|
+
return self.put(uri=updateUri, data=fields, decode=PY3_STR_DECODER)
|
|
362
|
+
|
|
363
|
+
def updateBulkDocuments(self, doc_ids, paramsToUpdate, updateLimits=1000):
|
|
364
|
+
|
|
365
|
+
uri = '/%s/_bulk_docs/' % self.name
|
|
366
|
+
conflictDocIDs = []
|
|
367
|
+
for ids in grouper(doc_ids, updateLimits):
|
|
368
|
+
# get original documens
|
|
369
|
+
docs = self.allDocs(options={"include_docs": True}, keys=ids)['rows']
|
|
370
|
+
data = {}
|
|
371
|
+
data['docs'] = []
|
|
372
|
+
for j in docs:
|
|
373
|
+
doc = {}
|
|
374
|
+
doc.update(j['doc'])
|
|
375
|
+
nestedDictUpdate(doc, paramsToUpdate)
|
|
376
|
+
data['docs'].append(doc)
|
|
377
|
+
|
|
378
|
+
if data['docs']:
|
|
379
|
+
retval = self.post(uri, data)
|
|
380
|
+
for result in retval:
|
|
381
|
+
if result.get('error', None) == 'conflict':
|
|
382
|
+
conflictDocIDs.append(result['id'])
|
|
383
|
+
|
|
384
|
+
return conflictDocIDs
|
|
385
|
+
|
|
386
|
+
def updateBulkDocumentsWithConflictHandle(self, doc_ids, updateParams, updateLimits=1000, maxConflictLimit=10):
|
|
387
|
+
"""
|
|
388
|
+
param: doc_ids: list couch doc ids for updates, shouldn't contain any duplicate or empty string
|
|
389
|
+
param: updateParams: dictionary of parameters to be updated.
|
|
390
|
+
param: updateLimits: number of documents in one commit
|
|
391
|
+
pram maxConflictLimit: number of conflicts fix tries before we give up to fix it to prevent infinite calls
|
|
392
|
+
"""
|
|
393
|
+
conflictDocIDs = self.updateBulkDocuments(doc_ids, updateParams, updateLimits)
|
|
394
|
+
if conflictDocIDs:
|
|
395
|
+
# wait a second before trying again for the confict documents
|
|
396
|
+
if maxConflictLimit == 0:
|
|
397
|
+
return conflictDocIDs
|
|
398
|
+
time.sleep(1)
|
|
399
|
+
self.updateBulkDocumentsWithConflictHandle(conflictDocIDs, updateParams,
|
|
400
|
+
maxConflictLimit=maxConflictLimit - 1)
|
|
401
|
+
return []
|
|
402
|
+
|
|
403
|
+
def putDocument(self, doc_id, fields):
|
|
404
|
+
"""
|
|
405
|
+
Call the update function update_func defined in the design document
|
|
406
|
+
design for the document doc_id with a query string built from fields.
|
|
407
|
+
|
|
408
|
+
http://wiki.apache.org/couchdb/Document_Update_Handlers
|
|
409
|
+
"""
|
|
410
|
+
# Clean up /'s in the name etc.
|
|
411
|
+
doc_id = urllib.parse.quote_plus(doc_id)
|
|
412
|
+
|
|
413
|
+
updateUri = '/%s/%s' % (self.name, doc_id)
|
|
414
|
+
return self.put(uri=updateUri, data=fields, decode=PY3_STR_DECODER)
|
|
415
|
+
|
|
416
|
+
def documentExists(self, id, rev=None):
|
|
417
|
+
"""
|
|
418
|
+
Check if a document exists by ID. If specified check that the revision rev exists.
|
|
419
|
+
"""
|
|
420
|
+
uri = "/%s/%s" % (self.name, urllib.parse.quote_plus(id))
|
|
421
|
+
if rev:
|
|
422
|
+
uri += '?' + urllib.parse.urlencode({'rev': rev})
|
|
423
|
+
try:
|
|
424
|
+
self.makeRequest(uri, {}, 'HEAD')
|
|
425
|
+
return True
|
|
426
|
+
except CouchNotFoundError:
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
def delete_doc(self, id, rev=None):
|
|
430
|
+
"""
|
|
431
|
+
Immediately delete a document identified by id and rev.
|
|
432
|
+
If revision is not provided, we need to first fetch this
|
|
433
|
+
document to read the current revision number.
|
|
434
|
+
|
|
435
|
+
:param id: string with the document name
|
|
436
|
+
:param rev: string with the revision number
|
|
437
|
+
:return: an empty dictionary if it fails to fetch the document,
|
|
438
|
+
or a dictionary with the deletion outcome, e.g.:
|
|
439
|
+
{'ok': True, 'id': 'doc_name', 'rev': '3-f68156d'}
|
|
440
|
+
"""
|
|
441
|
+
uri = '/%s/%s' % (self.name, urllib.parse.quote_plus(id))
|
|
442
|
+
if not rev:
|
|
443
|
+
# then we need to fetch the latest revision number
|
|
444
|
+
doc = self.getDoc(id)
|
|
445
|
+
if "_rev" not in doc:
|
|
446
|
+
logging.warning("Failed to retrieve doc id: %s for deletion.", id)
|
|
447
|
+
return doc
|
|
448
|
+
rev = doc["_rev"]
|
|
449
|
+
uri += '?' + urllib.parse.urlencode({'rev': rev})
|
|
450
|
+
return self.delete(uri)
|
|
451
|
+
|
|
452
|
+
def compact(self, views=None, blocking=False, blocking_poll=5, callback=False):
|
|
453
|
+
"""
|
|
454
|
+
Compact the database: http://wiki.apache.org/couchdb/Compaction
|
|
455
|
+
|
|
456
|
+
If given, views should be a list of design document name (minus the
|
|
457
|
+
_design/ - e.g. myviews not _design/myviews). For each view in the list
|
|
458
|
+
view compaction will be triggered. Also, if the views list is provided
|
|
459
|
+
_view_cleanup is called to remove old view output.
|
|
460
|
+
|
|
461
|
+
If True blocking will cause this call to wait until the compaction is
|
|
462
|
+
completed, polling for status with frequency blocking_poll and calling
|
|
463
|
+
the function specified by callback on each iteration.
|
|
464
|
+
|
|
465
|
+
The callback function can be used for logging and could also be used to
|
|
466
|
+
timeout the compaction based on status (e.g. don't time out if compaction
|
|
467
|
+
is less than X% complete. The callback function takes the Database (self)
|
|
468
|
+
as an argument. If the callback function raises an exception the block is
|
|
469
|
+
removed and the compact call returns.
|
|
470
|
+
"""
|
|
471
|
+
views = views or []
|
|
472
|
+
response = self.post('/%s/_compact' % self.name)
|
|
473
|
+
if views:
|
|
474
|
+
for view in views:
|
|
475
|
+
response[view] = self.post('/%s/_compact/%s' % (self.name, view))
|
|
476
|
+
response['view_cleanup'] = self.post('/%s/_view_cleanup' % (self.name))
|
|
477
|
+
|
|
478
|
+
if blocking:
|
|
479
|
+
while self.info()['compact_running']:
|
|
480
|
+
if callback:
|
|
481
|
+
try:
|
|
482
|
+
callback(self)
|
|
483
|
+
except Exception:
|
|
484
|
+
return response
|
|
485
|
+
time.sleep(blocking_poll)
|
|
486
|
+
return response
|
|
487
|
+
|
|
488
|
+
def changes(self, since=-1):
|
|
489
|
+
"""
|
|
490
|
+
Get the changes since sequence number. Store the last sequence value to
|
|
491
|
+
self.last_seq. If the since is negative use self.last_seq.
|
|
492
|
+
"""
|
|
493
|
+
if since < 0:
|
|
494
|
+
since = self.last_seq
|
|
495
|
+
data = self.get('/%s/_changes/?since=%s' % (self.name, since))
|
|
496
|
+
self.last_seq = data['last_seq']
|
|
497
|
+
return data
|
|
498
|
+
|
|
499
|
+
def changesWithFilter(self, filter, limit=1000, since=-1):
|
|
500
|
+
"""
|
|
501
|
+
Get the changes since sequence number. Store the last sequence value to
|
|
502
|
+
self.last_seq. If the since is negative use self.last_seq.
|
|
503
|
+
"""
|
|
504
|
+
if since < 0:
|
|
505
|
+
since = self.last_seq
|
|
506
|
+
data = self.get('/%s/_changes?limit=%s&since=%s&filter=%s' % (self.name, limit, since, filter))
|
|
507
|
+
self.last_seq = data['last_seq']
|
|
508
|
+
return data
|
|
509
|
+
|
|
510
|
+
def purge(self, data):
|
|
511
|
+
return self.post('/%s/_purge' % self.name, data)
|
|
512
|
+
|
|
513
|
+
def loadView(self, design, view, options=None, keys=None):
|
|
514
|
+
"""
|
|
515
|
+
Load a view by getting, for example:
|
|
516
|
+
http://localhost:5984/tester/_view/viewtest/age_name?count=10&group=true
|
|
517
|
+
|
|
518
|
+
The following URL query arguments are allowed:
|
|
519
|
+
|
|
520
|
+
GET
|
|
521
|
+
key=keyvalue
|
|
522
|
+
startkey=keyvalue
|
|
523
|
+
startkey_docid=docid
|
|
524
|
+
endkey=keyvalue
|
|
525
|
+
endkey_docid=docid
|
|
526
|
+
limit=max rows to return
|
|
527
|
+
stale=ok
|
|
528
|
+
descending=true
|
|
529
|
+
skip=number of rows to skip
|
|
530
|
+
group=true Version 0.8.0 and forward
|
|
531
|
+
group_level=int
|
|
532
|
+
reduce=false Trunk only (0.9)
|
|
533
|
+
include_docs=true Trunk only (0.9)
|
|
534
|
+
POST
|
|
535
|
+
{"keys": ["key1", "key2", ...]} Trunk only (0.9)
|
|
536
|
+
|
|
537
|
+
more info: http://wiki.apache.org/couchdb/HTTP_view_API
|
|
538
|
+
"""
|
|
539
|
+
options = options or {}
|
|
540
|
+
keys = keys or []
|
|
541
|
+
encodedOptions = {}
|
|
542
|
+
for k, v in viewitems(options):
|
|
543
|
+
# We can't encode the stale option, as it will be converted to '"ok"'
|
|
544
|
+
# which couch barfs on.
|
|
545
|
+
if k == "stale":
|
|
546
|
+
encodedOptions[k] = v
|
|
547
|
+
else:
|
|
548
|
+
encodedOptions[k] = self.encode(v)
|
|
549
|
+
|
|
550
|
+
if keys:
|
|
551
|
+
if encodedOptions:
|
|
552
|
+
data = urllib.parse.urlencode(encodedOptions)
|
|
553
|
+
retval = self.post('/%s/_design/%s/_view/%s?%s' % \
|
|
554
|
+
(self.name, design, view, data), {'keys': keys})
|
|
555
|
+
else:
|
|
556
|
+
retval = self.post('/%s/_design/%s/_view/%s' % \
|
|
557
|
+
(self.name, design, view), {'keys': keys})
|
|
558
|
+
else:
|
|
559
|
+
retval = self.get('/%s/_design/%s/_view/%s' % \
|
|
560
|
+
(self.name, design, view), encodedOptions)
|
|
561
|
+
if 'error' in retval:
|
|
562
|
+
raise RuntimeError("Error in CouchDB: viewError '%s' reason '%s'" % \
|
|
563
|
+
(retval['error'], retval['reason']))
|
|
564
|
+
else:
|
|
565
|
+
return retval
|
|
566
|
+
|
|
567
|
+
def loadList(self, design, list, view, options=None, keys=None):
|
|
568
|
+
"""
|
|
569
|
+
Load data from a list function. This returns data that hasn't been
|
|
570
|
+
decoded, since a list can return data in any format. It is expected that
|
|
571
|
+
the caller of this function knows what data is being returned and how to
|
|
572
|
+
deal with it appropriately.
|
|
573
|
+
"""
|
|
574
|
+
options = options or {}
|
|
575
|
+
keys = keys or []
|
|
576
|
+
encodedOptions = {}
|
|
577
|
+
for k, v in viewitems(options):
|
|
578
|
+
encodedOptions[k] = self.encode(v)
|
|
579
|
+
|
|
580
|
+
if keys:
|
|
581
|
+
if encodedOptions:
|
|
582
|
+
data = urllib.parse.urlencode(encodedOptions)
|
|
583
|
+
retval = self.post('/%s/_design/%s/_list/%s/%s?%s' % \
|
|
584
|
+
(self.name, design, list, view, data), {'keys': keys},
|
|
585
|
+
decode=PY3_STR_DECODER)
|
|
586
|
+
else:
|
|
587
|
+
retval = self.post('/%s/_design/%s/_list/%s/%s' % \
|
|
588
|
+
(self.name, design, list, view), {'keys': keys},
|
|
589
|
+
decode=PY3_STR_DECODER)
|
|
590
|
+
else:
|
|
591
|
+
retval = self.get('/%s/_design/%s/_list/%s/%s' % \
|
|
592
|
+
(self.name, design, list, view), encodedOptions,
|
|
593
|
+
decode=PY3_STR_DECODER)
|
|
594
|
+
|
|
595
|
+
return retval
|
|
596
|
+
|
|
597
|
+
def getDoc(self, docName):
|
|
598
|
+
"""
|
|
599
|
+
Return a single document from the database.
|
|
600
|
+
"""
|
|
601
|
+
try:
|
|
602
|
+
return self.get('/%s/%s' % (self.name, docName))
|
|
603
|
+
except CouchError as e:
|
|
604
|
+
# if empty dict, then doc does not exist in the db
|
|
605
|
+
if getattr(e, "data", None) == {}:
|
|
606
|
+
return {}
|
|
607
|
+
self.checkForCouchError(getattr(e, "status", None), getattr(e, "reason", None))
|
|
608
|
+
|
|
609
|
+
def allDocs(self, options=None, keys=None):
|
|
610
|
+
"""
|
|
611
|
+
Return all the documents in the database
|
|
612
|
+
options is a dict type parameter which can be passed to _all_docs
|
|
613
|
+
id {'startkey': 'a', 'limit':2, 'include_docs': true}
|
|
614
|
+
keys is the list of key (ids) for doc to be returned
|
|
615
|
+
"""
|
|
616
|
+
options = options or {}
|
|
617
|
+
keys = keys or []
|
|
618
|
+
encodedOptions = {}
|
|
619
|
+
for k, v in viewitems(options):
|
|
620
|
+
encodedOptions[k] = self.encode(v)
|
|
621
|
+
|
|
622
|
+
if keys:
|
|
623
|
+
if encodedOptions:
|
|
624
|
+
data = urllib.parse.urlencode(encodedOptions)
|
|
625
|
+
return self.post('/%s/_all_docs?%s' % (self.name, data),
|
|
626
|
+
{'keys': keys})
|
|
627
|
+
else:
|
|
628
|
+
return self.post('/%s/_all_docs' % self.name,
|
|
629
|
+
{'keys': keys})
|
|
630
|
+
else:
|
|
631
|
+
return self.get('/%s/_all_docs' % self.name, encodedOptions)
|
|
632
|
+
|
|
633
|
+
def info(self):
|
|
634
|
+
"""
|
|
635
|
+
Return information about the databaes (size, number of documents etc).
|
|
636
|
+
"""
|
|
637
|
+
return self.get('/%s/' % self.name)
|
|
638
|
+
|
|
639
|
+
def addAttachment(self, id, rev, value, name=None, contentType=None, checksum=None, add_checksum=False):
|
|
640
|
+
"""
|
|
641
|
+
Add an attachment stored in value to a document identified by id at revision rev.
|
|
642
|
+
If specified the attachement will be uploaded as name, other wise the attachment is
|
|
643
|
+
named "attachment".
|
|
644
|
+
|
|
645
|
+
If not set CouchDB will try to determine contentType and default to text/plain.
|
|
646
|
+
|
|
647
|
+
If checksum is specified pass this to CouchDB, it will refuse if the MD5 checksum
|
|
648
|
+
doesn't match the one provided. If add_checksum is True calculate the checksum of
|
|
649
|
+
the attachment and pass that into CouchDB for validation. The checksum should be the
|
|
650
|
+
base64 encoded binary md5 (as returned by hashlib.md5().digest())
|
|
651
|
+
"""
|
|
652
|
+
if name is None:
|
|
653
|
+
name = "attachment"
|
|
654
|
+
req_headers = {}
|
|
655
|
+
|
|
656
|
+
if add_checksum:
|
|
657
|
+
# calculate base64 encoded MD5
|
|
658
|
+
keyhash = hashlib.md5()
|
|
659
|
+
value_str = str(value) if not isinstance(value, (newstr, newbytes)) else value
|
|
660
|
+
keyhash.update(encodeUnicodeToBytes(value_str))
|
|
661
|
+
content_md5 = base64.b64encode(keyhash.digest())
|
|
662
|
+
req_headers['Content-MD5'] = decodeBytesToUnicode(content_md5) if PY3 else content_md5
|
|
663
|
+
elif checksum:
|
|
664
|
+
req_headers['Content-MD5'] = decodeBytesToUnicode(checksum) if PY3 else checksum
|
|
665
|
+
return self.put('/%s/%s/%s?rev=%s' % (self.name, id, name, rev),
|
|
666
|
+
value, encode=False,
|
|
667
|
+
contentType=contentType,
|
|
668
|
+
incoming_headers=req_headers)
|
|
669
|
+
|
|
670
|
+
def getAttachment(self, id, name="attachment"):
|
|
671
|
+
"""
|
|
672
|
+
_getAttachment_
|
|
673
|
+
|
|
674
|
+
Retrieve an attachment for a couch document.
|
|
675
|
+
"""
|
|
676
|
+
url = "/%s/%s/%s" % (self.name, id, name)
|
|
677
|
+
attachment = self.get(url, None, encode=False, decode=PY3_STR_DECODER)
|
|
678
|
+
|
|
679
|
+
# there has to be a better way to do this but if we're not de-jsoning
|
|
680
|
+
# the return values, then this is all I can do for error checking,
|
|
681
|
+
# right?
|
|
682
|
+
# TODO: MAKE BETTER ERROR HANDLING
|
|
683
|
+
if (attachment.find('{"error":"not_found","reason":"deleted"}') != -1):
|
|
684
|
+
raise RuntimeError("File not found, deleted")
|
|
685
|
+
if id == "nonexistantid":
|
|
686
|
+
print(attachment)
|
|
687
|
+
return attachment
|
|
688
|
+
|
|
689
|
+
def bulkDeleteByIDs(self, ids):
|
|
690
|
+
"""
|
|
691
|
+
delete bulk documents
|
|
692
|
+
"""
|
|
693
|
+
# do the safety check other wise it will delete whole db.
|
|
694
|
+
if not isinstance(ids, list):
|
|
695
|
+
raise RuntimeError("Bulk delete requires a list of ids, wrong data type")
|
|
696
|
+
if not ids:
|
|
697
|
+
return None
|
|
698
|
+
|
|
699
|
+
docs = self.allDocs(keys=ids)['rows']
|
|
700
|
+
for j in docs:
|
|
701
|
+
doc = {}
|
|
702
|
+
if "id" not in j:
|
|
703
|
+
print("Document not found: %s" % j)
|
|
704
|
+
continue
|
|
705
|
+
doc["_id"] = j['id']
|
|
706
|
+
doc["_rev"] = j['value']['rev']
|
|
707
|
+
self.queueDelete(doc)
|
|
708
|
+
return self.commit()
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
class RotatingDatabase(Database):
|
|
712
|
+
"""
|
|
713
|
+
A rotating database is actually multiple databases:
|
|
714
|
+
- one active database (self)
|
|
715
|
+
- N inactive databases (waiting to be removed)
|
|
716
|
+
- one archive database
|
|
717
|
+
- one configuration/seed database
|
|
718
|
+
|
|
719
|
+
The active database is the one which serves current requests. It is active
|
|
720
|
+
for a certain time window and then archived and marked as inactive.
|
|
721
|
+
|
|
722
|
+
Inactive databases no longer recieve queries, although are still available
|
|
723
|
+
on the server. They are queued up for deletion. This allows you to have a
|
|
724
|
+
system where active databases are rotated daily and are kept in the server
|
|
725
|
+
for a week. Inactive databases have a document in them defined as:
|
|
726
|
+
{
|
|
727
|
+
'_id': 'inactive',
|
|
728
|
+
'archived_at': TIMESTAMP, # added when archived
|
|
729
|
+
'expires_at': TIMESTAMP+delta # added when archived
|
|
730
|
+
}
|
|
731
|
+
which is used to persist state across instatiations of the class.
|
|
732
|
+
|
|
733
|
+
The archive database stores the results of views on the active databases
|
|
734
|
+
once they are rotated out of service.
|
|
735
|
+
|
|
736
|
+
The configuration/seed database holds the following information:
|
|
737
|
+
* names of known inactive databases
|
|
738
|
+
* name of current active database
|
|
739
|
+
* name of archive database
|
|
740
|
+
* design documents needed to seed new databases
|
|
741
|
+
|
|
742
|
+
Once rotated the current active database is made inactive, a new active
|
|
743
|
+
database created, views are copied to the archive database as necessary and
|
|
744
|
+
the inactive databases queued for removal.
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
def __init__(self, dbname='database', url='http://localhost:5984',
|
|
748
|
+
size=1000, archivename=None, seedname=None,
|
|
749
|
+
timing=None, views=None):
|
|
750
|
+
"""
|
|
751
|
+
dbaname: base name for databases, active databases will have
|
|
752
|
+
timestamp appended
|
|
753
|
+
url: url of the CouchDB server
|
|
754
|
+
size: how big the data queue can get
|
|
755
|
+
archivename: database to archive view results to, default is
|
|
756
|
+
dbname_archive
|
|
757
|
+
seedname: database where seed views and configuration/state are held
|
|
758
|
+
default is $dbname_seedcfg
|
|
759
|
+
timing: a dict containing two timedeltas 'archive' and 'expire',
|
|
760
|
+
if not present assume the database will br rotated by
|
|
761
|
+
external code
|
|
762
|
+
views: a list of views (design/name) to archive. The assumption
|
|
763
|
+
is that these views have been loaded into the seed
|
|
764
|
+
database via couchapp or someother process.
|
|
765
|
+
"""
|
|
766
|
+
views = views or []
|
|
767
|
+
# Store the base database name
|
|
768
|
+
self.basename = dbname
|
|
769
|
+
|
|
770
|
+
# Since we're going to be making databases hold onto a server
|
|
771
|
+
self.server = CouchServer(url)
|
|
772
|
+
|
|
773
|
+
# self is the "active" database
|
|
774
|
+
Database.__init__(self, self._get_new_name(), url, size)
|
|
775
|
+
# forcibly make sure I exist
|
|
776
|
+
self.server.connectDatabase(self.name)
|
|
777
|
+
|
|
778
|
+
# Set up the databases for the seed
|
|
779
|
+
if not seedname:
|
|
780
|
+
seedname = '%s_seedcfg' % (self.basename)
|
|
781
|
+
self.seed_db = self.server.connectDatabase(seedname, url, size)
|
|
782
|
+
|
|
783
|
+
# TODO: load a rotating DB from the seed db
|
|
784
|
+
|
|
785
|
+
# TODO: Maybe call self._rotate() here?
|
|
786
|
+
|
|
787
|
+
self.timing = timing
|
|
788
|
+
|
|
789
|
+
self.archive_config = {}
|
|
790
|
+
self.archive_db = None
|
|
791
|
+
self.views = []
|
|
792
|
+
if views:
|
|
793
|
+
# If views isn't set in the constructor theres nothing to archive
|
|
794
|
+
if not archivename:
|
|
795
|
+
archivename = '%s_archive' % (self.basename)
|
|
796
|
+
# TODO: check that the views listed exist in the seed
|
|
797
|
+
# TODO: support passing in view options
|
|
798
|
+
self.views = views
|
|
799
|
+
self.archive_db = self.server.connectDatabase(archivename, url, size)
|
|
800
|
+
self.archive_config['views'] = self.views
|
|
801
|
+
self.archive_config['database'] = archivename
|
|
802
|
+
self.archive_config['type'] = 'archive_config'
|
|
803
|
+
self.archive_config['timing'] = str(self.timing)
|
|
804
|
+
# copy views from the seed to the active db
|
|
805
|
+
self._copy_views()
|
|
806
|
+
if self.archive_config:
|
|
807
|
+
# TODO: deal with multiple instances, load from doc?
|
|
808
|
+
self.seed_db.commitOne(self.archive_config)
|
|
809
|
+
|
|
810
|
+
def _get_new_name(self):
|
|
811
|
+
return '%s_%s' % (self.basename, int(time.time()))
|
|
812
|
+
|
|
813
|
+
def _copy_views(self):
|
|
814
|
+
"""
|
|
815
|
+
Copy design documents from self.seed_db to the new active database.
|
|
816
|
+
This means that all views in the design doc are copied, regardless of
|
|
817
|
+
whether they are actually archived.
|
|
818
|
+
"""
|
|
819
|
+
for design_to_copy in set(['_design/%s' % design.split('/')[0] for design in self.views]):
|
|
820
|
+
design = self.seed_db.document(design_to_copy)
|
|
821
|
+
del design['_rev']
|
|
822
|
+
self.queue(design)
|
|
823
|
+
self.commit()
|
|
824
|
+
|
|
825
|
+
def _rotate(self):
|
|
826
|
+
"""
|
|
827
|
+
Rotate the active database:
|
|
828
|
+
1. create the new active database
|
|
829
|
+
2. set self.name to the new database name
|
|
830
|
+
3. write the inactive document to the old active database
|
|
831
|
+
4. write the inactive document to the seed db
|
|
832
|
+
"""
|
|
833
|
+
retiring_db = self.server.connectDatabase(self.name)
|
|
834
|
+
# do the switcheroo
|
|
835
|
+
new_active_db = self.server.connectDatabase(self._get_new_name())
|
|
836
|
+
self.name = new_active_db.name
|
|
837
|
+
self._copy_views()
|
|
838
|
+
# "connect" to the old server, write inactive doc
|
|
839
|
+
retiring_db.commitOne({'_id': 'inactive'}, timestamp=True)
|
|
840
|
+
|
|
841
|
+
# record new inactive db to config
|
|
842
|
+
# TODO: update function?
|
|
843
|
+
|
|
844
|
+
state_doc = {'_id': retiring_db.name, 'rotate_state': 'inactive'}
|
|
845
|
+
if not self.archive_config:
|
|
846
|
+
# Not configured to archive anything, so skip inactive state
|
|
847
|
+
# set the old db as archived instead
|
|
848
|
+
state_doc['rotate_state'] = 'archived'
|
|
849
|
+
self.seed_db.commitOne(state_doc, timestamp=True)
|
|
850
|
+
|
|
851
|
+
def _archive(self):
|
|
852
|
+
"""
|
|
853
|
+
Archive inactive databases
|
|
854
|
+
"""
|
|
855
|
+
if self.archive_config:
|
|
856
|
+
# TODO: This should be a worker thread/pool thingy so it's non-blocking
|
|
857
|
+
for inactive_db in self.inactive_dbs():
|
|
858
|
+
archiving_db = Database(inactive_db, self['host'])
|
|
859
|
+
for view_to_archive in self.views:
|
|
860
|
+
# TODO: improve handling views and options here
|
|
861
|
+
design, view = view_to_archive.split('/')
|
|
862
|
+
for data in archiving_db.loadView(design, view, options={'group': True})['rows']:
|
|
863
|
+
self.archive_db.queue(data)
|
|
864
|
+
self.archive_db.commit()
|
|
865
|
+
# Now set the inactive view to archived
|
|
866
|
+
db_state = self.seed_db.document(inactive_db)
|
|
867
|
+
db_state['rotate_state'] = 'archived'
|
|
868
|
+
self.seed_db.commit(db_state)
|
|
869
|
+
|
|
870
|
+
def _expire(self):
|
|
871
|
+
"""
|
|
872
|
+
Delete inactive databases that have expired, and remove state docs.
|
|
873
|
+
"""
|
|
874
|
+
now = datetime.now()
|
|
875
|
+
then = now - self.timing['expire']
|
|
876
|
+
|
|
877
|
+
options = {'startkey': 0, 'endkey': int(time.mktime(then.timetuple()))}
|
|
878
|
+
expired = self._find_dbs_in_state('archived', options)
|
|
879
|
+
for db in expired:
|
|
880
|
+
try:
|
|
881
|
+
self.server.deleteDatabase(db['id'])
|
|
882
|
+
except CouchNotFoundError:
|
|
883
|
+
# if it's gone we don't care
|
|
884
|
+
pass
|
|
885
|
+
db_state = self.seed_db.document(db['id'])
|
|
886
|
+
self.seed_db.queueDelete(db_state)
|
|
887
|
+
self.seed_db.commit()
|
|
888
|
+
|
|
889
|
+
def _create_design_doc(self):
|
|
890
|
+
"""Create a design doc with a view for the rotate state"""
|
|
891
|
+
tempDesignDoc = {'views': {
|
|
892
|
+
'rotateState': {
|
|
893
|
+
'map': "function(doc) {emit(doc.timestamp, doc.rotate_state, doc._id);}"
|
|
894
|
+
},
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
self.seed_db.put('/%s/_design/TempDesignDoc' % self.seed_db.name, tempDesignDoc)
|
|
898
|
+
|
|
899
|
+
def _find_dbs_in_state(self, state, options=None):
|
|
900
|
+
"""Creates a design document with a single (temporary) view in it"""
|
|
901
|
+
options = options or {}
|
|
902
|
+
if self.seed_db.documentExists("_design/TempDesignDoc"):
|
|
903
|
+
logging.info("Skipping designDoc creation because it already exists!")
|
|
904
|
+
else:
|
|
905
|
+
self._create_design_doc()
|
|
906
|
+
|
|
907
|
+
data = self.seed_db.loadView("TempDesignDoc", "rotateState", options=options)
|
|
908
|
+
return data['rows']
|
|
909
|
+
|
|
910
|
+
def inactive_dbs(self):
|
|
911
|
+
"""
|
|
912
|
+
Return a list on inactive databases
|
|
913
|
+
"""
|
|
914
|
+
return [doc['value'] for doc in self._find_dbs_in_state('inactive')]
|
|
915
|
+
|
|
916
|
+
def archived_dbs(self):
|
|
917
|
+
"""
|
|
918
|
+
Return a list of archived databases
|
|
919
|
+
"""
|
|
920
|
+
return [doc['value'] for doc in self._find_dbs_in_state('archived')]
|
|
921
|
+
|
|
922
|
+
def makeRequest(self, uri=None, data=None, type='GET', incoming_headers=None,
|
|
923
|
+
encode=True, decode=True, contentType=None,
|
|
924
|
+
cache=False, rotate=True):
|
|
925
|
+
"""
|
|
926
|
+
Intercept the request, determine if I need to rotate, then carry out the
|
|
927
|
+
request as normal.
|
|
928
|
+
"""
|
|
929
|
+
incoming_headers = incoming_headers or {}
|
|
930
|
+
if self.timing and rotate:
|
|
931
|
+
|
|
932
|
+
# check to see whether I should rotate the database before processing the request
|
|
933
|
+
db_age = datetime.fromtimestamp(float(self.name.split('_')[-1]))
|
|
934
|
+
db_expires = db_age + self.timing['archive']
|
|
935
|
+
if datetime.now() > db_expires:
|
|
936
|
+
# save the current name for later
|
|
937
|
+
old_db = self.name
|
|
938
|
+
if self._queue:
|
|
939
|
+
# data I've got queued up should go to the old database
|
|
940
|
+
# can't call self.commit() due to recursion
|
|
941
|
+
uri = '/%s/_bulk_docs/' % self.name
|
|
942
|
+
data['docs'] = list(self._queue)
|
|
943
|
+
self.makeRequest(uri, data, 'POST', rotate=False)
|
|
944
|
+
self._reset_queue()
|
|
945
|
+
self._rotate() # make the new database
|
|
946
|
+
# The uri passed in will be wrong, and the db may no longer exist if it has expired
|
|
947
|
+
# so replacte the old name with the new
|
|
948
|
+
uri.replace(old_db, self.name, 1)
|
|
949
|
+
# write the data to the current database
|
|
950
|
+
Database.makeRequest(self, uri, data, type, incoming_headers, encode, decode, contentType, cache)
|
|
951
|
+
# now do some maintenance on the archived/expired databases
|
|
952
|
+
self._archive()
|
|
953
|
+
self._expire()
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
class CouchServer(CouchDBRequests):
|
|
957
|
+
"""
|
|
958
|
+
An object representing the CouchDB server, use it to list, create, delete
|
|
959
|
+
and connect to databases.
|
|
960
|
+
|
|
961
|
+
More info http://wiki.apache.org/couchdb/HTTP_database_API
|
|
962
|
+
"""
|
|
963
|
+
|
|
964
|
+
def __init__(self, dburl='http://localhost:5984', usePYCurl=True, ckey=None, cert=None, capath=None):
|
|
965
|
+
"""
|
|
966
|
+
Set up a connection to the CouchDB server
|
|
967
|
+
"""
|
|
968
|
+
check_server_url(dburl)
|
|
969
|
+
CouchDBRequests.__init__(self, url=dburl, usePYCurl=usePYCurl, ckey=ckey, cert=cert, capath=capath)
|
|
970
|
+
self.url = dburl
|
|
971
|
+
self.ckey = ckey
|
|
972
|
+
self.cert = cert
|
|
973
|
+
|
|
974
|
+
def getCouchWelcome(self):
|
|
975
|
+
"""
|
|
976
|
+
Retrieve CouchDB welcome information (which includes the version number)
|
|
977
|
+
:return: a dictionary
|
|
978
|
+
"""
|
|
979
|
+
return self.get('')
|
|
980
|
+
|
|
981
|
+
def listDatabases(self):
|
|
982
|
+
"List all the databases the server hosts"
|
|
983
|
+
return self.get('/_all_dbs')
|
|
984
|
+
|
|
985
|
+
def createDatabase(self, dbname, size=1000):
|
|
986
|
+
"""
|
|
987
|
+
A database must be named with all lowercase characters (a-z),
|
|
988
|
+
digits (0-9), or any of the _$()+-/ characters and must end with a slash
|
|
989
|
+
in the URL.
|
|
990
|
+
"""
|
|
991
|
+
check_name(dbname)
|
|
992
|
+
|
|
993
|
+
self.put("/%s" % urllib.parse.quote_plus(dbname))
|
|
994
|
+
# Pass the Database constructor the unquoted name - the constructor will
|
|
995
|
+
# quote it for us.
|
|
996
|
+
return Database(dbname=dbname, url=self.url, size=size, ckey=self.ckey, cert=self.cert)
|
|
997
|
+
|
|
998
|
+
def deleteDatabase(self, dbname):
|
|
999
|
+
"""Delete a database from the server"""
|
|
1000
|
+
check_name(dbname)
|
|
1001
|
+
dbname = urllib.parse.quote_plus(dbname)
|
|
1002
|
+
if "cmsweb" in self.url:
|
|
1003
|
+
msg = f"You can't be serious that you want to delete a PRODUCTION database!!! "
|
|
1004
|
+
msg += f"At url: {self.url}, for database name: {dbname}. Bailing out!"
|
|
1005
|
+
raise RuntimeError(msg)
|
|
1006
|
+
return self.delete("/%s" % dbname)
|
|
1007
|
+
|
|
1008
|
+
def connectDatabase(self, dbname='database', create=True, size=1000):
|
|
1009
|
+
"""
|
|
1010
|
+
Return a Database instance, pointing to a database in the server. If the
|
|
1011
|
+
database doesn't exist create it if create is True.
|
|
1012
|
+
"""
|
|
1013
|
+
check_name(dbname)
|
|
1014
|
+
if create and dbname not in self.listDatabases():
|
|
1015
|
+
return self.createDatabase(dbname)
|
|
1016
|
+
return Database(dbname=dbname, url=self.url, size=size, ckey=self.ckey, cert=self.cert)
|
|
1017
|
+
|
|
1018
|
+
def replicate(self, source, destination, continuous=False,
|
|
1019
|
+
create_target=False, cancel=False, doc_ids=False,
|
|
1020
|
+
filter=False, query_params=False, sleepSecs=0, selector=False):
|
|
1021
|
+
"""
|
|
1022
|
+
Trigger replication between source and destination. CouchDB options are
|
|
1023
|
+
defined in: https://docs.couchdb.org/en/3.1.2/api/server/common.html#replicate
|
|
1024
|
+
with further details in: https://docs.couchdb.org/en/stable/replication/replicator.html
|
|
1025
|
+
|
|
1026
|
+
Source and destination need to be appropriately urlquoted after the port
|
|
1027
|
+
number. E.g. if you have a database with /'s in the name you need to
|
|
1028
|
+
convert them into %2F's.
|
|
1029
|
+
|
|
1030
|
+
TODO: Improve source/destination handling - can't simply URL quote,
|
|
1031
|
+
though, would need to decompose the URL and rebuild it.
|
|
1032
|
+
|
|
1033
|
+
:param source: string with the source url to replicate data from
|
|
1034
|
+
:param destination: string with the destination url to replicate data to
|
|
1035
|
+
:param continuous: boolean to perform a continuous replication or not
|
|
1036
|
+
:param create_target: boolean to create the target database, if non-existent
|
|
1037
|
+
:param cancel: boolean to stop a replication (but we better just delete the doc!)
|
|
1038
|
+
:param doc_ids: a list of specific doc ids that we would like to replicate
|
|
1039
|
+
:param filter: string with the name of the filter function to be used. Note that
|
|
1040
|
+
this filter is expected to have been defined in the design doc.
|
|
1041
|
+
:param query_params: dictionary of parameters to pass over to the filter function
|
|
1042
|
+
:param sleepSecs: amount of seconds to sleep after the replication job is created
|
|
1043
|
+
:param selector: a new'ish feature for filter functions in Erlang
|
|
1044
|
+
:return: status of the replication creation
|
|
1045
|
+
"""
|
|
1046
|
+
listDbs = self.listDatabases()
|
|
1047
|
+
if source not in listDbs:
|
|
1048
|
+
check_server_url(source)
|
|
1049
|
+
if destination not in listDbs:
|
|
1050
|
+
if create_target and not destination.startswith("http"):
|
|
1051
|
+
check_name(destination)
|
|
1052
|
+
else:
|
|
1053
|
+
check_server_url(destination)
|
|
1054
|
+
|
|
1055
|
+
if not destination.startswith("http"):
|
|
1056
|
+
destination = '%s/%s' % (self.url, destination)
|
|
1057
|
+
if not source.startswith("http"):
|
|
1058
|
+
source = '%s/%s' % (self.url, source)
|
|
1059
|
+
data = {"source": source, "target": destination}
|
|
1060
|
+
# There must be a nicer way to do this, but I've not had coffee yet...
|
|
1061
|
+
if continuous: data["continuous"] = continuous
|
|
1062
|
+
if create_target: data["create_target"] = create_target
|
|
1063
|
+
if cancel: data["cancel"] = cancel
|
|
1064
|
+
if doc_ids: data["doc_ids"] = doc_ids
|
|
1065
|
+
if filter:
|
|
1066
|
+
data["filter"] = filter
|
|
1067
|
+
if query_params:
|
|
1068
|
+
data["query_params"] = query_params
|
|
1069
|
+
if selector: data["selector"] = selector
|
|
1070
|
+
|
|
1071
|
+
resp = self.post('/_replicator', data)
|
|
1072
|
+
# Sleep required for CouchDB 3.x unit tests
|
|
1073
|
+
time.sleep(sleepSecs)
|
|
1074
|
+
return resp
|
|
1075
|
+
|
|
1076
|
+
def status(self):
|
|
1077
|
+
"""
|
|
1078
|
+
See what active tasks are running on the server.
|
|
1079
|
+
"""
|
|
1080
|
+
return {'databases': self.listDatabases(),
|
|
1081
|
+
'server_stats': self.get('/_stats'),
|
|
1082
|
+
'active_tasks': self.get('/_active_tasks')}
|
|
1083
|
+
|
|
1084
|
+
def __str__(self):
|
|
1085
|
+
"""
|
|
1086
|
+
List all the databases the server has
|
|
1087
|
+
"""
|
|
1088
|
+
return self.listDatabases().__str__()
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
# define some standard couch error classes
|
|
1092
|
+
# from:
|
|
1093
|
+
# http://wiki.apache.org/couchdb/HTTP_status_list
|
|
1094
|
+
|
|
1095
|
+
class CouchError(Exception):
|
|
1096
|
+
"An error thrown by CouchDB"
|
|
1097
|
+
|
|
1098
|
+
def __init__(self, reason, data, result, status=None):
|
|
1099
|
+
Exception.__init__(self)
|
|
1100
|
+
self.reason = reason
|
|
1101
|
+
self.data = data
|
|
1102
|
+
self.result = result
|
|
1103
|
+
self.type = "CouchError"
|
|
1104
|
+
self.status = status
|
|
1105
|
+
|
|
1106
|
+
def __str__(self):
|
|
1107
|
+
"""Stringify the error"""
|
|
1108
|
+
errorMsg = ""
|
|
1109
|
+
if self.type == "CouchError":
|
|
1110
|
+
errorMsg += "A NEW COUCHDB ERROR TYPE/STATUS HAS BEEN FOUND! "
|
|
1111
|
+
errorMsg += "UPDATE CMSCOUCH.PY IMPLEMENTATION WITH A NEW COUCH ERROR/STATUS! "
|
|
1112
|
+
errorMsg += f"Status: {self.status}\n"
|
|
1113
|
+
errorMsg += f"Error type: {self.type}, Status code: {self.status}, "
|
|
1114
|
+
errorMsg += f"Reason: {self.reason}, Data: {repr(self.data)}"
|
|
1115
|
+
return errorMsg
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
class CouchBadRequestError(CouchError):
|
|
1119
|
+
def __init__(self, reason, data, result, status):
|
|
1120
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1121
|
+
self.type = "CouchBadRequestError"
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
class CouchUnauthorisedError(CouchError):
|
|
1125
|
+
def __init__(self, reason, data, result, status):
|
|
1126
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1127
|
+
self.type = "CouchUnauthorisedError"
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
class CouchNotFoundError(CouchError):
|
|
1131
|
+
def __init__(self, reason, data, result, status):
|
|
1132
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1133
|
+
self.type = "CouchNotFoundError"
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
class CouchNotAllowedError(CouchError):
|
|
1137
|
+
def __init__(self, reason, data, result, status):
|
|
1138
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1139
|
+
self.type = "CouchNotAllowedError"
|
|
1140
|
+
|
|
1141
|
+
class CouchNotAcceptableError(CouchError):
|
|
1142
|
+
def __init__(self, reason, data, result, status):
|
|
1143
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1144
|
+
self.type = "CouchNotAcceptableError"
|
|
1145
|
+
|
|
1146
|
+
class CouchConflictError(CouchError):
|
|
1147
|
+
def __init__(self, reason, data, result, status):
|
|
1148
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1149
|
+
self.type = "CouchConflictError"
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
class CouchFeatureGone(CouchError):
|
|
1153
|
+
def __init__(self, reason, data, result, status):
|
|
1154
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1155
|
+
self.type = "CouchFeatureGone"
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
class CouchPreconditionFailedError(CouchError):
|
|
1159
|
+
def __init__(self, reason, data, result, status):
|
|
1160
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1161
|
+
self.type = "CouchPreconditionFailedError"
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
class CouchRequestTooLargeError(CouchError):
|
|
1165
|
+
def __init__(self, reason, data, result, status):
|
|
1166
|
+
# calculate the size of this JSON serialized object
|
|
1167
|
+
docSize = sys.getsizeof(json.dumps(data))
|
|
1168
|
+
errorMsg = f"Document has {docSize} bytes and it's too large to be accepted by CouchDB. "
|
|
1169
|
+
errorMsg += f"Check the CouchDB configuration to see the current value "
|
|
1170
|
+
errorMsg += f"under 'couchdb.max_document_size' (default is 8M bytes)."
|
|
1171
|
+
CouchError.__init__(self, reason, errorMsg, result, status)
|
|
1172
|
+
self.type = "CouchRequestTooLargeError"
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
class CouchExpectationFailedError(CouchError):
|
|
1176
|
+
def __init__(self, reason, data, result, status):
|
|
1177
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1178
|
+
self.type = "CouchExpectationFailedError"
|
|
1179
|
+
|
|
1180
|
+
class CouchRequestedRangeNotSatisfiableError(CouchError):
|
|
1181
|
+
def __init__(self, reason, data, result, status):
|
|
1182
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1183
|
+
self.type = "CouchRequestedRangeNotSatisfiableError"
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
class CouchInternalServerError(CouchError):
|
|
1187
|
+
def __init__(self, reason, data, result, status):
|
|
1188
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1189
|
+
self.type = "CouchInternalServerError"
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
class CouchForbidden(CouchError):
|
|
1193
|
+
def __init__(self, reason, data, result, status):
|
|
1194
|
+
CouchError.__init__(self, reason, data, result, status)
|
|
1195
|
+
self.type = "CouchForbidden"
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
class CouchMonitor(object):
|
|
1199
|
+
def __init__(self, couchURL):
|
|
1200
|
+
if isinstance(couchURL, CouchServer):
|
|
1201
|
+
self.couchServer = couchURL
|
|
1202
|
+
else:
|
|
1203
|
+
self.couchServer = CouchServer(couchURL)
|
|
1204
|
+
|
|
1205
|
+
self.replicatorDB = self.couchServer.connectDatabase('_replicator', False)
|
|
1206
|
+
|
|
1207
|
+
# use the CouchDB version to decide which APIs and schema is available
|
|
1208
|
+
couchInfo = self.couchServer.getCouchWelcome()
|
|
1209
|
+
self.couchVersion = couchInfo.get("version")
|
|
1210
|
+
|
|
1211
|
+
def deleteReplicatorDocs(self, source=None, target=None, repDocs=None):
|
|
1212
|
+
if repDocs is None:
|
|
1213
|
+
repDocs = self.replicatorDB.allDocs(options={'include_docs': True})['rows']
|
|
1214
|
+
|
|
1215
|
+
filteredDocs = self._filterReplicationDocs(repDocs, source, target)
|
|
1216
|
+
if not filteredDocs:
|
|
1217
|
+
return
|
|
1218
|
+
for doc in filteredDocs:
|
|
1219
|
+
self.replicatorDB.queueDelete(doc)
|
|
1220
|
+
return self.replicatorDB.commit()
|
|
1221
|
+
|
|
1222
|
+
def _filterReplicationDocs(self, repDocs, source, target):
|
|
1223
|
+
filteredDocs = []
|
|
1224
|
+
for j in repDocs:
|
|
1225
|
+
if '_design' not in j['id']:
|
|
1226
|
+
if (source is None and target is None) or \
|
|
1227
|
+
(j['doc']['source'] == source and j['doc']['target'] == target):
|
|
1228
|
+
doc = {}
|
|
1229
|
+
doc["_id"] = j['id']
|
|
1230
|
+
doc["_rev"] = j['value']['rev']
|
|
1231
|
+
filteredDocs.append(doc)
|
|
1232
|
+
return filteredDocs
|
|
1233
|
+
|
|
1234
|
+
def getActiveTasks(self):
|
|
1235
|
+
"""
|
|
1236
|
+
Return all the active tasks in Couch (compaction, replication, indexing, etc)
|
|
1237
|
+
:return: a list with the current active tasks
|
|
1238
|
+
|
|
1239
|
+
For further information:
|
|
1240
|
+
https://docs.couchdb.org/en/3.1.2/api/server/common.html#active-tasks
|
|
1241
|
+
"""
|
|
1242
|
+
return self.couchServer.get("/_active_tasks")
|
|
1243
|
+
|
|
1244
|
+
def getSchedulerJobs(self):
|
|
1245
|
+
"""
|
|
1246
|
+
Return all replication jobs created either via _replicate or _replicator dbs.
|
|
1247
|
+
It does not include replications that have either completed or failed.
|
|
1248
|
+
:return: a list with the current replication jobs
|
|
1249
|
+
|
|
1250
|
+
For further information:
|
|
1251
|
+
https://docs.couchdb.org/en/3.1.2/api/server/common.html#api-server-scheduler-jobs
|
|
1252
|
+
"""
|
|
1253
|
+
resp = []
|
|
1254
|
+
data = self.couchServer.get("/_scheduler/jobs")
|
|
1255
|
+
return data.get("jobs", resp)
|
|
1256
|
+
|
|
1257
|
+
def getSchedulerDocs(self):
|
|
1258
|
+
"""
|
|
1259
|
+
Return all replication documents and their states, even if they have completed or
|
|
1260
|
+
failed.
|
|
1261
|
+
:return: a list with the current replication docs
|
|
1262
|
+
|
|
1263
|
+
Replication states can be found at:
|
|
1264
|
+
https://docs.couchdb.org/en/3.1.2/replication/replicator.html#replicator-states
|
|
1265
|
+
For further information:
|
|
1266
|
+
https://docs.couchdb.org/en/3.1.2/api/server/common.html#api-server-scheduler-docs
|
|
1267
|
+
"""
|
|
1268
|
+
# NOTE: if there are no docs, this call can give a response like:
|
|
1269
|
+
# {"error":"not_found","reason":"Database does not exist."}
|
|
1270
|
+
resp = []
|
|
1271
|
+
try:
|
|
1272
|
+
data = self.couchServer.get("/_scheduler/docs")
|
|
1273
|
+
except CouchNotFoundError as exc:
|
|
1274
|
+
logging.warning("/_scheduler/docs API returned: %s", getattr(exc, "result", ""))
|
|
1275
|
+
return resp
|
|
1276
|
+
return data.get("docs", resp)
|
|
1277
|
+
|
|
1278
|
+
def couchReplicationStatus(self):
|
|
1279
|
+
"""
|
|
1280
|
+
check couchdb replication status with compatible output of checkCouchReplications
|
|
1281
|
+
|
|
1282
|
+
:return: a list of dictionaries with the status of the replications and an
|
|
1283
|
+
error message
|
|
1284
|
+
"""
|
|
1285
|
+
# Do not import checkStatus at the beginning of the CMSCouch module
|
|
1286
|
+
# to avoid dependecy conflicts in the CMS Sandbox at the WN level
|
|
1287
|
+
from WMCore.Database.CouchMonitoring import checkStatus
|
|
1288
|
+
|
|
1289
|
+
output = []
|
|
1290
|
+
sdict = checkStatus(kind='scheduler')
|
|
1291
|
+
rdict = checkStatus(kind='replicator')
|
|
1292
|
+
method = 'scheduler+replicator'
|
|
1293
|
+
# update sdict only with entries from replicator dict which are not present in scheduler
|
|
1294
|
+
for key, val in rdict['current_status'].items():
|
|
1295
|
+
if key not in sdict['current_status']:
|
|
1296
|
+
sdict['current_status'][key] = val
|
|
1297
|
+
stateFailures = ['error', 'failed']
|
|
1298
|
+
for rid, record in sdict['current_status'].items():
|
|
1299
|
+
if record['state'] in stateFailures:
|
|
1300
|
+
status['state'] = 'error'
|
|
1301
|
+
source = sanitizeURL(record['source'])
|
|
1302
|
+
target = sanitizeURL(record['target'])
|
|
1303
|
+
error = record['error']
|
|
1304
|
+
history = pformat(record['history'])
|
|
1305
|
+
msg = f"Replication from {source} to {target} for document {rid} is in a bad state: {error}; "
|
|
1306
|
+
msg += f"History: {history}"
|
|
1307
|
+
status = {'name': 'CouchServer', 'status': 'error', 'error_message': msg, 'method': method}
|
|
1308
|
+
output.append(status)
|
|
1309
|
+
|
|
1310
|
+
# if our replication is fine we should check that it is not in a stale phase
|
|
1311
|
+
activeTasks = self.getActiveTasks()
|
|
1312
|
+
activeTasks = [task for task in activeTasks if task["type"].lower() == "replication"]
|
|
1313
|
+
resp = self.checkReplicationState()
|
|
1314
|
+
for replTask in activeTasks:
|
|
1315
|
+
if self.isReplicationStale(replTask):
|
|
1316
|
+
source = sanitizeURL(replTask['source'])['url']
|
|
1317
|
+
target = sanitizeURL(replTask['target'])['url']
|
|
1318
|
+
msg = f"Replication from {source} to {target} is stale and it's last"
|
|
1319
|
+
msg += f"update time was at: {replTask.get('updated_on')}"
|
|
1320
|
+
resp['status'] = 'error'
|
|
1321
|
+
resp['error_message'] += msg
|
|
1322
|
+
resp['method'] = 'stale phase'
|
|
1323
|
+
resp['name'] = 'CouchServer'
|
|
1324
|
+
output.append(resp)
|
|
1325
|
+
# check if we did not record any replication status, then add the ok status
|
|
1326
|
+
if len(output) == 0:
|
|
1327
|
+
status = {'name': 'CouchServer', 'status': 'ok', 'error_message': ''}
|
|
1328
|
+
output.append(status)
|
|
1329
|
+
return output
|
|
1330
|
+
|
|
1331
|
+
def checkCouchReplications(self, replicationsList):
|
|
1332
|
+
"""
|
|
1333
|
+
Check whether the list of expected replications exist in CouchDB
|
|
1334
|
+
and also check their status.
|
|
1335
|
+
|
|
1336
|
+
:param replicationsList: a list of dictionary with the replication
|
|
1337
|
+
document setup.
|
|
1338
|
+
:return: a list of dictionaries with the status of the replications and an
|
|
1339
|
+
error message
|
|
1340
|
+
"""
|
|
1341
|
+
output = []
|
|
1342
|
+
method = 'comparison of replications docs vs active tasks'
|
|
1343
|
+
activeTasks = self.getActiveTasks()
|
|
1344
|
+
# filter out any task that is not a database replication
|
|
1345
|
+
activeTasks = [task for task in activeTasks if task["type"].lower() == "replication"]
|
|
1346
|
+
|
|
1347
|
+
if len(replicationsList) != len(activeTasks):
|
|
1348
|
+
msg = f"Expected to have {len(replicationsList)} replication tasks, "
|
|
1349
|
+
msg += f"but only {len(activeTasks)} in CouchDB. "
|
|
1350
|
+
msg += f"Current replications are: {activeTasks}"
|
|
1351
|
+
status = {'name': 'CouchServer', 'status': 'error', 'error_message': msg, 'method': method}
|
|
1352
|
+
output.append(status)
|
|
1353
|
+
|
|
1354
|
+
resp = self.checkReplicationState()
|
|
1355
|
+
if resp['status'] != 'ok':
|
|
1356
|
+
output.append(resp)
|
|
1357
|
+
|
|
1358
|
+
# finally, check if replications are being updated in a timely fashion
|
|
1359
|
+
for replTask in activeTasks:
|
|
1360
|
+
if not self.isReplicationOK(replTask):
|
|
1361
|
+
source = sanitizeURL(replTask['source'])['url']
|
|
1362
|
+
target = sanitizeURL(replTask['target'])['url']
|
|
1363
|
+
msg = f"Replication from {source} to {target} is stale and it's last"
|
|
1364
|
+
msg += f"update time was at: {replTask.get('updated_on')}"
|
|
1365
|
+
resp['status'] = 'error'
|
|
1366
|
+
resp['error_message'] += msg
|
|
1367
|
+
resp['method'] = method
|
|
1368
|
+
resp['name'] = 'CouchServer'
|
|
1369
|
+
output.append(resp)
|
|
1370
|
+
|
|
1371
|
+
# check if we did not record any replication status, then add the ok status
|
|
1372
|
+
if len(output) == 0:
|
|
1373
|
+
status = {'name': 'CouchServer', 'status': 'ok', 'error_message': ''}
|
|
1374
|
+
output.append(status)
|
|
1375
|
+
return output
|
|
1376
|
+
|
|
1377
|
+
def checkReplicationState(self):
|
|
1378
|
+
"""
|
|
1379
|
+
Check the state of the existent replication tasks.
|
|
1380
|
+
NOTE that this can't be done for CouchDB 1.6, since there is
|
|
1381
|
+
replication state.
|
|
1382
|
+
|
|
1383
|
+
:return: a dictionary with the status of the replications and an
|
|
1384
|
+
error message
|
|
1385
|
+
"""
|
|
1386
|
+
resp = {'status': 'ok', 'error_message': ""}
|
|
1387
|
+
if self.couchVersion == "1.6.1":
|
|
1388
|
+
return resp
|
|
1389
|
+
|
|
1390
|
+
for replDoc in self.getSchedulerDocs():
|
|
1391
|
+
if replDoc['state'].lower() not in ["pending", "running"]:
|
|
1392
|
+
source = sanitizeURL(replDoc['source'])['url']
|
|
1393
|
+
target = sanitizeURL(replDoc['target'])['url']
|
|
1394
|
+
msg = f"Replication from {source} to {target} is in a bad state: {replDoc.get('state')}; "
|
|
1395
|
+
resp['status'] = "error"
|
|
1396
|
+
resp['error_message'] += msg
|
|
1397
|
+
return resp
|
|
1398
|
+
|
|
1399
|
+
def isReplicationOK(self, replInfo):
|
|
1400
|
+
"""
|
|
1401
|
+
Ensure that the replication document is up-to-date as a
|
|
1402
|
+
function of the checkpoint interval.
|
|
1403
|
+
|
|
1404
|
+
:param replInfo: dictionary with the replication information
|
|
1405
|
+
:return: True if replication is working fine, otherwise False
|
|
1406
|
+
"""
|
|
1407
|
+
maxUpdateInterval = replInfo['checkpoint_interval'] / 1000
|
|
1408
|
+
lastUpdate = replInfo["updated_on"]
|
|
1409
|
+
|
|
1410
|
+
if lastUpdate + maxUpdateInterval > int(time.time()):
|
|
1411
|
+
# then it has been recently updated
|
|
1412
|
+
return True
|
|
1413
|
+
return False
|
|
1414
|
+
|
|
1415
|
+
def isReplicationStale(self, replInfo, niter=10):
|
|
1416
|
+
"""
|
|
1417
|
+
Ensure that the replication document is up-to-date as a
|
|
1418
|
+
function of the checkpoint interval.
|
|
1419
|
+
|
|
1420
|
+
:param replInfo: dictionary with the replication information
|
|
1421
|
+
:param niter: number of iteration for checkpoint interval
|
|
1422
|
+
:return: True if replication is working fine, otherwise False
|
|
1423
|
+
"""
|
|
1424
|
+
maxUpdateInterval = niter * replInfo['checkpoint_interval'] / 1000
|
|
1425
|
+
lastUpdate = replInfo["updated_on"]
|
|
1426
|
+
|
|
1427
|
+
if lastUpdate + maxUpdateInterval > int(time.time()):
|
|
1428
|
+
# then it has been recently updated and it means replication is not stale
|
|
1429
|
+
return False
|
|
1430
|
+
return True
|