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.
Files changed (347) hide show
  1. Utils/CPMetrics.py +270 -0
  2. Utils/CertTools.py +100 -0
  3. Utils/EmailAlert.py +50 -0
  4. Utils/ExtendedUnitTestCase.py +62 -0
  5. Utils/FileTools.py +182 -0
  6. Utils/IteratorTools.py +80 -0
  7. Utils/MathUtils.py +31 -0
  8. Utils/MemoryCache.py +119 -0
  9. Utils/Patterns.py +24 -0
  10. Utils/Pipeline.py +137 -0
  11. Utils/PortForward.py +97 -0
  12. Utils/ProcFS.py +112 -0
  13. Utils/ProcessStats.py +194 -0
  14. Utils/PythonVersion.py +17 -0
  15. Utils/Signals.py +36 -0
  16. Utils/TemporaryEnvironment.py +27 -0
  17. Utils/Throttled.py +227 -0
  18. Utils/Timers.py +130 -0
  19. Utils/Timestamps.py +86 -0
  20. Utils/TokenManager.py +143 -0
  21. Utils/Tracing.py +60 -0
  22. Utils/TwPrint.py +98 -0
  23. Utils/Utilities.py +318 -0
  24. Utils/__init__.py +11 -0
  25. Utils/wmcoreDTools.py +707 -0
  26. WMCore/ACDC/Collection.py +57 -0
  27. WMCore/ACDC/CollectionTypes.py +12 -0
  28. WMCore/ACDC/CouchCollection.py +67 -0
  29. WMCore/ACDC/CouchFileset.py +238 -0
  30. WMCore/ACDC/CouchService.py +73 -0
  31. WMCore/ACDC/DataCollectionService.py +485 -0
  32. WMCore/ACDC/Fileset.py +94 -0
  33. WMCore/ACDC/__init__.py +11 -0
  34. WMCore/Algorithms/Alarm.py +39 -0
  35. WMCore/Algorithms/MathAlgos.py +274 -0
  36. WMCore/Algorithms/MiscAlgos.py +67 -0
  37. WMCore/Algorithms/ParseXMLFile.py +115 -0
  38. WMCore/Algorithms/Permissions.py +27 -0
  39. WMCore/Algorithms/Singleton.py +58 -0
  40. WMCore/Algorithms/SubprocessAlgos.py +129 -0
  41. WMCore/Algorithms/__init__.py +7 -0
  42. WMCore/Cache/GenericDataCache.py +98 -0
  43. WMCore/Cache/WMConfigCache.py +572 -0
  44. WMCore/Cache/__init__.py +0 -0
  45. WMCore/Configuration.py +659 -0
  46. WMCore/DAOFactory.py +47 -0
  47. WMCore/DataStructs/File.py +177 -0
  48. WMCore/DataStructs/Fileset.py +140 -0
  49. WMCore/DataStructs/Job.py +182 -0
  50. WMCore/DataStructs/JobGroup.py +142 -0
  51. WMCore/DataStructs/JobPackage.py +49 -0
  52. WMCore/DataStructs/LumiList.py +734 -0
  53. WMCore/DataStructs/Mask.py +219 -0
  54. WMCore/DataStructs/MathStructs/ContinuousSummaryHistogram.py +197 -0
  55. WMCore/DataStructs/MathStructs/DiscreteSummaryHistogram.py +92 -0
  56. WMCore/DataStructs/MathStructs/SummaryHistogram.py +117 -0
  57. WMCore/DataStructs/MathStructs/__init__.py +0 -0
  58. WMCore/DataStructs/Pickleable.py +24 -0
  59. WMCore/DataStructs/Run.py +256 -0
  60. WMCore/DataStructs/Subscription.py +175 -0
  61. WMCore/DataStructs/WMObject.py +47 -0
  62. WMCore/DataStructs/WorkUnit.py +112 -0
  63. WMCore/DataStructs/Workflow.py +60 -0
  64. WMCore/DataStructs/__init__.py +8 -0
  65. WMCore/Database/CMSCouch.py +1430 -0
  66. WMCore/Database/ConfigDBMap.py +29 -0
  67. WMCore/Database/CouchMonitoring.py +450 -0
  68. WMCore/Database/CouchUtils.py +118 -0
  69. WMCore/Database/DBCore.py +198 -0
  70. WMCore/Database/DBCreator.py +113 -0
  71. WMCore/Database/DBExceptionHandler.py +59 -0
  72. WMCore/Database/DBFactory.py +117 -0
  73. WMCore/Database/DBFormatter.py +177 -0
  74. WMCore/Database/Dialects.py +13 -0
  75. WMCore/Database/ExecuteDAO.py +327 -0
  76. WMCore/Database/MongoDB.py +241 -0
  77. WMCore/Database/MySQL/Destroy.py +42 -0
  78. WMCore/Database/MySQL/ListUserContent.py +20 -0
  79. WMCore/Database/MySQL/__init__.py +9 -0
  80. WMCore/Database/MySQLCore.py +132 -0
  81. WMCore/Database/Oracle/Destroy.py +56 -0
  82. WMCore/Database/Oracle/ListUserContent.py +19 -0
  83. WMCore/Database/Oracle/__init__.py +9 -0
  84. WMCore/Database/ResultSet.py +44 -0
  85. WMCore/Database/Transaction.py +91 -0
  86. WMCore/Database/__init__.py +9 -0
  87. WMCore/Database/ipy_profile_couch.py +438 -0
  88. WMCore/GlobalWorkQueue/CherryPyThreads/CleanUpTask.py +29 -0
  89. WMCore/GlobalWorkQueue/CherryPyThreads/HeartbeatMonitor.py +105 -0
  90. WMCore/GlobalWorkQueue/CherryPyThreads/LocationUpdateTask.py +28 -0
  91. WMCore/GlobalWorkQueue/CherryPyThreads/ReqMgrInteractionTask.py +35 -0
  92. WMCore/GlobalWorkQueue/CherryPyThreads/__init__.py +0 -0
  93. WMCore/GlobalWorkQueue/__init__.py +0 -0
  94. WMCore/GroupUser/CouchObject.py +127 -0
  95. WMCore/GroupUser/Decorators.py +51 -0
  96. WMCore/GroupUser/Group.py +33 -0
  97. WMCore/GroupUser/Interface.py +73 -0
  98. WMCore/GroupUser/User.py +96 -0
  99. WMCore/GroupUser/__init__.py +11 -0
  100. WMCore/Lexicon.py +836 -0
  101. WMCore/REST/Auth.py +202 -0
  102. WMCore/REST/CherryPyPeriodicTask.py +166 -0
  103. WMCore/REST/Error.py +333 -0
  104. WMCore/REST/Format.py +642 -0
  105. WMCore/REST/HeartbeatMonitorBase.py +90 -0
  106. WMCore/REST/Main.py +636 -0
  107. WMCore/REST/Server.py +2435 -0
  108. WMCore/REST/Services.py +24 -0
  109. WMCore/REST/Test.py +120 -0
  110. WMCore/REST/Tools.py +38 -0
  111. WMCore/REST/Validation.py +250 -0
  112. WMCore/REST/__init__.py +1 -0
  113. WMCore/ReqMgr/DataStructs/RequestStatus.py +209 -0
  114. WMCore/ReqMgr/DataStructs/RequestType.py +13 -0
  115. WMCore/ReqMgr/DataStructs/__init__.py +0 -0
  116. WMCore/ReqMgr/__init__.py +1 -0
  117. WMCore/Services/AlertManager/AlertManagerAPI.py +111 -0
  118. WMCore/Services/AlertManager/__init__.py +0 -0
  119. WMCore/Services/CRIC/CRIC.py +238 -0
  120. WMCore/Services/CRIC/__init__.py +0 -0
  121. WMCore/Services/DBS/DBS3Reader.py +1044 -0
  122. WMCore/Services/DBS/DBSConcurrency.py +44 -0
  123. WMCore/Services/DBS/DBSErrors.py +112 -0
  124. WMCore/Services/DBS/DBSReader.py +23 -0
  125. WMCore/Services/DBS/DBSUtils.py +166 -0
  126. WMCore/Services/DBS/DBSWriterObjects.py +381 -0
  127. WMCore/Services/DBS/ProdException.py +133 -0
  128. WMCore/Services/DBS/__init__.py +8 -0
  129. WMCore/Services/FWJRDB/FWJRDBAPI.py +118 -0
  130. WMCore/Services/FWJRDB/__init__.py +0 -0
  131. WMCore/Services/HTTPS/HTTPSAuthHandler.py +66 -0
  132. WMCore/Services/HTTPS/__init__.py +0 -0
  133. WMCore/Services/LogDB/LogDB.py +201 -0
  134. WMCore/Services/LogDB/LogDBBackend.py +191 -0
  135. WMCore/Services/LogDB/LogDBExceptions.py +11 -0
  136. WMCore/Services/LogDB/LogDBReport.py +85 -0
  137. WMCore/Services/LogDB/__init__.py +0 -0
  138. WMCore/Services/MSPileup/__init__.py +0 -0
  139. WMCore/Services/MSUtils/MSUtils.py +54 -0
  140. WMCore/Services/MSUtils/__init__.py +0 -0
  141. WMCore/Services/McM/McM.py +173 -0
  142. WMCore/Services/McM/__init__.py +8 -0
  143. WMCore/Services/MonIT/Grafana.py +133 -0
  144. WMCore/Services/MonIT/__init__.py +0 -0
  145. WMCore/Services/PyCondor/PyCondorAPI.py +154 -0
  146. WMCore/Services/PyCondor/__init__.py +0 -0
  147. WMCore/Services/ReqMgr/ReqMgr.py +261 -0
  148. WMCore/Services/ReqMgr/__init__.py +0 -0
  149. WMCore/Services/ReqMgrAux/ReqMgrAux.py +419 -0
  150. WMCore/Services/ReqMgrAux/__init__.py +0 -0
  151. WMCore/Services/RequestDB/RequestDBReader.py +267 -0
  152. WMCore/Services/RequestDB/RequestDBWriter.py +39 -0
  153. WMCore/Services/RequestDB/__init__.py +0 -0
  154. WMCore/Services/Requests.py +624 -0
  155. WMCore/Services/Rucio/Rucio.py +1290 -0
  156. WMCore/Services/Rucio/RucioUtils.py +74 -0
  157. WMCore/Services/Rucio/__init__.py +0 -0
  158. WMCore/Services/RucioConMon/RucioConMon.py +121 -0
  159. WMCore/Services/RucioConMon/__init__.py +0 -0
  160. WMCore/Services/Service.py +400 -0
  161. WMCore/Services/StompAMQ/__init__.py +0 -0
  162. WMCore/Services/TagCollector/TagCollector.py +155 -0
  163. WMCore/Services/TagCollector/XMLUtils.py +98 -0
  164. WMCore/Services/TagCollector/__init__.py +0 -0
  165. WMCore/Services/UUIDLib.py +13 -0
  166. WMCore/Services/UserFileCache/UserFileCache.py +160 -0
  167. WMCore/Services/UserFileCache/__init__.py +8 -0
  168. WMCore/Services/WMAgent/WMAgent.py +63 -0
  169. WMCore/Services/WMAgent/__init__.py +0 -0
  170. WMCore/Services/WMArchive/CMSSWMetrics.py +526 -0
  171. WMCore/Services/WMArchive/DataMap.py +463 -0
  172. WMCore/Services/WMArchive/WMArchive.py +33 -0
  173. WMCore/Services/WMArchive/__init__.py +0 -0
  174. WMCore/Services/WMBS/WMBS.py +97 -0
  175. WMCore/Services/WMBS/__init__.py +0 -0
  176. WMCore/Services/WMStats/DataStruct/RequestInfoCollection.py +300 -0
  177. WMCore/Services/WMStats/DataStruct/__init__.py +0 -0
  178. WMCore/Services/WMStats/WMStatsPycurl.py +145 -0
  179. WMCore/Services/WMStats/WMStatsReader.py +445 -0
  180. WMCore/Services/WMStats/WMStatsWriter.py +273 -0
  181. WMCore/Services/WMStats/__init__.py +0 -0
  182. WMCore/Services/WMStatsServer/WMStatsServer.py +134 -0
  183. WMCore/Services/WMStatsServer/__init__.py +0 -0
  184. WMCore/Services/WorkQueue/WorkQueue.py +492 -0
  185. WMCore/Services/WorkQueue/__init__.py +0 -0
  186. WMCore/Services/__init__.py +8 -0
  187. WMCore/Services/pycurl_manager.py +574 -0
  188. WMCore/WMBase.py +50 -0
  189. WMCore/WMConnectionBase.py +164 -0
  190. WMCore/WMException.py +183 -0
  191. WMCore/WMExceptions.py +269 -0
  192. WMCore/WMFactory.py +76 -0
  193. WMCore/WMInit.py +377 -0
  194. WMCore/WMLogging.py +104 -0
  195. WMCore/WMSpec/ConfigSectionTree.py +442 -0
  196. WMCore/WMSpec/Persistency.py +135 -0
  197. WMCore/WMSpec/Steps/BuildMaster.py +87 -0
  198. WMCore/WMSpec/Steps/BuildTools.py +201 -0
  199. WMCore/WMSpec/Steps/Builder.py +97 -0
  200. WMCore/WMSpec/Steps/Diagnostic.py +89 -0
  201. WMCore/WMSpec/Steps/Emulator.py +62 -0
  202. WMCore/WMSpec/Steps/ExecuteMaster.py +208 -0
  203. WMCore/WMSpec/Steps/Executor.py +210 -0
  204. WMCore/WMSpec/Steps/StepFactory.py +213 -0
  205. WMCore/WMSpec/Steps/TaskEmulator.py +75 -0
  206. WMCore/WMSpec/Steps/Template.py +204 -0
  207. WMCore/WMSpec/Steps/Templates/AlcaHarvest.py +76 -0
  208. WMCore/WMSpec/Steps/Templates/CMSSW.py +613 -0
  209. WMCore/WMSpec/Steps/Templates/DQMUpload.py +59 -0
  210. WMCore/WMSpec/Steps/Templates/DeleteFiles.py +70 -0
  211. WMCore/WMSpec/Steps/Templates/LogArchive.py +84 -0
  212. WMCore/WMSpec/Steps/Templates/LogCollect.py +105 -0
  213. WMCore/WMSpec/Steps/Templates/StageOut.py +105 -0
  214. WMCore/WMSpec/Steps/Templates/__init__.py +10 -0
  215. WMCore/WMSpec/Steps/WMExecutionFailure.py +21 -0
  216. WMCore/WMSpec/Steps/__init__.py +8 -0
  217. WMCore/WMSpec/Utilities.py +63 -0
  218. WMCore/WMSpec/WMSpecErrors.py +12 -0
  219. WMCore/WMSpec/WMStep.py +347 -0
  220. WMCore/WMSpec/WMTask.py +1997 -0
  221. WMCore/WMSpec/WMWorkload.py +2288 -0
  222. WMCore/WMSpec/WMWorkloadTools.py +382 -0
  223. WMCore/WMSpec/__init__.py +9 -0
  224. WMCore/WorkQueue/DataLocationMapper.py +273 -0
  225. WMCore/WorkQueue/DataStructs/ACDCBlock.py +47 -0
  226. WMCore/WorkQueue/DataStructs/Block.py +48 -0
  227. WMCore/WorkQueue/DataStructs/CouchWorkQueueElement.py +148 -0
  228. WMCore/WorkQueue/DataStructs/WorkQueueElement.py +274 -0
  229. WMCore/WorkQueue/DataStructs/WorkQueueElementResult.py +152 -0
  230. WMCore/WorkQueue/DataStructs/WorkQueueElementsSummary.py +185 -0
  231. WMCore/WorkQueue/DataStructs/__init__.py +0 -0
  232. WMCore/WorkQueue/Policy/End/EndPolicyInterface.py +44 -0
  233. WMCore/WorkQueue/Policy/End/SingleShot.py +22 -0
  234. WMCore/WorkQueue/Policy/End/__init__.py +32 -0
  235. WMCore/WorkQueue/Policy/PolicyInterface.py +17 -0
  236. WMCore/WorkQueue/Policy/Start/Block.py +258 -0
  237. WMCore/WorkQueue/Policy/Start/Dataset.py +180 -0
  238. WMCore/WorkQueue/Policy/Start/MonteCarlo.py +131 -0
  239. WMCore/WorkQueue/Policy/Start/ResubmitBlock.py +171 -0
  240. WMCore/WorkQueue/Policy/Start/StartPolicyInterface.py +316 -0
  241. WMCore/WorkQueue/Policy/Start/__init__.py +34 -0
  242. WMCore/WorkQueue/Policy/__init__.py +57 -0
  243. WMCore/WorkQueue/WMBSHelper.py +772 -0
  244. WMCore/WorkQueue/WorkQueue.py +1237 -0
  245. WMCore/WorkQueue/WorkQueueBackend.py +750 -0
  246. WMCore/WorkQueue/WorkQueueBase.py +39 -0
  247. WMCore/WorkQueue/WorkQueueExceptions.py +44 -0
  248. WMCore/WorkQueue/WorkQueueReqMgrInterface.py +278 -0
  249. WMCore/WorkQueue/WorkQueueUtils.py +130 -0
  250. WMCore/WorkQueue/__init__.py +13 -0
  251. WMCore/Wrappers/JsonWrapper/JSONThunker.py +342 -0
  252. WMCore/Wrappers/JsonWrapper/__init__.py +7 -0
  253. WMCore/Wrappers/__init__.py +6 -0
  254. WMCore/__init__.py +10 -0
  255. wmglobalqueue-2.4.5.1.data/data/bin/wmc-dist-patch +15 -0
  256. wmglobalqueue-2.4.5.1.data/data/bin/wmc-dist-unpatch +8 -0
  257. wmglobalqueue-2.4.5.1.data/data/bin/wmc-httpd +3 -0
  258. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/.couchapprc +1 -0
  259. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/README.md +40 -0
  260. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/index.html +264 -0
  261. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/ElementInfoByWorkflow.js +96 -0
  262. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/StuckElementInfo.js +57 -0
  263. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/WorkloadInfoTable.js +80 -0
  264. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/dataTable.js +70 -0
  265. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/namespace.js +23 -0
  266. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/style/main.css +75 -0
  267. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/couchapp.json +4 -0
  268. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/childQueueFilter.js +13 -0
  269. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/filterDeletedDocs.js +3 -0
  270. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/queueFilter.js +11 -0
  271. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/language +1 -0
  272. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/mustache.js +333 -0
  273. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/validate.js +27 -0
  274. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/workqueue_utils.js +61 -0
  275. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/elementsDetail.js +28 -0
  276. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/filter.js +86 -0
  277. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/stuckElements.js +38 -0
  278. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/workRestrictions.js +153 -0
  279. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/workflowSummary.js +28 -0
  280. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/rewrites.json +73 -0
  281. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/shows/redirect.js +23 -0
  282. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/shows/status.js +40 -0
  283. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/ElementSummaryByWorkflow.html +27 -0
  284. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/StuckElementSummary.html +26 -0
  285. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/TaskStatus.html +23 -0
  286. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/WorkflowSummary.html +27 -0
  287. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/workqueue-common-lib.html +2 -0
  288. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib-remote.html +16 -0
  289. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib.html +18 -0
  290. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/updates/in-place.js +50 -0
  291. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/validate_doc_update.js +8 -0
  292. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.couch.app.js +235 -0
  293. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.pathbinder.js +173 -0
  294. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeData/map.js +8 -0
  295. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeData/reduce.js +2 -0
  296. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeParentData/map.js +8 -0
  297. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeParentData/reduce.js +2 -0
  298. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activePileupData/map.js +8 -0
  299. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activePileupData/reduce.js +2 -0
  300. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/analyticsData/map.js +11 -0
  301. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/analyticsData/reduce.js +1 -0
  302. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/availableByPriority/map.js +6 -0
  303. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/conflicts/map.js +5 -0
  304. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elements/map.js +5 -0
  305. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByData/map.js +8 -0
  306. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByParent/map.js +8 -0
  307. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByParentData/map.js +8 -0
  308. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByPileupData/map.js +8 -0
  309. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByStatus/map.js +8 -0
  310. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsBySubscription/map.js +6 -0
  311. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/map.js +8 -0
  312. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/reduce.js +3 -0
  313. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsDetailByWorkflowAndStatus/map.js +26 -0
  314. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/map.js +10 -0
  315. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/reduce.js +1 -0
  316. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/map.js +6 -0
  317. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/reduce.js +1 -0
  318. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/map.js +6 -0
  319. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/reduce.js +1 -0
  320. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/map.js +6 -0
  321. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/reduce.js +1 -0
  322. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByRequest/map.js +6 -0
  323. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByRequest/reduce.js +1 -0
  324. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatus/map.js +6 -0
  325. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatus/reduce.js +1 -0
  326. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/map.js +6 -0
  327. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/reduce.js +1 -0
  328. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/openRequests/map.js +6 -0
  329. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/recent-items/map.js +5 -0
  330. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/map.js +6 -0
  331. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/reduce.js +1 -0
  332. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/specsByWorkflow/map.js +5 -0
  333. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/stuckElements/map.js +38 -0
  334. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/map.js +12 -0
  335. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/reduce.js +3 -0
  336. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrl/map.js +6 -0
  337. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrl/reduce.js +2 -0
  338. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/map.js +6 -0
  339. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/reduce.js +2 -0
  340. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/workflowSummary/map.js +9 -0
  341. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/workflowSummary/reduce.js +10 -0
  342. wmglobalqueue-2.4.5.1.dist-info/METADATA +26 -0
  343. wmglobalqueue-2.4.5.1.dist-info/RECORD +347 -0
  344. wmglobalqueue-2.4.5.1.dist-info/WHEEL +5 -0
  345. wmglobalqueue-2.4.5.1.dist-info/licenses/LICENSE +202 -0
  346. wmglobalqueue-2.4.5.1.dist-info/licenses/NOTICE +16 -0
  347. 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