wucommon.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #! /usr/bin/python3
  2. ################################################################################
  3. # HPCC SYSTEMS software Copyright (C) 2019 HPCC Systems®.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. ################################################################################
  17. import argparse
  18. import logging
  19. import datetime
  20. import traceback
  21. import requests.packages.urllib3
  22. import sys
  23. import inspect
  24. from requests import Session
  25. from zeep import Client
  26. from zeep.transports import Transport
  27. from pathlib import Path
  28. from collections import namedtuple
  29. from zeep.cache import SqliteCache
  30. TestCase = namedtuple('testCase', ['ScopeFilter',
  31. 'NestedFilter',
  32. 'PropertiesToReturn',
  33. 'ScopeOptions',
  34. 'PropertyOptions'])
  35. def safeMkdir(path):
  36. try:
  37. path.mkdir(parents=True)
  38. except FileExistsError as e:
  39. pass
  40. except (FileNotFoundError, PermissionError) as e:
  41. logging.error("'%s' \nExit." % (str(e)))
  42. exit(-1)
  43. except:
  44. print("Unexpected error:" + str(sys.exc_info()[0]) + " (line: " + str(inspect.stack()[0][2]) + ")" )
  45. traceback.print_stack()
  46. exit(-1)
  47. class DebugTransport(Transport):
  48. def post(self, address, message, headers):
  49. self.xml_request = message.decode('utf-8')
  50. response = super().post(address, message, headers)
  51. self.response = response
  52. return response
  53. class Statistics:
  54. def __init__(self):
  55. self.successCount = 0
  56. self.failureCount = 0
  57. def addCount(self, success):
  58. if (success):
  59. self.successCount += 1
  60. else:
  61. self.failureCount += 1
  62. class WuTest:
  63. def connect(self):
  64. # Consume WSDL and generate soap structures
  65. try:
  66. session = Session()
  67. if (self.args.nosslverify):
  68. session.verify = False
  69. session.sslverify = False
  70. self.transport = DebugTransport(cache=SqliteCache(), session=session)
  71. self.wudetails = Client(self.wudetailsservice_wsdl_url, transport=self.transport)
  72. except:
  73. logging.critical ('Unable to connect/obtain WSDL from ' + self.wudetailsservice_wsdl_url)
  74. raise
  75. def __init__(self, maskFields=(), maskMeasureTypes=(), maskVersion=False, maskWUID=False):
  76. argumentparser = argparse.ArgumentParser()
  77. argumentparser.add_argument('-o', '--outdir', help='Results directory', default='results', metavar='dir')
  78. argumentparser.add_argument('-d', '--debug', help='Enable debug', action='store_true', default=False)
  79. argumentparser.add_argument('--logresp', help='Log wudetails responses (in results directory)', action='store_true', default=False)
  80. argumentparser.add_argument('--logreq', help='Log wudetails requests (in results directory)', action='store_true', default=False)
  81. argumentparser.add_argument('-n', '--lastndays', help="Use workunits from last 'n' days", type=int, default=1, metavar='days')
  82. argumentparser.add_argument('--nosslverify', help="Disable SSL certificate verification", action='store_true', default=False)
  83. argumentparser.add_argument('--baseurl', help="Base url for both WUQuery and WUDetails", default='http://localhost:8010', metavar='url')
  84. argumentparser.add_argument('--user', help='Username for authentication', metavar='username')
  85. argumentparser.add_argument('--pw', help='Password for authentication', metavar='password')
  86. argumentparser.add_argument('--httpdigestauth', help='User HTTP digest authentication(Basic auth default)', action='store_true')
  87. self.args = argumentparser.parse_args()
  88. if (self.args.debug):
  89. loglevel = logging.DEBUG
  90. else:
  91. loglevel=logging.INFO
  92. logging.basicConfig(level=loglevel, format='[%(levelname)s] %(message)s')
  93. requests.packages.urllib3.disable_warnings()
  94. self.keydir = 'key'
  95. self.wuqueryservice_wsdl_url = self.args.baseurl + '/WsWorkunits/WUQuery.json?ver_=1.71&wsdl'
  96. self.wudetailsservice_wsdl_url = self.args.baseurl + '/WsWorkunits/WUDetails.json?ver_=1.71&wsdl'
  97. self.outdir = self.args.outdir
  98. if (len(maskFields)==0 and len(maskMeasureTypes)==0 and maskWUID==False and maskVersion==False):
  99. self.enableMasking = False
  100. else:
  101. self.enableMasking = True
  102. self.maskFields = maskFields
  103. self.maskMeasureTypes = maskMeasureTypes
  104. self.maskVersion = maskVersion
  105. self.maskWUID = maskWUID
  106. self.resultdir = Path(self.args.outdir)
  107. safeMkdir(self.resultdir)
  108. self.tcasekeydir = Path(self.keydir)
  109. safeMkdir(self.tcasekeydir)
  110. try:
  111. self.connect()
  112. try:
  113. self.scopeFilter = self.wudetails.get_type('ns0:WUScopeFilter')
  114. self.nestedFilter = self.wudetails.get_type('ns0:WUNestedFilter')
  115. self.propertiesToReturn = self.wudetails.get_type('ns0:WUPropertiesToReturn')
  116. self.scopeOptions = self.wudetails.get_type('ns0:WUScopeOptions')
  117. self.propertyOptions = self.wudetails.get_type('ns0:WUPropertyOptions')
  118. self.extraProperties = self.wudetails.get_type('ns0:WUExtraProperties')
  119. except:
  120. logging.critical ('WSDL different from expected')
  121. raise
  122. except:
  123. sys.exit('Aborting!')
  124. self.wuquery = Client(self.wuqueryservice_wsdl_url, transport=self.transport)
  125. # Mask out fields in the response structure
  126. #
  127. def maskoutFields(self, wudetails_resp, wuid):
  128. try:
  129. if (self.maskWUID and wudetails_resp['WUID']==wuid):
  130. wudetails_resp['WUID'] = '{masked WUID - matches request}'
  131. if (self.maskVersion and wudetails_resp['MaxVersion'].isnumeric()):
  132. wudetails_resp['MaxVersion'] = '{masked number}'
  133. if (wudetails_resp['Scopes'] != None):
  134. for scope in wudetails_resp['Scopes']['Scope']:
  135. properties = scope['Properties']
  136. if (properties == None):
  137. continue
  138. for property in properties['Property']:
  139. if ((property['Name'] in self.maskFields) or (property['Measure'] in self.maskMeasureTypes)):
  140. if (property['RawValue'] != None):
  141. property['RawValue'] = '{masked}'
  142. if (property['Formatted'] != None):
  143. property['Formatted'] = '{masked}'
  144. property['Creator'] = '{masked}'
  145. except:
  146. logging.critical('Unable to process WUDetails response: %s', wuid)
  147. raise
  148. def makeWuDetailsRequest(self,testfilename,wuid,tcase):
  149. outfile = (self.resultdir / testfilename).with_suffix('.json')
  150. errfile = outfile.with_suffix('.err')
  151. if (outfile.exists()): outfile.unlink()
  152. if (errfile.exists()): errfile.unlink()
  153. try:
  154. wuresp = self.wudetails.service.WUDetails(WUID=wuid,
  155. ScopeFilter=tcase.ScopeFilter,
  156. NestedFilter=tcase.NestedFilter,
  157. PropertiesToReturn=tcase.PropertiesToReturn,
  158. ScopeOptions=tcase.ScopeOptions,
  159. PropertyOptions=tcase.PropertyOptions)
  160. except:
  161. logging.critical('Unable to submit WUDetails request: %s', testfilename)
  162. raise
  163. finally:
  164. if (self.args.logreq):
  165. reqfile = outfile.with_suffix('.req')
  166. try:
  167. if (reqfile.exists()): reqfile.unlink()
  168. with reqfile.open(mode='w') as f:
  169. print (self.transport.xml_request, file=f)
  170. except:
  171. logging.critical('Unable write logrequest to file: %s', reqfile)
  172. pass
  173. if (self.args.logresp):
  174. respfile = outfile.with_suffix('.resp')
  175. if (respfile.exists()): respfile.unlink()
  176. with respfile.open(mode='w') as f:
  177. print (wuresp, file=f)
  178. if (self.enableMasking):
  179. self.maskoutFields(wuresp, wuid)
  180. return wuresp
  181. # Get a list of workunits that will be used for testing wudetails
  182. def getTestWorkunits(self, requiredJobs):
  183. # Calculate date range (LastNDays not processed correctly by wuquery)
  184. enddate = datetime.datetime.now()
  185. startdate = enddate - datetime.timedelta(days=self.args.lastndays)
  186. self.matchedWU = {}
  187. logging.debug ('Gathering Workunits')
  188. for reqjob in requiredJobs:
  189. reqJobname = reqjob[0]
  190. reqClusters = reqjob[1]
  191. nextPage = 0
  192. while (nextPage >=0):
  193. wuqueryresp = self.wuquery.service.WUQuery(Owner='regress',
  194. State='completed',
  195. PageSize=500,
  196. PageStartFrom=nextPage,
  197. LastNDays=self.args.lastndays,
  198. StartDate=startdate.strftime('%Y-%m-%dT00:00:00'),
  199. EndDate=enddate.strftime('%Y-%m-%dT23:59:59'),
  200. Jobname=reqJobname + '*',
  201. Descending='1')
  202. try:
  203. nextPage = wuqueryresp['NextPage']
  204. workunits = wuqueryresp['Workunits']['ECLWorkunit']
  205. except:
  206. break
  207. try:
  208. logging.debug('jobname %s count: %d', reqJobname,len(workunits))
  209. workunits.sort(key=lambda k: k['Jobname'], reverse=True)
  210. # Extract jobname from jobname with date postfix
  211. for wu in workunits:
  212. s = wu['Jobname'].split('-')
  213. cluster = wu['Cluster']
  214. if (len(s) >2):
  215. sep = '-'
  216. job = sep.join(s[0:len(s)-2])
  217. else:
  218. job = wu['Jobname']
  219. key = job + '_' + cluster
  220. if ( (job == reqJobname) and (cluster in reqClusters) and (key not in self.matchedWU)):
  221. self.matchedWU[key] = wu['Wuid']
  222. except:
  223. logging.error('Unexpected response from WUQuery: %s', wuqueryresp)
  224. raise
  225. return self.matchedWU
  226. def getMissingJobCount(self,requiredJobs):
  227. missingjobcount = 0
  228. for reqjob in requiredJobs:
  229. jobname = reqjob[0]
  230. for cluster in reqjob[1]:
  231. key = jobname + '_' + cluster
  232. if (key not in self.matchedWU):
  233. logging.error('Missing job: %s (%s)', jobname, cluster)
  234. missingjobcount += 1
  235. return missingjobcount
  236. def getMatchedJobCount(self):
  237. return len(self.matchedWU)