xref: /illumos-gate/usr/src/tools/onbld/Checks/DbLookups.py (revision 8d0c3d29bb99f6521f2dc5058a7e4debebad7899)
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 (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
25#
26
27#
28# Various database lookup classes/methods, i.e.:
29#     * monaco
30#     * bugs.opensolaris.org (b.o.o.)
31#     * arc.opensolaris.org/cgi-bin/arc.cgi (for ARC off SWAN)
32#     * candi.sfbay.sun.com/cgi-bin/arc.cgi (for ARC on SWAN)
33#
34
35import csv
36import re
37import urllib
38import urllib2
39import htmllib
40import os
41from socket import socket, AF_INET, SOCK_STREAM
42
43from onbld.Checks import onSWAN
44
45class NonExistentBug(Exception):
46	def __str__(self):
47		return "Bug %s does not exist" % (Exception.__str__(self))
48
49class BugDBException(Exception):
50	def __str__(self):
51		return "Unknown bug database: %s" % (Exception.__str__(self))
52
53class 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####################################################################
242class ARCException(Exception):
243	"""This covers arc.cgi script failure."""
244	def __str__(self):
245		return "Error retrieving ARC data: %s" % (Exception.__str__(self))
246
247def ARC(arclist, arcPath=None):
248	if not arcPath:
249		if onSWAN():
250			arcPath = "http://candi.sfbay.sun.com/cgi-bin/arc.cgi"
251		else:
252			arcPath = "http://arc.opensolaris.org/cgi-bin/arc.cgi"
253	fields = ["present", "arc", "year", "case", "status", "title"]
254	opts = [("case", "%s/%s" % (a, c)) for a, c in arclist]
255	req = urllib2.Request(arcPath, urllib.urlencode(opts))
256	try:
257		data = urllib2.urlopen(req).readlines()
258	except urllib2.HTTPError, e:
259		print "ERROR: HTTP error at " + req.get_full_url() + \
260			" got error: " + str(e.code)
261		raise e
262
263	except urllib2.URLError, e:
264		print "ERROR: could not connect to " + req.get_full_url() + \
265			' got error: "' + e.reason[1] + '"'
266		raise e
267	ret = {}
268	for line in csv.DictReader(data, fields):
269		if line["present"] == "exists":
270			yc = "%s/%s" % (line["year"], line["case"])
271			ret[(line["arc"], yc)] = line["title"]
272		elif line["present"] == "fatal":
273			raise ARCException(line["arc"])
274
275	return ret
276
277####################################################################
278
279# Pointers to the webrti server hostname & port to use
280# Using it directly is probably not *officially* supported, so we'll
281# have a pointer to the official `webrticli` command line interface
282# if using a direct socket connection fails for some reason, so we
283# have a fallback
284WEBRTI_HOST = 'webrti.sfbay.sun.com'
285WEBRTI_PORT = 9188
286WEBRTICLI = '/net/onnv.sfbay.sun.com/export/onnv-gate/public/bin/webrticli'
287
288
289class RtiException(Exception):
290	pass
291
292class RtiCallFailed(RtiException):
293	def __str__(self):
294		return "Unable to call webrti: %s" % (RtiException.__str__(self))
295
296class RtiSystemProblem(RtiException):
297	def __str__(self):
298		return "RTI status cannot be determined for CR: %s" % (RtiException.__str__(self))
299
300class RtiIncorrectCR(RtiException):
301	def __str__(self):
302		return "Incorrect CR number specified: %s" % (RtiException.__str__(self))
303
304class RtiNotFound(RtiException):
305	def __str__(self):
306		return "RTI not found for CR: %s" % (RtiException.__str__(self))
307
308class RtiNeedConsolidation(RtiException):
309	def __str__(self):
310		return "More than one consolidation has this CR: %s" % (RtiException.__str__(self))
311
312class RtiBadGate(RtiException):
313	def __str__(self):
314		return "Incorrect gate name specified: %s" % (RtiException.__str__(self))
315
316class RtiUnknownException(Exception):
317	def __str__(self):
318		return "Unknown webrti return code: %s" % (RtiException.__str__(self))
319
320class RtiOffSwan(RtiException):
321	def __str__(self):
322		return "RTI status checks need SWAN access: %s" % (RtiException.__str__(self))
323
324WEBRTI_ERRORS = {
325	'1': RtiSystemProblem,
326	'2': RtiIncorrectCR,
327	'3': RtiNotFound,
328	'4': RtiNeedConsolidation,
329	'5': RtiBadGate,
330}
331
332# Our Rti object which we'll use to represent an Rti query
333# It's really just a wrapper around the Rti connection, and attempts
334# to establish a direct socket connection and query the webrti server
335# directly (thus avoiding a system/fork/exec call).  If it fails, it
336# falls back to the webrticli command line client.
337
338returnCodeRe = re.compile(r'.*RETURN_CODE=(\d+)')
339class Rti:
340	"""Lookup an RTI.
341
342	Usage:
343	r = Rti("6640538")
344	print r.rtiNumber();
345	"""
346
347	def __init__(self, cr, gate=None, consolidation=None):
348		"""Create an Rti object for the specified change request.
349
350		Argument:
351		cr: change request id
352
353		Keyword arguments, to limit scope of RTI search:
354		gate: path to gate workspace (default=None)
355		consolidation: consolidation name (default=None)
356		"""
357
358		bufSz = 1024
359		addr = (WEBRTI_HOST, WEBRTI_PORT)
360		# If the passed 'cr' was given as an int, then wrap it
361		# into a string to make our life easier
362		if isinstance(cr, int):
363			cr = str(cr)
364		self.__queryCr = cr
365		self.__queryGate = gate
366		self.__queryConsolidation = consolidation
367
368		self.__webRtiOutput = []
369		self.__mainCR = []
370		self.__rtiNumber = []
371		self.__consolidation = []
372		self.__project = []
373		self.__status = []
374		self.__rtiType = []
375		try:
376			# try to use a direct connection to the
377			# webrti server first
378			sock = socket(AF_INET, SOCK_STREAM)
379			sock.connect(addr)
380			command = "WEBRTICLI/1.0\nRTIstatus\n%s\n" % cr
381			if consolidation:
382				command += "-c\n%s\n" % consolidation
383			if gate:
384				command += "-g\n%s\n" % gate
385			command += "\n"
386			sock.send(command)
387			dataList = []
388			# keep receiving data from the socket until the
389			# server closes the connection
390			stillReceiving = True
391			while stillReceiving:
392				dataPiece = sock.recv(bufSz)
393				if dataPiece:
394					dataList.append(dataPiece)
395				else:
396					stillReceiving = False
397			# create the lines, skipping the first
398			# ("WEBRTCLI/1.0\n")
399			data = '\n'.join(''.join(dataList).split('\n')[1:])
400		except:
401			if not onSWAN():
402				raise RtiOffSwan(cr)
403
404			if not os.path.exists(WEBRTICLI):
405				raise RtiCallFailed('not found')
406
407			# fallback to the "supported" webrticli interface
408			command = WEBRTICLI
409			if consolidation:
410				command += " -c " + consolidation
411			if gate:
412				command += " -g " + gate
413			command += " RTIstatus " + cr
414
415			try:
416				cliPipe = os.popen(command)
417			except:
418				# we couldn't call the webrticli for some
419				# reason, so return a failure
420				raise RtiCallFailed('unknown')
421
422			data = cliPipe.readline()
423
424		# parse the data to see if we got a return code
425		# if we did, then that's bad.  if we didn't,
426		# then our call was successful
427		m = returnCodeRe.search(data)
428		if m:
429			rc = m.group(1)
430			# we got a return code, set it in our
431			# object, set the webRtiOutput for debugging
432			# or logging, and return a failure
433			if rc in WEBRTI_ERRORS:
434				exc = WEBRTI_ERRORS[rc]
435				if exc == RtiBadGate:
436					edata = gate
437				else:
438					edata = cr
439			else:
440				exc = RtiUnknownException
441				edata = rc
442			raise exc(edata)
443
444		data = data.splitlines()
445		# At this point, we should have valid data
446		for line in data:
447			line = line.rstrip('\r\n')
448			self.__webRtiOutput.append(line)
449			fields = line.split(':')
450			self.__mainCR.append(fields[0])
451			self.__rtiNumber.append(fields[1])
452			self.__consolidation.append(fields[2])
453			self.__project.append(fields[3])
454			self.__status.append(fields[4])
455			self.__rtiType.append(fields[5])
456
457	# accessors in case callers need the raw data
458	def mainCR(self):
459		return self.__mainCR
460	def rtiNumber(self):
461		return self.__rtiNumber
462	def consolidation(self):
463		return self.__consolidation
464	def project(self):
465		return self.__project
466	def status(self):
467		return self.__status
468	def rtiType(self):
469		return self.__rtiType
470	def queryCr(self):
471		return self.__queryCr
472	def queryGate(self):
473		return self.__queryGate
474	def queryConsolidation(self):
475		return self.__queryConsolidation
476
477	# in practice, most callers only care about the following
478	def accepted(self):
479		for status in self.__status:
480			if status != "S_ACCEPTED":
481				return False
482		return True
483
484	# for logging/debugging in case the caller wants the raw webrti output
485	def webRtiOutput(self):
486		return self.__webRtiOutput
487
488