1#!/usr/local/bin/python3.5 2 3################################################################################################### 4# 5# pyral.context - Python module for tracking Rally connection context 6# 7# used by pyral.restapi 8# 9################################################################################################### 10 11__version__ = (1, 5, 2) 12 13import sys, os 14import platform 15import subprocess 16import time 17import socket 18import json 19import re # we use compile, match 20from pprint import pprint 21import six 22quote = six.moves.urllib.parse.quote 23 24# intra-package imports 25from .rallyresp import RallyRESTResponse 26from .entity import processSchemaInfo, getSchemaItem 27from .entity import InvalidRallyTypeNameError, UnrecognizedAllowedValuesReference 28 29################################################################################################### 30 31__all__ = ["RallyContext", "RallyContextHelper"] 32 33################################################################################################### 34 35INITIAL_REQUEST_TIME_LIMIT = 5 # in seconds 36SERVICE_REQUEST_TIME_LIMIT = 120 # in seconds 37 38IPV4_ADDRESS_PATT = re.compile(r'^\d+\.\d+\.\d+\.\d+$') 39FORMATTED_ID_PATT = re.compile(r'^[A-Z]{1,2}\d+$') 40SCHEME_PREFIX_PATT = re.compile(r'^https?://') 41 42PROJECT_PATH_ELEMENT_SEPARATOR = ' // ' 43 44################################################################################################## 45 46class RallyRESTAPIError(Exception): pass 47 48################################################################################################## 49 50class RallyContext(object): 51 52 def __init__(self, server, user, password, service_url, 53 subscription=None, workspace=None, project=None): 54 self.server = server 55 self.user = user 56 self.password = password 57 self.service_url = service_url 58 self.subs_name = subscription 59 self.workspace = workspace 60 self.project = project 61 62 def asDict(self): 63 context_dict = { 'server' : self.server, 64 'user' : self.user, 65 'password': self.password, 66 'service_url': self.service_url, 67 } 68 if self.subs_name: 69 context_dict['subscription'] = self.subs_name 70 if self.workspace: 71 context_dict['workspace'] = self.workspace 72 if self.project: 73 context_dict['project'] = self.project 74 75 return context_dict 76 77 def subscription(self): 78 return self.subs_name 79 80 def serviceURL(self): 81 return self.service_url 82 83 def identity(self): 84 subs = self.subs_name or 'None' 85 workspace = self.workspace or 'None' 86 project = self.project or 'None' 87 88 return " | ".join([self.server, self.user or "None", self.password, subs, workspace, project]) 89 90 def __repr__(self): 91 return self.identity() 92 93################################################################################################## 94 95class RallyContextHelper(object): 96 97 def __init__(self, agent, server, user, password): 98 self.agent = agent 99 self.server = server 100 self.user = user 101 self.password = password 102 103 # capture this user's User, UserProfile, Subscription records to extract 104 # the workspaces and projects this user has access to (and their defaults) 105 self._subs_name = "" 106 self._subs_workspaces = [] # a list of Workspace "shell" objects 107 self._workspaces = [] 108 self._workspace_ref = {} 109 self._workspace_inflated = {} 110 self._defaultWorkspace = None 111 self._currentWorkspace = None 112 self._inflated = False 113 114 self._projects = {} # key by workspace name with list of projects per workspace 115 self._project_ref = {} # key by workspace name with dict of project_name: project_ref 116 self._project_path = {} # keyed by project ref, value is "base // intermed // leaf", only for "pathed" projects 117 self._defaultProject = None 118 self._currentProject = None 119 self.context = RallyContext(server, user, password, self.agent.serviceURL()) 120 self.defaultContext = self.context # to be updated on check call 121 self.operatingContext = self.context # to be updated on check call 122 123 124 def check(self, server, workspace, project, isolated_workspace): 125 """ 126 Make an initial attempt to contact the Rally web server and retrieve info 127 for the user associated with the credentials supplied upon instantiation. 128 Raise a RallyRESTAPIError if any problem is encountered. 129 Otherwise call our internal method to set some relevant default information 130 from the returned response. 131 This method serves double-duty of verifying that the server can be contacted 132 and speaks Rally WSAPI, and establishes the default workspace and project for 133 the user. 134 """ 135## 136## print(" RallyContextHelper.check starting ...") 137## sys.stdout.flush() 138## 139 socket.setdefaulttimeout(INITIAL_REQUEST_TIME_LIMIT) 140 target_host = server 141 self.isolated_workspace = isolated_workspace 142 143 big_proxy = os.environ.get('HTTPS_PROXY', False) 144 small_proxy = os.environ.get('https_proxy', False) 145 proxy = big_proxy if big_proxy else small_proxy if small_proxy else False 146 proxy_host = False 147 if proxy: 148 if proxy.startswith('http'): 149 proxy = SCHEME_PREFIX_PATT.sub('', proxy) 150 creds, proxy_port, proxy_host = "", "", proxy 151 if proxy.count('@') == 1: 152 creds, proxy = proxy.split('@') 153 proxy_host = proxy 154 if proxy.count(":") == 1: 155 proxy_host, proxy_port = proxy.split(':') 156## 157## print("your proxy host is set to: |%s|" % (proxy_host)) 158## 159 target_host = proxy_host or server 160 161 user_response = self._getUserInfo() 162 subscription = self._loadSubscription() 163 # caller must either specify a valid workspace/project 164 # or must have a DefaultWorkspace/DefaultProject in their UserProfile 165 self._getDefaults(user_response) 166 167 if workspace: 168 workspaces = self._getSubscriptionWorkspaces(subscription, workspace=workspace, limit=10) 169 if not workspaces: 170 problem = "Specified workspace of '%s' either does not exist or the user does not have permission to access that workspace" 171 raise RallyRESTAPIError(problem % workspace) 172 if len(workspaces) > 1: 173 problem = "Multiple workspaces (%d) found with the same name of '%s'. " +\ 174 "You must specify a workspace with a unique name." 175 raise RallyRESTAPIError(problem % (len(workspaces), workspace)) 176 self._currentWorkspace = workspaces[0].Name 177 178 if not workspace and not self._defaultWorkspace: 179 problem = "No Workspace was specified and there is no DefaultWorkspace setting for the user" 180 raise RallyRESTAPIError(problem) 181 182 if not workspace and self._defaultWorkspace: 183 workspaces = self._getSubscriptionWorkspaces(subscription, workspace=self._defaultWorkspace, limit=10) 184 185 if not self.isolated_workspace: 186 self._getSubscriptionWorkspaces(subscription, limit=0) 187## 188## print("ContextHelper _currentWorkspace: %s" % self._currentWorkspace) 189## print("ContextHelper _defaultProject: %s" % self._defaultProject) 190## 191 self._getWorkspacesAndProjects(workspace=self._currentWorkspace, project=self._defaultProject) 192 self._setOperatingContext(project) 193 schema_info = self.agent.getSchemaInfo(self._currentWorkspace) 194 processSchemaInfo(self.getWorkspace(), schema_info) 195 196 197 def _getUserInfo(self): 198 # note the use of the _disableAugments keyword arg in the call 199 user_name_query = 'UserName = "%s"' % self.user 200## 201## print("user_name_query: |%s|" % user_name_query) 202## 203 basic_user_fields = "ObjectID,UserName,DisplayName,FirstName,LastName,Disabled,UserProfile" 204 try: 205 timer_start = time.time() 206 if self.user: 207 response = self.agent.get('User', fetch=basic_user_fields, query=user_name_query, _disableAugments=True) 208 else: 209 response = self.agent.get('User', fetch=basic_user_fields, _disableAugments=True) 210 timer_stop = time.time() 211 except Exception as ex: 212## 213## print("-----") 214## print(str(ex)) 215## 216 if str(ex.args[0]).startswith('404 Service unavailable'): 217 # TODO: discern whether we should mention server or target_host as the culprit 218 raise RallyRESTAPIError("hostname: '%s' non-existent or unreachable" % self.server) 219 else: 220 raise 221 elapsed = timer_stop - timer_start 222## 223## print(f'response.status_code: {response.status_code}') 224## print(f'response data: {repr(response.data)}') 225## 226 if response.status_code != 200: 227## 228## print("context check response:\n%s\n" % response) 229## print("request attempt elapsed time: %6.2f" % elapsed) 230## 231 if response.status_code == 401: 232 raise RallyRESTAPIError("Invalid credentials") 233 234 if response.status_code == 404: 235## 236## print("response.errors: {0}".format(response.errors[0])) 237## 238 if elapsed >= float(INITIAL_REQUEST_TIME_LIMIT): 239 problem = "Request timed out on attempt to reach %s" % self.server 240 elif response.errors and 'certificate verify failed' in str(response.errors[0]): 241 problem = "SSL certificate verification failed" 242 elif response.errors and 'ProxyError' in str(response.errors[0]): 243 mo = re.search(r'ProxyError\((.+)\)$', response.errors[0]) 244 problem = mo.groups()[0][:-1] 245 problem = re.sub(r'NewConnectionError.+>:', '', problem)[:-3] 246 elif response.errors and 'Max retries exceeded with url' in str(response.errors[0]): 247 problem = "Target Rally host: '%s' non-existent or unreachable" % self.server 248 elif response.errors and 'NoneType' in str(response.errors[0]): 249 problem = "Target Rally host: '%s' non-existent or unreachable" % self.server 250 else: 251 sys.stderr.write("404 Response for request\n") 252## 253## sys.stderr.write("\n".join(str(response.errors)) + "\n") 254## 255 if response.warnings: 256 sys.stderr.write("\n".join(str(response.warnings)) + "\n") 257 sys.stderr.flush() 258 problem = "404 Target host: '%s' is either not reachable or doesn't support the Rally WSAPI" % self.server 259 else: # might be a 401 No Authentication or 401 The username or password you entered is incorrect. 260## 261## print(response.status_code) 262## print(response.headers) 263## print(response.errors) 264## 265 if 'The username or password you entered is incorrect.' in str(response.errors[0]): 266 problem = "Invalid credentials" 267 else: 268 error_blurb = response.errors[0][:80] if response.errors else "" 269 problem = "%s %s" % (response.status_code, error_blurb) 270 raise RallyRESTAPIError(problem) 271## 272## print(" RallyContextHelper.check -> _getUserInfo got the User info request response...") 273## print("response resource: %s" % response.resource) 274## print("response status code: %s" % response.status_code) 275## print("response headers: %s" % response.headers) 276## print("response errors: %s" % response.errors) 277## print("response warnings: %s" % response.warnings) 278## print("response resultCount: %s" % response.resultCount) 279## sys.stdout.flush() 280## 281 return response 282 283 284 def _loadSubscription(self): 285 sub = self.agent.get('Subscription', fetch=True, _disableAugments=True) 286 if sub.errors: 287 raise Exception(sub.errors[0]) 288 subscription = sub.next() 289 self._subs_name = subscription.Name 290 self.context.subs_name = subscription.Name 291 return subscription 292 293 294 def _setOperatingContext(self, project_name): 295 """ 296 This is called after we've determined that there is access to what is now 297 in self._currentWorkspace. Query for projects in the self._currentWorkspace. 298 Set the self._defaultProject arbitrarily to the first Project.Name in the returned set, 299 and then thereafter reset that to the project_name parameter value if a match for that 300 exists in the returned set. If the project_name parameter is non-None and there is NOT 301 a match in the returned set raise an Exception stating that fact. 302 """ 303 result = self.agent.get('Project', fetch="Name", workspace=self._currentWorkspace, project=None) 304 305 if not result or result.resultCount == 0: 306 problem = "No accessible Projects found in the Workspace '%s'" % self._defaultWorkspace 307 raise RallyRESTAPIError(problem) 308 309 try: 310 projects = [proj for proj in result] 311 except: 312 problem = "Unable to obtain Project Name values for projects in the '%s' Workspace" 313 raise RallyRESTAPIError(problem % self._defaultWorkspace) 314 315 # does the project_name contain a ' // ' path element separator token? 316 # if so, then we have to sidebar process this 317 if project_name and PROJECT_PATH_ELEMENT_SEPARATOR in project_name: 318 target_project = self._findMultiElementPathToProject(project_name) 319 if not target_project: 320 problem = "No such accessible multi-element-path Project: %s found in the Workspace '%s'" 321 raise RallyRESTAPIError(problem % (project_name, self._currentWorkspace)) 322 # have to set: 323 # self._defaultProject, self._currentProject 324 # self._workspace_ref, self._project_ref 325 # self.defaultContext, self.operatingContext 326 327 else: 328 match_for_default_project = [project for project in projects if project.Name == self._defaultProject] 329 match_for_named_project = [project for project in projects if project.Name == project_name] 330 331 if project_name: 332 if not match_for_named_project: 333 problem = "The current Workspace '%s' does not contain an accessible Project with the name of '%s'" 334 raise RallyRESTAPIError(problem % (self._currentWorkspace, project_name)) 335 else: 336 project = match_for_named_project[0] 337 proj_ref = project._ref 338 self._defaultProject = project.Name 339 self._currentProject = project.Name 340 else: 341 if not match_for_default_project: 342 problem = "The current Workspace '%s' does not contain a Project with the name of '%s'" 343 raise RallyRESTAPIError(problem % (self._currentWorkspace, project_name)) 344 else: 345 project = match_for_default_project[0] 346 proj_ref = project._ref 347 self._defaultProject = project.Name 348 self._currentProject = project.Name 349## 350## print(" Default Workspace : %s" % self._defaultWorkspace) 351## print(" Default Project : %s" % self._defaultProject) 352## 353 if not self._workspaces: 354 self._workspaces = [self._defaultWorkspace] 355 if not self._projects: 356 self._projects = {self._defaultWorkspace : [self._defaultProject]} 357 if not self._workspace_ref: 358 wksp_name, wkspace_ref = self.getWorkspace() 359 short_ref = "/".join(wkspace_ref.split('/')[-2:]) # we only need the 'workspace/<oid>' part to be a valid ref 360 self._workspace_ref = {self._defaultWorkspace : short_ref} 361 if not self._project_ref: 362 short_ref = "/".join(proj_ref.split('/')[-2:]) # we only need the 'project/<oid>' part to be a valid ref 363 self._project_ref = {self._defaultWorkspace : {self._defaultProject : short_ref}} 364 365 self.defaultContext = RallyContext(self.server, self.user, self.password, 366 self.agent.serviceURL(), subscription=self._subs_name, 367 workspace=self._defaultWorkspace, project=self._defaultProject) 368 self.operatingContext = RallyContext(self.server, self.user, self.password, 369 self.agent.serviceURL(), subscription=self._subs_name, 370 workspace=self._currentWorkspace, project=self._currentProject) 371 self.context = self.operatingContext 372## 373## print(" completed _setOperatingContext processing...") 374## 375 376 def _findMultiElementPathToProject(self, project_name): 377 """ 378 Given a project_name in BaseProject // NextLevelProject // TargetProjectName form, 379 determine the existence/accessiblity of each successive path from the BaseProject 380 on towards the full path ending with TargetProjectName. 381 If found return a pyral entity for the TargetProject which will include the ObjectID (oid) 382 after setting an attribute for FullProjectPath with the value of project_name. 383 """ 384 proj_path_elements = project_name.split(PROJECT_PATH_ELEMENT_SEPARATOR) 385 base_path_element = proj_path_elements[0] 386 result = self.agent.get('Project', fetch="Name,ObjectID,Parent", 387 query='Name = "%s"' % base_path_element, 388 workspace=self._currentWorkspace, project=base_path_element, 389 projectScopeDown=False) 390 if not result or (result.errors or result.resultCount != 1): 391 problem = "No such accessible base Project found in the Workspace '%s'" % project_name 392 raise RallyRESTAPIError(problem) 393 base_project = result.next() 394 parent = base_project 395 project_path = [base_project.Name] 396 397 for proj_path_element in proj_path_elements[1:]: 398 project_path.append(proj_path_element) 399 criteria = ['Name = "%s"' % proj_path_element , 'Parent = %s' % parent._ref] 400 result = self.agent.get('Project', fetch="Name,ObjectID,Parent", query=criteria, workspace=self._currentWorkspace, project=parent.ref) 401 if not result or result.errors or result.resultCount != 1: 402 problem = "No such accessible Project found: '%s'" % PROJECT_PATH_ELEMENT_SEPARATOR.join(project_path) 403 raise RallyRESTAPIError(problem) 404 path_el = result.next() 405 parent = path_el 406 if PROJECT_PATH_ELEMENT_SEPARATOR.join(project_path) != project_name: 407 raise RallyRESTAPIError() 408 return path_el 409 410 411 def _getDefaults(self, user_response): 412 """ 413 We have to circumvent the normal machinery as this is part of setting up the 414 normal machinery. So, once having obtained the User object, we grab the 415 User.UserProfile.OID value and issue a GET for that using _getResourceByOID 416 and handling the response (wrapped in a RallyRESTResponse). 417 """ 418## 419## print("in RallyContextHelper._getDefaults, response arg has:") 420## #pprint(response.data[u'Results']) 421## pprint(response.data) 422## 423 user = user_response.next() 424## 425## pprint(response.data[u'Results'][0]) 426## 427 self.user_oid = user.oid 428## 429## print(" RallyContextHelper._getDefaults calling _getResourceByOID to get UserProfile info...") 430## sys.stdout.flush() 431## 432 upraw = self.agent._getResourceByOID(self.context, 'UserProfile', user.UserProfile.oid, _disableAugments=True) 433## 434## print(" RallyContextHelper._getDefaults got the raw UserProfile info via _getResourceByOID...") 435## print(upraw.status_code) 436## print(upraw.content) 437## sys.stdout.flush() 438## 439 resp = RallyRESTResponse(self.agent, self.context, 'UserProfile', upraw, "full", 0) 440 up = resp.data['QueryResult']['Results']['UserProfile'] 441## 442## print("got the UserProfile info...") 443## pprint(up) 444## print("+" * 80) 445## 446 if up['DefaultWorkspace']: 447 self._defaultWorkspace = up['DefaultWorkspace']['_refObjectName'] 448## 449## print(" set _defaultWorkspace to: %s" % self._defaultWorkspace) 450## 451 self._currentWorkspace = self._defaultWorkspace[:] 452 wkspace_ref = up['DefaultWorkspace']['_ref'] 453 else: 454 self._defaultWorkspace = None 455 self._currentWorkspace = None 456 wkspace_ref = None 457 458 if up['DefaultProject']: 459 self._defaultProject = up['DefaultProject']['_refObjectName'] 460 self._currentProject = self._defaultProject[:] 461 proj_ref = up['DefaultProject']['_ref'] 462 else: 463 self._defaultProject = None 464 self._currentProject = None 465 proj_ref = None 466## 467## print(" Default Workspace : %s" % self._defaultWorkspace) 468## print(" Default Project : %s" % self._defaultProject) 469## 470 471 472 def _getSubscriptionWorkspaces(self, subscription, workspace=None, limit=0): 473 wksp_coll_ref_base = "%s/Workspaces" % subscription._ref 474 criteria = "(State = Open)" 475 # if workspace then augment the query 476 if isinstance(workspace, str) and len(workspace) > 0: 477 urlencoded_workspace_name = quote(workspace) 478 criteria = '((Name = "%s") AND %s)' % (urlencoded_workspace_name, criteria) 479 workspaces_collection_url = '%s?fetch=true&query=%s&pagesize=200&start=1' % \ 480 (wksp_coll_ref_base, criteria) 481 timer_start = time.time() 482 workspaces = self.agent.getCollection(workspaces_collection_url, _disableAugments=True) 483 timer_stop = time.time() 484 elapsed = timer_stop - timer_start 485## 486## print("getting the Workspace collection took %5.3f seconds" % elapsed) 487## 488 subscription.Workspaces = [wksp for wksp in workspaces] 489## 490## num_wksps = len(subscription.Workspaces) 491## if not limit: print("Subscription %s has %d active Workspaces" % (subscription.Name, num_wksps)) 492## 493 self._subs_workspaces = subscription.Workspaces 494## 495## print("Subscription default Workspace: %s" % self._defaultWorkspace.Name) 496## 497 return subscription.Workspaces 498 499 500 def currentContext(self): 501 return self.context 502 503 504 def setWorkspace(self, workspace_name): 505## 506## print("in setWorkspace, exising workspace: %s OID: %s" % (self._currentWorkspace, self.currentWorkspaceRef())) 507## 508 if self.isAccessibleWorkspaceName(workspace_name): 509 if workspace_name not in self._workspaces: 510 self._getWorkspacesAndProjects(workspace=workspace_name) 511 # TODO: also nab the schema info for this if it hasn't already been snarfed 512 self._currentWorkspace = workspace_name 513 self.context.workspace = workspace_name 514## 515## print(" current workspace set to: %s OID: %s" % (workspace_name, self.currentWorkspaceRef())) 516## 517 self.resetDefaultProject() 518## 519## print(" context project set to: %s" % self._currentProject) 520## 521 try: 522 # make sure that entity._rally_schema gets filled for this workspace 523 # this will fault and be caught if getSchemaItem raises an Exception 524 getSchemaItem(self.getWorkspace(), 'Defect') 525 except Exception as msg: 526 schema_info = self.agent.getSchemaInfo(self.getWorkspace()) 527 processSchemaInfo(self.getWorkspace(), schema_info) 528 else: 529 raise Exception("Attempt to set workspace to an invalid setting: %s" % workspace_name) 530 531 532 def getWorkspace(self): 533 """ 534 Return a 2 tuple of (name of the current workspace, ref for the current workspace) 535 """ 536 return (self._currentWorkspace, self.currentWorkspaceRef()) 537 538 539 def isAccessibleWorkspaceName(self, workspace_name): 540 """ 541 """ 542 hits = [wksp.Name for wksp in self._subs_workspaces 543 if workspace_name == wksp.Name 544 and str(wksp.State) != 'Closed' 545 ] 546 accessible = True if hits else False 547 return accessible 548 549 550 def getAccessibleWorkspaces(self): 551 """ 552 fill the instance cache items if not already done, then 553 return a list of (workspaceName, workspaceRef) tuples 554 """ 555 if self._inflated != 'wide': 556 self._inflated = 'wide' # to avoid recursion limits hell 557 self._getWorkspacesAndProjects(workspace='*') 558 559 workspaceInfo = [] 560 for workspace in self._workspaces: 561 if workspace in self._workspace_ref: 562 wksp = [wksp for wksp in self._subs_workspaces if wksp.Name == workspace][0] 563 if wksp.State != 'Closed': 564 workspaceInfo.append((workspace, self._workspace_ref[workspace])) 565 return workspaceInfo 566 567 568 def getCurrentWorkspace(self): 569 """ 570 Return the name of the current workspace 571 """ 572 return self._currentWorkspace 573 574 575 def currentWorkspaceRef(self): 576 """ 577 Return the ref associated with the current workspace if you can find one 578 """ 579## 580## print("default workspace: %s" % self._defaultWorkspace) 581## print("current workspace: %s" % self._currentWorkspace) 582## 583 if self._currentWorkspace: 584 return self._workspace_ref[self._currentWorkspace] 585 else: 586 return None 587 588 589 def setProject(self, project_name, name=None): 590 """ 591 Set the current context project with the given project_name. 592 593 If the project_name has the form of a reference, then set the 594 _current_project to that project_name value directly. 595 """ 596## 597## print("ContextHelper.setProject project_name is a %s value: %s" % (type(project_name), project_name)) 598## 599 if re.search(r'project/\d+$', project_name): # is project_name really a ref string? 600 self._currentProject = project_name 601 self.context.project = project_name 602 if name: 603 self._project_path[project_name] = name # recall, project_name here is really a reference string 604 return True 605 606 projects = self.getAccessibleProjects(self._currentWorkspace) 607 hits = [name for name, ref in projects if project_name == name] 608 if hits and len(hits) == 1: 609 self._currentProject = project_name 610 self.context.project = project_name 611 else: 612 raise Exception("Attempt to set project to an invalid setting: %s" % project_name) 613 614 615 def getProject(self): 616 """ 617 Return a two tuple of (name of the current project, ref for the current project) 618 """ 619 if not re.search(r'project/\d+$', self._currentProject): 620 return (self._currentProject, self.currentProjectRef()) 621 cur_project = self._project_path[self._currentProject] or self._currentProject 622 return (cur_project, self.currentProjectRef()) 623 624 625 def getAccessibleProjects(self, workspace='default'): 626 """ 627 Return a list of (projectName, projectRef) tuples 628 """ 629 projectInfo = [] 630 if workspace == 'default' or not workspace: 631 workspace = self._defaultWorkspace 632 elif workspace == 'current': 633 workspace = self._currentWorkspace 634 635 if workspace not in self._workspaces: # can't return anything meaningful then... 636 if self._inflated == 'wide': # can't return anything meaningful then... 637 return projectInfo 638 self._getWorkspacesAndProjects(workspace=workspace) 639 # check self._workspaces again... 640 if workspace not in self._workspaces: 641 return projectInfo 642## else: 643## print(" self._workspaces augmented, now has your target workspace") 644## sys.stdout.flush() 645## 646 for projName, projRef in list(self._project_ref[workspace].items()): 647 projectInfo.append((projName, projRef)) 648 return projectInfo 649 650 651 def resetDefaultProject(self): 652 """ 653 Get the set of current valid projects by calling 654 getAccessibleProjects(self._currentWorkspace) 655 If _currentProject and _defaultProject are in set of currently valid projects, 656 then merely return (_currentProject, ref for _currentProject) 657 Otherwise set _defaultProject to the first project name (sorted alphabetically) 658 in the set of currently valid projects. 659 if the _currentProject isn't valid at this point, reset it to the _defaultProject value 660 Then return a 2 tuple of (_defaultProject, ref for the _defaultProject) 661 """ 662 current_valid_projects = self.getAccessibleProjects(self._currentWorkspace) 663 proj_names = sorted([name for name, ref in current_valid_projects]) 664 proj_refs = self._project_ref[self._currentWorkspace] 665 if str(self._defaultProject) in proj_names and str(self._currentProject) in proj_names: 666 return (self._defaultProject, proj_refs[self._defaultProject]) 667 668 if str(self._defaultProject) not in proj_names: 669 self._defaultProject = proj_names[0] 670 if str(self._currentProject) not in proj_names: 671 self.setProject(self._defaultProject) 672 return (self._defaultProject, proj_refs[self._defaultProject]) 673 674 675 def currentProjectRef(self): 676 """ 677 Return the ref associated with the project in the currently selected workspace. 678 If there isn't a currently selected workspace, return an empty string. 679 """ 680 if not self._currentWorkspace: 681 return "" 682 if not self._currentProject: 683 return "" 684## 685## print(" currentProjectRef() ... ") 686## print(" _currentWorkspace: '%s'" % self._currentWorkspace) 687## print(" _currentProject : '%s'" % self._currentProject) 688## print(" _project_ref keys: %s" % repr(self._project_ref.keys())) 689## 690 # 691 # this next condition could be True in limited circumstances, like on initialization 692 # when info for the _currentProject hasn't yet been retrieved, 693 # which will be manifested by the _currentWorkspace not having an entry in _project_ref 694 # 695 if self._currentWorkspace not in self._project_ref: 696 return "" 697 698 if re.search(r'project/\d+$', self._currentProject): 699 return self._currentProject 700 701 proj_refs = self._project_ref[self._currentWorkspace] 702 if self._currentProject in proj_refs: 703 return proj_refs[self._currentProject] 704 else: 705 return "" 706 707 708 def _establishContext(self, kwargs): 709 workspace = None 710 project = None 711 if kwargs and 'workspace' in kwargs: 712 workspace = kwargs['workspace'] 713 if kwargs and 'project' in kwargs: 714 project = kwargs['project'] 715## 716## print("_establishContext calling _getWorkspacesAndProjects(workspace=%s, project=%s)" % (workspace, project)) 717## 718 self._getWorkspacesAndProjects(workspace=workspace, project=project) 719 if workspace: 720 self._inflated = 'minimal' 721 722 def identifyContext(self, **kwargs): 723 """ 724 Look for workspace, project, projectScopeUp, projectScopeDown entries in kwargs. 725 If present, check cache for values to provide for hrefs. 726 Return back a tuple of (RallyContext instance, augment list with hrefs) 727 """ 728## 729## print("... RallyContextHelper.identifyContext kwargs: %s" % repr(kwargs)) 730## sys.stdout.flush() 731## 732 augments = [] 733 734 if '_disableAugments' in kwargs: 735 return self.context, augments 736 737 if not self._inflated: 738 self._inflated = 'minimal' # to avoid recursion limits hell 739 self._establishContext(kwargs) 740 741 workspace = None 742 if 'workspace' in kwargs and kwargs['workspace']: 743 workspace = kwargs['workspace'] 744 eligible_workspace_names = [wksp.Name for wksp in self._subs_workspaces] 745 746 if workspace not in eligible_workspace_names: 747 problem = 'Workspace specified: "%s" not accessible with current credentials' 748 raise RallyRESTAPIError(problem % workspace.Name) 749 if workspace not in self._workspaces and self._inflated != 'wide': 750 ec_kwargs = {'workspace' : workspace} 751 self._establishContext(ec_kwargs) 752 self._inflated = 'narrow' 753 754 wks_ref = self._workspace_ref[workspace] 755 augments.append("workspace=%s" % wks_ref) 756 self.context.workspace = workspace 757 758 project = None 759 if 'project' in kwargs: 760 if not kwargs['project']: 761 self.context.project = None 762 return self.context, augments 763 764 project = kwargs['project'] 765 wks = workspace or self._currentWorkspace or self._defaultWorkspace 766 if project in self._projects[wks]: 767 prj_ref = self._project_ref[wks][project] 768 elif PROJECT_PATH_ELEMENT_SEPARATOR in project: # ' // ' 769 proj_path_leaf = self._findMultiElementPathToProject(project) 770 prj_ref = proj_path_leaf.ref 771 project = proj_path_leaf.Name 772 elif re.search(r'project/\d+$', project): 773 prj_ref = project 774 else: 775 problem = 'Project specified: "%s" (in workspace: "%s") not accessible with current credentials' % \ 776 (project, workspace) 777 raise RallyRESTAPIError(problem) 778 779 augments.append("project=%s" % prj_ref) 780 self.context.project = project 781 782 if 'projectScopeUp' in kwargs: 783 projectScopeUp = kwargs['projectScopeUp'] 784 if projectScopeUp in [1, True, 'true', 'True']: 785 augments.append("projectScopeUp=true") 786 elif projectScopeUp in [0, False, 'false', 'False']: 787 augments.append("projectScopeUp=false") 788 else: 789 augments.append("projectScopeUp=false") 790 else: 791 augments.append("projectScopeUp=false") 792 793 if 'projectScopeDown' in kwargs: 794 projectScopeDown = kwargs['projectScopeDown'] 795 if projectScopeDown in [1, True, 'true', 'True']: 796 augments.append("projectScopeDown=true") 797 elif projectScopeDown in [0, False, 'false', 'False']: 798 augments.append("projectScopeDown=false") 799 else: 800 augments.append("projectScopeDown=false") 801 else: 802 augments.append("projectScopeDown=false") 803 804 if not workspace and project: 805 self.context = self.operatingContext 806 807 # check to see if the _current_project is actually in the _current_workspace or is a known m-e-p Project ref 808## 809 #print() 810 #print("identifyContext: operatingContext: %s" % self.operatingContext) 811 #print("identifyContext: project keyword: %s" % project) 812 #print("identifyContext: _currentProject: %s" % self._currentProject) 813 #print("ContextHelper._project_path: %s" % self._project_path) 814## 815 if self._currentProject in self._projects[self._currentWorkspace] or self._currentProject in self._project_path.keys(): 816 return self.context, augments 817 818 problem = "the current Workspace |%s| does not contain a Project that matches the current setting of the Project: %s" % (self._currentWorkspace, self._currentProject) 819 raise RallyRESTAPIError(problem) 820 821 #if self._currentProject not in self._projects[self._currentWorkspace]: 822 # problem = "the current Workspace |%s| does not contain a Project that matches the current setting of the Project: %s" % (self._currentWorkspace, self._currentProject) 823 # raise RallyRESTAPIError(problem) 824## 825 #return self.context, augments 826 827 828 def _getWorkspacesAndProjects(self, **kwargs): 829 """ 830 Issue requests to obtain a complete inventory of the workspaces and projects 831 that are accessible in the subscription to the active user. 832 """ 833 target_workspace = self._currentWorkspace or self._defaultWorkspace 834 if kwargs: 835 if 'workspace' in kwargs and kwargs['workspace']: 836 target_workspace = kwargs['workspace'] 837 if target_workspace == '*': # wild card value to specify all workspaces 838 target_workspace = None 839## 840## print("in _getWorkspacesAndProjects(%s)" % repr(kwargs)) 841## print("_getWorkspacesAndProjects, target_workspace: %s" % target_workspace) 842## print("_getWorkspacesAndProjects, self._currentWorkspace: %s" % self._currentWorkspace) 843## print("_getWorkspacesAndProjects, self._defaultWorkspace: %s" % self._defaultWorkspace) 844## 845 for workspace in self._subs_workspaces: 846 # short-circuit issuing any WS calls if we don't need to 847 if target_workspace and workspace.Name != target_workspace: 848 continue 849 if self._workspace_inflated.get(workspace.Name, False) == True: 850 continue 851## 852## print(workspace.Name, workspace.oid) 853## 854 # fill out self._workspaces and self._workspace_ref 855 if workspace.Name not in self._workspaces: 856 self._workspaces.append(workspace.Name) 857 # we only need the 'workspace/<oid>' fragment to qualify as a valid ref 858 self._workspace_ref[workspace.Name] = '/'.join(workspace._ref.split('/')[-2:]) 859 self._projects[ workspace.Name] = [] 860 self._project_ref[ workspace.Name] = {} 861 resp = self.agent._getResourceByOID( self.context, 'workspace', workspace.oid, _disableAugments=True) 862 response = resp.json() 863 # If SLM gave back consistent responses, we could use RallyRESTResponse, but no joy... 864 # Carefully weasel into the response to get to the guts of what we need 865 # and note we specify only the necessary fetch fields or this query takes a *lot* longer... 866 base_proj_coll_url = response['Workspace']['Projects']['_ref'] 867 projects_collection_url = '%s?fetch="ObjectID,Name,State"&pagesize=200&start=1' % base_proj_coll_url 868 response = self.agent.getCollection(projects_collection_url, _disableAugments=True) 869#not-as-bad?# response = self.agent.get('Project', fetch="ObjectID,Name,State", workspace=workspace.Name) 870 871## 872## print(" Number of Projects: %d" % response.data[u'TotalResultCount']) 873## for item in response.data[u'Results']: 874## print(" %-36.36s" % (item[u'_refObjectName'], )) 875## 876 for project in response: 877 projName = project.Name 878 # we only need the project/123534 section to qualify as a valid ref 879 projRef = '/'.join(project.ref.split('/')[-2:]) 880 if projName not in self._projects[workspace.Name]: 881 self._projects[ workspace.Name].append(projName) 882 self._project_ref[workspace.Name][projName] = projRef 883 self._workspace_inflated[workspace.Name] = True 884 885 if target_workspace != self._defaultWorkspace: 886 if 'workspace' in kwargs and kwargs['workspace']: 887 self._inflated = 'narrow' 888 else: 889 self._inflated = 'wide' 890 891 892 def getSchemaItem(self, entity_name): 893 return getSchemaItem(self.getWorkspace(), entity_name) 894 895 896 def __repr__(self): 897 items = [] 898 items.append('%s = %s' % ('server', self.server)) 899 items.append('%s = %s' % ('defaultContext', self.defaultContext)) 900 items.append('%s = %s' % ('operatingContext', self.operatingContext)) 901 items.append('%s = %s' % ('_subs_name', self._subs_name)) 902 items.append('%s = %s' % ('_workspaces', repr(self._workspaces))) 903 items.append('%s = %s' % ('_projects', repr(self._projects))) 904 items.append('%s = %s' % ('_workspace_ref', repr(self._workspace_ref))) 905 items.append('%s = %s' % ('_project_ref', repr(self._project_ref))) 906 items.append('%s = %s' % ('_defaultWorkspace', self._defaultWorkspace)) 907 items.append('%s = %s' % ('_defaultProject', self._defaultProject)) 908 items.append('%s = %s' % ('_currentWorkspace', self._currentWorkspace)) 909 items.append('%s = %s' % ('_currentProject', self._currentProject)) 910 representation = "\n".join(items) 911 return representation 912 913