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