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