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 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 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 281WEBRTI_HOST = 'webrti.sfbay.sun.com' 282WEBRTI_PORT = 9188 283WEBRTICLI = '/net/onnv.sfbay.sun.com/export/onnv-gate/public/bin/webrticli' 284 285 286class RtiException(Exception): 287 pass 288 289class RtiCallFailed(RtiException): 290 def __str__(self): 291 return "Unable to call webrti: %s" % (RtiException.__str__(self)) 292 293class RtiSystemProblem(RtiException): 294 def __str__(self): 295 return "RTI status cannot be determined for CR: %s" % (RtiException.__str__(self)) 296 297class RtiIncorrectCR(RtiException): 298 def __str__(self): 299 return "Incorrect CR number specified: %s" % (RtiException.__str__(self)) 300 301class RtiNotFound(RtiException): 302 def __str__(self): 303 return "RTI not found for CR: %s" % (RtiException.__str__(self)) 304 305class RtiNeedConsolidation(RtiException): 306 def __str__(self): 307 return "More than one consolidation has this CR: %s" % (RtiException.__str__(self)) 308 309class RtiBadGate(RtiException): 310 def __str__(self): 311 return "Incorrect gate name specified: %s" % (RtiException.__str__(self)) 312 313class RtiUnknownException(Exception): 314 def __str__(self): 315 return "Unknown webrti return code: %s" % (RtiException.__str__(self)) 316 317class RtiOffSwan(RtiException): 318 def __str__(self): 319 return "RTI status checks need SWAN access: %s" % (RtiException.__str__(self)) 320 321WEBRTI_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 335returnCodeRe = re.compile(r'.*RETURN_CODE=(\d+)') 336class 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