Home | History | Annotate | Download | only in Checks
      1 #! /usr/bin/python
      2 #
      3 # CDDL HEADER START
      4 #
      5 # The contents of this file are subject to the terms of the
      6 # Common Development and Distribution License (the "License").
      7 # You may not use this file except in compliance with the License.
      8 #
      9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
     10 # or http://www.opensolaris.org/os/licensing.
     11 # See the License for the specific language governing permissions
     12 # and limitations under the License.
     13 #
     14 # When distributing Covered Code, include this CDDL HEADER in each
     15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
     16 # If applicable, add the following below this CDDL HEADER, with the
     17 # fields enclosed by brackets "[]" replaced with your own identifying
     18 # information: Portions Copyright [yyyy] [name of copyright owner]
     19 #
     20 # CDDL HEADER END
     21 #
     22 
     23 #
     24 # Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
     25 # Use is subject to license terms.
     26 #
     27 
     28 #
     29 # Various database lookup classes/methods, i.e.:
     30 #     * monaco
     31 #     * bugs.opensolaris.org (b.o.o.)
     32 #     * arc.opensolaris.org/cgi-bin/arc.cgi (for ARC)
     33 #
     34 
     35 import csv
     36 import re
     37 import urllib
     38 import urllib2
     39 import htmllib
     40 import os
     41 from socket import socket, AF_INET, SOCK_STREAM
     42 
     43 from onbld.Checks import onSWAN
     44 
     45 class NonExistentBug(Exception):
     46 	def __str__(self):
     47 		return "Bug %s does not exist" % (Exception.__str__(self))
     48 
     49 class BugDBException(Exception):
     50 	def __str__(self):
     51 		return "Unknown bug database: %s" % (Exception.__str__(self))
     52 
     53 class BugDB(object):
     54 	"""Lookup change requests.
     55 
     56 	Object can be used on or off of SWAN, using either monaco or
     57 	bugs.opensolaris.org as a database.
     58 
     59 	Usage:
     60 	bdb = BugDB()
     61 	r = bdb.lookup("6455550")
     62 	print r["6455550"]["synopsis"]
     63 	r = bdb.lookup(["6455550", "6505625"])
     64 	print r["6505625"]["synopsis"]
     65 	"""
     66 
     67 	def __init__(self, priority = ("bugster",), forceBoo=False):
     68 		"""Create a BugDB object.
     69 
     70 		Keyword argument:
     71 		forceBoo: use b.o.o even from SWAN (default=False)
     72 		priority: use bug databases in this order
     73 		"""
     74 		self.__validBugDB = ["bugster"]
     75 		self.__onSWAN = not forceBoo and onSWAN()
     76 		for database in priority:
     77 			if database not in self.__validBugDB:
     78 				raise BugDBException, database
     79 		self.__priority = priority
     80 
     81 
     82 	def __boobug(self, cr):
     83 		cr = str(cr)
     84 		url = "http://bugs.opensolaris.org/view_bug.do"
     85    		req = urllib2.Request(url, urllib.urlencode({"bug_id": cr}))
     86 		results = {}
     87 		try:
     88 			data = urllib2.urlopen(req).readlines()
     89 		except urllib2.HTTPError, e:
     90 			if e.code != 404:
     91 				print "ERROR: HTTP error at " + \
     92 					req.get_full_url() + \
     93 					" got error: " + str(e.code)
     94 				raise e
     95 			else:
     96 				raise NonExistentBug
     97 		except urllib2.URLError, e:
     98 			print "ERROR: could not connect to " + \
     99 				req.get_full_url() + \
    100 				' got error: "' + e.reason[1] + '"'
    101 			raise e
    102 		htmlParser = htmllib.HTMLParser(None)
    103 		metaHtmlRe = re.compile(r'^<meta name="([^"]+)" content="([^"]*)">$')
    104 		for line in data:
    105 			m = metaHtmlRe.search(line)
    106 			if not m:
    107 				continue
    108 			val = urllib.unquote(m.group(2))
    109 			htmlParser.save_bgn()
    110 			htmlParser.feed(val)
    111 			results[m.group(1)] = htmlParser.save_end()
    112 		htmlParser.close()
    113 
    114 		if "synopsis" not in results:
    115 			raise NonExistentBug(cr)
    116 					
    117 		results["cr_number"] = cr
    118 		results["sub_category"] = results.pop("subcategory")
    119 		results["status"] = results.pop("state")
    120 		results["date_submitted"] = results.pop("submit_date")
    121 		
    122 		return results
    123 
    124 
    125 	def __monaco(self, crs):
    126 		"""Return all info for requested change reports.
    127 
    128 		Argument:
    129 		crs: list of change request ids
    130 
    131 		Returns:
    132 		Dictionary, mapping CR=>dictionary, where the nested dictionary
    133 		is a mapping of field=>value
    134 		"""
    135 
    136 		#
    137 		# See if 'maxcrs' for maximal batch query size is defined
    138 		# if not, default to 200. 
    139 		# This clears the 2499 chars query limit
    140 		#
    141 		try:
    142 			maxcrs
    143 		except NameError:
    144 			maxcrs = 200
    145 		
    146 		i = 0
    147 		results = {}
    148 		data = []
    149 
    150 		while i < len(crs):
    151 			if len(crs) < ( i + maxcrs ):
    152 				j = len(crs)
    153 			else:
    154 				j = i + maxcrs
    155 
    156 			crstmp=crs[i:j]
    157 
    158 			#
    159 			# We request synopsis last, and split on only
    160 			# the number of separators that we expect to
    161 			# see such that a | in the synopsis doesn't
    162 			# throw us out of whack.
    163 			#
    164 			monacoFields = [ "cr_number", "category", "sub_category",
    165 				"area", "release", "build", "responsible_manager",
    166 				"responsible_engineer", "priority", "status", "sub_status",
    167 				"submitted_by", "date_submitted", "synopsis" ]
    168 			cmd = []
    169 			cmd.append("set What = cr." + ', cr.'.join(monacoFields))
    170 			cmd.append("")
    171 			cmd.append("set Which = cr.cr_number in (" + ','.join(crstmp) +")")
    172 			cmd.append("")
    173 			cmd.append("set FinalClauses = order by cr.cr_number")
    174 			cmd.append("")
    175 			cmd.append("doMeta genQuery cr")
    176 			url = "http://hestia.sfbay.sun.com/cgi-bin/expert?format="
    177 			url += "Pipe-delimited+text;Go=2;no_header=on;cmds="
    178 			url += urllib.quote_plus("\n".join(cmd))
    179 			try:
    180 				data += urllib2.urlopen(url).readlines()
    181 			except urllib2.HTTPError, e:
    182 				print "ERROR: HTTP error at " + url + \
    183 					" got error: " + str(e.code)
    184 				raise e
    185 	
    186 			except urllib2.URLError, e:
    187 				print "ERROR: could not connect to " + url + \
    188 					' got error: "' + e.reason[1] + '"'
    189 				raise e
    190 
    191 			i += maxcrs
    192 
    193 		for line in data:
    194 			line = line.rstrip('\n')
    195 			values = line.split('|', len(monacoFields) - 1)
    196 			v = 0
    197 			cr = values[0]
    198 			results[cr] = {}
    199 			for field in monacoFields:
    200 				results[cr][field] = values[v]
    201 				v += 1
    202 
    203 
    204 		return results
    205 
    206 	def lookup(self, crs):
    207 		"""Return all info for requested change reports.
    208 
    209 		Argument:
    210 		crs: one change request id (may be integer, string, or list),
    211 	             or multiple change request ids (must be a list)
    212 
    213 		Returns:
    214 		Dictionary, mapping CR=>dictionary, where the nested dictionary
    215 		is a mapping of field=>value
    216 		"""
    217 		results = {}
    218 		if not isinstance(crs, list):
    219 			crs = [str(crs)]
    220 		for database in self.__priority:
    221 			if database == "bugster":				
    222 				if self.__onSWAN:
    223 					results.update(self.__monaco(crs))
    224 				# else we're off-swan and querying via boo, which we can
    225 				# only do one bug at a time
    226 				else:
    227 					for cr in crs:
    228 						cr = str(cr)
    229 						try:
    230 							results[cr] = self.__boobug(cr)
    231 						except NonExistentBug:
    232 							continue
    233 
    234 			# the CR has already been found by one bug database
    235 			# so don't bother looking it up in the others
    236 			for cr in crs:
    237 				if cr in results:
    238 					crs.remove(cr)
    239 		
    240 		return results
    241 ####################################################################
    242 class ARCException(Exception):
    243 	"""This covers arc.cgi script failure."""
    244 	def __str__(self):
    245 		return "Error retrieving ARC data: %s" % (Exception.__str__(self))
    246 
    247 def ARC(arclist, arcPath=None):
    248 	if not arcPath:
    249 		arcPath = "http://arc.opensolaris.org/cgi-bin/arc.cgi"
    250 	fields = ["present", "arc", "year", "case", "status", "title"]
    251 	opts = [("case", "%s/%s" % (a, c)) for a, c in arclist]
    252 	req = urllib2.Request(arcPath, urllib.urlencode(opts))
    253 	try:
    254 		data = urllib2.urlopen(req).readlines()
    255 	except urllib2.HTTPError, e:
    256 		print "ERROR: HTTP error at " + req.get_full_url() + \
    257 			" got error: " + str(e.code)
    258 		raise e
    259 
    260 	except urllib2.URLError, e:
    261 		print "ERROR: could not connect to " + req.get_full_url() + \
    262 			' got error: "' + e.reason[1] + '"'
    263 		raise e
    264 	ret = {}
    265 	for line in csv.DictReader(data, fields):
    266 		if line["present"] == "exists":
    267 			yc = "%s/%s" % (line["year"], line["case"])
    268 			ret[(line["arc"], yc)] = line["title"]
    269 		elif line["present"] == "fatal":
    270 			raise ARCException(line["arc"])
    271 
    272 	return ret
    273 
    274 ####################################################################
    275 
    276 # Pointers to the webrti server hostname & port to use
    277 # Using it directly is probably not *officially* supported, so we'll
    278 # have a pointer to the official `webrticli` command line interface
    279 # if using a direct socket connection fails for some reason, so we
    280 # have a fallback
    281 WEBRTI_HOST = 'webrti.sfbay.sun.com'
    282 WEBRTI_PORT = 9188
    283 WEBRTICLI = '/net/onnv.sfbay.sun.com/export/onnv-gate/public/bin/webrticli'
    284 
    285 
    286 class RtiException(Exception):
    287 	pass
    288 
    289 class RtiCallFailed(RtiException):
    290 	def __str__(self):
    291 		return "Unable to call webrti: %s" % (RtiException.__str__(self))
    292 
    293 class RtiSystemProblem(RtiException):
    294 	def __str__(self):
    295 		return "RTI status cannot be determined for CR: %s" % (RtiException.__str__(self))
    296 
    297 class RtiIncorrectCR(RtiException):
    298 	def __str__(self):
    299 		return "Incorrect CR number specified: %s" % (RtiException.__str__(self))
    300 
    301 class RtiNotFound(RtiException):
    302 	def __str__(self):
    303 		return "RTI not found for CR: %s" % (RtiException.__str__(self))
    304 
    305 class RtiNeedConsolidation(RtiException):
    306 	def __str__(self):
    307 		return "More than one consolidation has this CR: %s" % (RtiException.__str__(self))
    308 
    309 class RtiBadGate(RtiException):
    310 	def __str__(self):
    311 		return "Incorrect gate name specified: %s" % (RtiException.__str__(self))
    312 
    313 class RtiUnknownException(Exception):
    314 	def __str__(self):
    315 		return "Unknown webrti return code: %s" % (RtiException.__str__(self))
    316 
    317 class RtiOffSwan(RtiException):
    318 	def __str__(self):
    319 		return "RTI status checks need SWAN access: %s" % (RtiException.__str__(self))
    320 
    321 WEBRTI_ERRORS = {
    322 	'1': RtiSystemProblem,
    323 	'2': RtiIncorrectCR,
    324 	'3': RtiNotFound,
    325 	'4': RtiNeedConsolidation,
    326 	'5': RtiBadGate,
    327 }
    328 
    329 # Our Rti object which we'll use to represent an Rti query
    330 # It's really just a wrapper around the Rti connection, and attempts
    331 # to establish a direct socket connection and query the webrti server
    332 # directly (thus avoiding a system/fork/exec call).  If it fails, it
    333 # falls back to the webrticli command line client.
    334 
    335 returnCodeRe = re.compile(r'.*RETURN_CODE=(\d+)')
    336 class Rti:
    337 	"""Lookup an RTI.
    338 
    339 	Usage:
    340 	r = Rti("6640538")
    341 	print r.rtiNumber();
    342 	"""
    343 
    344 	def __init__(self, cr, gate=None, consolidation=None):
    345 		"""Create an Rti object for the specified change request.
    346 
    347 		Argument:
    348 		cr: change request id
    349 
    350 		Keyword arguments, to limit scope of RTI search:
    351 		gate: path to gate workspace (default=None)
    352 		consolidation: consolidation name (default=None)
    353 		"""
    354 
    355 		bufSz = 1024
    356 		addr = (WEBRTI_HOST, WEBRTI_PORT)
    357 		# If the passed 'cr' was given as an int, then wrap it
    358 		# into a string to make our life easier
    359 		if isinstance(cr, int):
    360 			cr = str(cr)
    361 		self.__queryCr = cr
    362 		self.__queryGate = gate
    363 		self.__queryConsolidation = consolidation
    364 
    365 		self.__webRtiOutput = []
    366 		self.__mainCR = []
    367 		self.__rtiNumber = []
    368 		self.__consolidation = []
    369 		self.__project = []
    370 		self.__status = []
    371 		self.__rtiType = []
    372 		try:
    373 			# try to use a direct connection to the
    374 			# webrti server first
    375 			sock = socket(AF_INET, SOCK_STREAM)
    376 			sock.connect(addr)
    377 			command = "WEBRTICLI/1.0\nRTIstatus\n%s\n" % cr
    378 			if consolidation:
    379 				command += "-c\n%s\n" % consolidation
    380 			if gate:
    381 				command += "-g\n%s\n" % gate
    382 			command += "\n"
    383 			sock.send(command)
    384 			dataList = []
    385 			# keep receiving data from the socket until the
    386 			# server closes the connection
    387 			stillReceiving = True
    388 			while stillReceiving:
    389 				dataPiece = sock.recv(bufSz)
    390 				if dataPiece:
    391 					dataList.append(dataPiece)
    392 				else:
    393 					stillReceiving = False
    394 			# create the lines, skipping the first
    395 			# ("WEBRTCLI/1.0\n")
    396 			data = '\n'.join(''.join(dataList).split('\n')[1:])
    397 		except:
    398 			if not onSWAN():
    399 				raise RtiOffSwan(cr)
    400 
    401 			if not os.path.exists(WEBRTICLI):
    402 				raise RtiCallFailed('not found')
    403 
    404 			# fallback to the "supported" webrticli interface
    405 			command = WEBRTICLI
    406 			if consolidation:
    407 				command += " -c " + consolidation
    408 			if gate:
    409 				command += " -g " + gate
    410 			command += " RTIstatus " + cr
    411 
    412 			try:
    413 				cliPipe = os.popen(command)
    414 			except:
    415 				# we couldn't call the webrticli for some
    416 				# reason, so return a failure
    417 				raise RtiCallFailed('unknown')
    418 
    419 			data = cliPipe.readline()
    420 
    421 		# parse the data to see if we got a return code
    422 		# if we did, then that's bad.  if we didn't,
    423 		# then our call was successful
    424 		m = returnCodeRe.search(data)
    425 		if m:
    426 			rc = m.group(1)
    427 			# we got a return code, set it in our
    428 			# object, set the webRtiOutput for debugging
    429 			# or logging, and return a failure
    430 			if rc in WEBRTI_ERRORS:
    431 				exc = WEBRTI_ERRORS[rc]
    432 				if exc == RtiBadGate:
    433 					edata = gate
    434 				else:
    435 					edata = cr
    436 			else:
    437 				exc = RtiUnknownException
    438 				edata = rc
    439 			raise exc(edata)
    440 
    441 		data = data.splitlines()
    442 		# At this point, we should have valid data
    443 		for line in data:	
    444 			line = line.rstrip('\r\n')
    445 			self.__webRtiOutput.append(line) 
    446 			fields = line.split(':')
    447 			self.__mainCR.append(fields[0])
    448 			self.__rtiNumber.append(fields[1])
    449 			self.__consolidation.append(fields[2])
    450 			self.__project.append(fields[3])
    451 			self.__status.append(fields[4])
    452 			self.__rtiType.append(fields[5])
    453 
    454 	# accessors in case callers need the raw data
    455 	def mainCR(self):
    456 		return self.__mainCR
    457 	def rtiNumber(self):
    458 		return self.__rtiNumber
    459 	def consolidation(self):
    460 		return self.__consolidation
    461 	def project(self):
    462 		return self.__project
    463 	def status(self):
    464 		return self.__status
    465 	def rtiType(self):
    466 		return self.__rtiType
    467 	def queryCr(self):
    468 		return self.__queryCr
    469 	def queryGate(self):
    470 		return self.__queryGate
    471 	def queryConsolidation(self):
    472 		return self.__queryConsolidation
    473 
    474 	# in practice, most callers only care about the following
    475 	def accepted(self):
    476 		for status in self.__status:
    477 			if status != "S_ACCEPTED":
    478 				return False
    479 		return True
    480 
    481 	# for logging/debugging in case the caller wants the raw webrti output
    482 	def webRtiOutput(self):
    483 		return self.__webRtiOutput
    484 
    485