Skip to content

Commit 185661b

Browse files
committed
minor version update - ran oslcquery tests against 7.0.2SR1
1 parent 62fc4d1 commit 185661b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4813
-6061
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.11.3
2+
current_version = 0.13.0
33
commit = True
44
tag = True
55

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
SPDX-License-Identifier: MIT
1010

11-
version="0.11.3"
11+
version="0.13.0"
1212

1313

1414
Introduction

elmclient/__meta__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
app = 'elmoslcquery'
1111
description = 'Commandline OSLC query for ELM'
12-
version = '0.11.3'
12+
version = '0.13.0'
1313
license = 'MIT'
1414
author_name = 'Ian Barnard'
1515
author_mail = 'ian.barnard@uk.ibm.com'

elmclient/examples/oslcquery.py

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import time
1919
import urllib3
2020
import webbrowser
21+
import concurrent.futures
2122

2223
import cryptography
2324
import cryptography.fernet
@@ -79,9 +80,9 @@ def do_oslc_query(inputargs=None):
7980
parser.add_argument('-A', '--appstrings', default=None, help=f'A comma-seperated list of apps, the query goes to the first entry, default "rm". Each entry must be a domain or domain:contextroot e.g. rm or rm:rm1 - Default can be set using environemnt variable QUERY_APPSTRINGS')
8081
parser.add_argument('-C', '--component', help='The local component (optional, you *have* to specify the local configuration using -F)')
8182
parser.add_argument('-D', '--delaybetweenpages', type=float,default=0.0, help="Delay in seconds between each page of results - use this to reduce overall server load particularly for large result sets or when retrieving many properties")
82-
parser.add_argument('-E', '--globalproject', default=None, help="The global configuration project - needed if the globalconfiguration isn't unique")
83-
parser.add_argument('-F', '--configuration', default=None, help='The local configuration')
84-
parser.add_argument('-G', '--globalconfiguration', default=None, help='The global configuration (you must not specify local config as well!) - you can specify the id, the full URI, or the config name (not implemented yet)')
83+
parser.add_argument('-E', '--globalproject', default=None, help="The global configuration project - optional if the globalconfiguration is unique in the gcm app")
84+
parser.add_argument('-F', '--configuration', default=None, help='The local configuration name')
85+
parser.add_argument('-G', '--globalconfiguration', default=None, help='The global configuration (you must not specify local config as well!) - you can specify the id, the full URI, or the config name')
8586
parser.add_argument('-H', '--saveconfigs', default=None, help='Name of CSV file to save details of the local project components and configurations')
8687
parser.add_argument('-I', '--totalize', action="store_true", help="For any column with multiple results, put in the total instead of the results")
8788
parser.add_argument("-J", "--jazzurl", default=JAZZURL, help=f"jazz server url (without the /jts!) default {JAZZURL} - Default can be set using environemnt variable QUERY_JAZZURL - defaults to https://jazz.ibm.com:9443 which DOESN'T EXIST")
@@ -111,7 +112,8 @@ def do_oslc_query(inputargs=None):
111112
parser.add_argument('--saveprocessedresults', default=None, help="Save the processed results as JSON to this path/file" )
112113
parser.add_argument('--percontribution', action="store_true", help="When querying a GC, query once for each app-domain contribution in the GC tree, with added component and configuration columns in the result")
113114
parser.add_argument('--cacheable', action="store_true", help="Query results can be cached - use when you know the data isn't changing and you need faster re-run")
114-
parser.add_argument('--crossproject', action="store_true", help="For --percontriubtion GC queries follow gc contributions to other projects and query those too (requires access permission of course)")
115+
parser.add_argument('--crossproject', action="store_true", help="For --percontribution GC queries follow gc contributions to other projects and query those too (requires access permission of course)")
116+
parser.add_argument('--threading', action="store_true", help="For --percontriubtion GC queries, use threading to parallelize queries with processing results UNTESTED")
115117

116118
# saved credentials
117119
parser.add_argument('-0', '--savecreds', default=None, help="Save obfuscated credentials file for use with readcreds, then exit - this stores jazzurl, appstring, username and password")
@@ -474,32 +476,66 @@ def do_oslc_query(inputargs=None):
474476

475477
if args.percontribution:
476478
results = {}
477-
# get all the contributions in this domain, and the component they're in - these will be added to the results
478-
contribs = p.get_our_contributions(gcconfiguri)
479-
for i,(contriburi,compuri) in enumerate(contribs):
480-
print( f"{i+1}/{len(contribs)} {contriburi=} {compuri=}" )
481-
# find the component in teh config
482-
queryon = p.find_local_component(compuri)
483-
if queryon is None:
484-
print( f"Component not found from {compuri}" )
485-
if args.crossproject:
486-
# this component isn't in our current project
487-
# try to create a component for it and add to current project
488-
queryon = p.add_external_component(compuri)
489-
if queryon is None:
490-
burp
491-
continue
479+
futureresults = []
480+
workers = 4 if args.threading else 1
481+
def thread_fn(i,queryon,configuri):
482+
print( f"Thread start {i}" )
483+
queryon.set_local_config(configuri)
484+
try:
485+
thisresults = queryon.do_complex_query( args.resourcetype, querystring=args.query, searchterms=args.searchterms, select=args.select, isnulls=args.null, isnotnulls=args.value
486+
,orderby=args.orderby
487+
,show_progress=args.noprogressbar
488+
,verbose=args.verbose
489+
,maxresults=args.maxresults
490+
,delaybetweenpages=args.delaybetweenpages
491+
,pagesize=args.pagesize
492+
,resolvenames = args.resolvenames
493+
,totalize=args.totalize
494+
,saverawresults=args.saverawresults
495+
,addcolumns={'$contriburi':contriburi,'$compuri':compuri}
496+
,cacheable=args.cacheable
497+
)
498+
except KeyboardInterrupt:
499+
raise Exception( "Control-c" )
500+
print( f"Thread finish {i=}" )
501+
return thisresults
502+
with concurrent.futures.ThreadPoolExecutor(max_workers = workers) as executor:
503+
# get all the contributions in this domain, and the component they're in - these will be added to the results
504+
contribs = p.get_our_contributions(gcconfiguri)
505+
for i,(contriburi,compuri) in enumerate(sorted(contribs,key=lambda v:v[0])):
506+
print( f"{i+1}/{len(contribs)} {contriburi=} {compuri=}" )
507+
# find the component in teh config
508+
queryon = p.find_local_component(compuri)
509+
if queryon is None:
510+
print( f"Component not found from {compuri}" )
511+
if args.crossproject:
512+
# this component isn't in our current project
513+
# try to create a component for it and add to current project
514+
queryon = p.add_external_component(compuri)
515+
if queryon is None:
516+
raise Exception( f"Can't add external component for {compuri}" )
517+
else:
518+
print( f"Added external component {queryon=} for {compuri}" )
492519
else:
493-
print( f"Added external component {queryon=}" )
494-
# check the comonent is accessible (may have been achived!)
495-
if not app.is_accessible( compuri ):
496-
print( f"Archived component {compuri} !")
497-
continue
498-
# check if the config is accessible (may have been archived!)
499-
if not app.is_accessible( contriburi ):
500-
print( f"Archived configuration {contriburi} !")
501-
continue
502-
520+
raise Exception( "Component {compuri} not found in current project - maybe you need to use --crossproject?" )
521+
# check the comonent is accessible (may have been achived!)
522+
if not app.is_accessible( compuri ):
523+
print( f"**** Archived component {compuri} !")
524+
continue
525+
# check if the config is accessible (may have been archived!)
526+
if not app.is_accessible( contriburi ):
527+
print( f"**** Archived configuration {contriburi} !")
528+
continue
529+
if not args.threading:
530+
# work synchronously
531+
results.update(thread_fn(i,queryon,contriburi))
532+
else:
533+
# get the results later
534+
futureresults.append(executor.submit(thread_fn,i,queryon,contriburi))
535+
# now if working asynchronously retrieve the results
536+
for res in futureresults:
537+
results.update(res.result())
538+
if False:
503539
# set the config ready to do the query
504540
queryon.set_local_config(contriburi)
505541
# now do a query for each contribution

elmclient/httpops.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ def to_binary(text, encoding=None, errors='strict'):
9191
result = codecs.encode(text, encoding=encoding, errors=errors)
9292
return result
9393

94+
#################################################################################################
95+
96+
def getcookievalue( cookies, cookiename, defaultvalue=None):
97+
print( f"gcv {cookies=} {cookiename=} {defaultvalue=}" )
98+
for c in cookies:
99+
if c.name == cookiename:
100+
print( f"Found {cookiename} {c.value}" )
101+
return c.value
102+
print( f"Not found {cookiename}" )
103+
return defaultvalue
104+
94105
##############################################################################################
95106

96107
class _FormParser(html.parser.HTMLParser):
@@ -203,6 +214,7 @@ def execute_get_binary( self, reluri, *, params=None, headers=None, **kwargs):
203214
return response
204215

205216
def execute_post_content( self, uri, *, params=None, data=None, headers={}, put=False, **kwargs):
217+
logger.debug("+++++++++++++++++++")
206218
data = data if data is not None else ""
207219
reqheaders = {}
208220
if headers is not None:
@@ -211,6 +223,7 @@ def execute_post_content( self, uri, *, params=None, data=None, headers={}, put=
211223
if put:
212224
request.method = "PUT"
213225
response = request.execute( **kwargs )
226+
logger.debug("-----------------")
214227
return response
215228

216229
def execute_get(self, reluri, *, params=None, headers=None, **kwargs):
@@ -308,7 +321,7 @@ def _execute_request( self, *, no_error_log=False, close=False, cacheable=True,
308321
for wait_dur in [2, 5, 10, 0]:
309322
try:
310323
if not cacheable:
311-
# add a header so the response isnm't cached
324+
# add a header so the response isn't cached
312325
self._req.headers['Cache-Control'] = "no-store, max-age=0"
313326
result = self._execute_one_request_with_login( no_error_log=no_error_log, close=close, **kwargs)
314327
return result
@@ -319,6 +332,35 @@ def _execute_request( self, *, no_error_log=False, close=False, cacheable=True,
319332
logger.warning( f'RETRY: Retry after {wait_dur} seconds... URL: {self._req.url}' )
320333
time.sleep(wait_dur)
321334
raise Exception('programming error this point should never be reached')
335+
336+
def _execute_request_newbutunused( self, *, no_error_log=False, close=False, cacheable=True, **kwargs ):
337+
logger.debug( f"execute_preventing_csrf {cacheable=}",)
338+
try:
339+
if not cacheable:
340+
# add a header so the response isn't cached
341+
self._req.headers['Cache-Control'] = "no-store, max-age=0"
342+
result = self._execute_one_request_with_login(no_error_log=no_error_log, close=close, **kwargs)
343+
logger.debug( f"execute_request result {result}" )
344+
return result
345+
except requests.HTTPError as e:
346+
logger.debug( f"execute_request Exception {e}" )
347+
if (e.response.status_code==http.client.FORBIDDEN or e.response.status_code==http.client.CONFLICT) and e.response.text.find('X-Jazz-CSRF-Prevent'):
348+
# Add special header and try again
349+
print( f"{e.response=}" )
350+
print( f"{e.request._cookies=}" )
351+
jsessionid = getcookievalue( e.request._cookies, 'JSESSIONID',None)
352+
if not jsessionid:
353+
logger.debug("Retrying request with CSRF header, but coudln't get JSESSIONID from the cookie for url [%s]." % request.url)
354+
raise
355+
356+
# e.close()
357+
logger.debug(" Retrying request with CSRF header...")
358+
self._req.headers['X-Jazz-CSRF-Prevent'] = jsessionid
359+
return self._execute_one_request_with_login( close=close, **kwargs )
360+
else:
361+
raise
362+
363+
322364

323365
# log a request/response, which may be the result of one or more redirections, so first log each of their request/response
324366
def log_redirection_history( self, response, intent, action=None, donotlogbody=False ):
@@ -443,6 +485,29 @@ def _is_retryable_error( self, e ):
443485
]:
444486
return True
445487
return False
488+
489+
def get_auth_path(self, request_url, response):
490+
request_url_parsed = urllib.parse.urlparse(request_url)
491+
form_auth_path = [c.path for c in response.cookies if c.name == 'JazzFormAuth']
492+
auth_app_context = form_auth_path[0] if len(form_auth_path) == 1 else request_url_parsed.path.split('/')[1]
493+
return auth_app_context
494+
495+
496+
def tidy_cookies(self):
497+
'''
498+
LQE 7.0.2SR1 and 7.0.3 has the unpleasant habit of including double-quotes in the auth cookie path so it looks like "/lqe" (which includes the quotation marks in the path) rather than /lqe, and then the path is never matched so authentication is lost
499+
This code cleans up the path on all cookies on the session4
500+
# return True if any cookie changed
501+
'''
502+
result = False
503+
for cookie in self._session.cookies:
504+
if cookie.path.startswith('%22'):
505+
oldvalue = cookie.path
506+
cookie.path = cookie.path.replace("%22","")
507+
logger.debug( f"REVISED cookie {cookie.name} path {oldvalue} to {cookie.path=}" )
508+
result = True
509+
return result
510+
446511

447512
# execute a request once, except:
448513
# 1. if the response indicates login is required then login and try the request again
@@ -489,6 +554,7 @@ def _execute_one_request_with_login( self, *, no_error_log=False, close=False, d
489554
try:
490555
prepped = self._session.prepare_request( request )
491556
response = self._session.send( prepped )
557+
492558
self.log_redirection_history( response, intent=intent, action=action )
493559

494560
response.raise_for_status()
@@ -512,12 +578,14 @@ def _execute_one_request_with_login( self, *, no_error_log=False, close=False, d
512578
self._session.is_authenticated = False
513579
auth_url = e.response.headers['X-jazz-web-oauth-url']
514580
login_response = self._login(auth_url)
581+
515582
if login_response:
516583
logger.trace("WIRE: NOT retrying")
517584
response = login_response
518585
else:
519586
logger.trace("WIRE: retrying")
520587
logger.trace( f"Auth completed (in theory) result - 1" )
588+
521589
retry_after_login_needed = True
522590
elif e.response.status_code == 401 and 'X-JSA-AUTHORIZATION-REDIRECT' in e.response.headers:
523591
logger.trace("WIRE: JAS Login required!")
@@ -605,12 +673,14 @@ def _login(self, auth_url):
605673
return None # no more auth required
606674
login_url = auth_url_response.url # Take the redirected URL and login action URL
607675
self._authorize(login_url)
676+
608677
logger.trace("authorized")
609678
else:
610679
logger.error('''Something has changed since this script was written. I can no longer determine where to authorize myself.''')
611680
raise Exception("Login not possible(1)!")
612681

613682
try:
683+
614684
# Now we should have the proper oauth cookies, so try again
615685
response = self._session.get(auth_url)
616686
except requests.exceptions.RequestException as e:
@@ -700,4 +770,3 @@ def _jazz_form_authorize(self, request_url, prev_request, prev_response):
700770
logger.info( f"Failed to jazz_authorize with auth URL {auth_url} with exception {e}" ) # was logger.error despite subsequent authentication success
701771
raise Exception("Jazz FORM authorize not possible!")
702772
return response
703-

0 commit comments

Comments
 (0)