Skip to content

Commit b947283

Browse files
author
Glenn Snyder
committed
refactoring and fixing
1 parent 1fd6a14 commit b947283

File tree

1 file changed

+54
-263
lines changed

1 file changed

+54
-263
lines changed

examples/wait_for_scan_results.py

Lines changed: 54 additions & 263 deletions
Original file line numberDiff line numberDiff line change
@@ -10,281 +10,72 @@
1010
'''
1111

1212
import argparse
13+
import arrow
1314
from datetime import datetime
1415
import json
1516
import logging
1617
import sys
1718
import time
18-
import timestring
1919

2020
from blackduck.HubRestApi import HubInstance, object_id
2121

22-
parser = argparse.ArgumentParser("Wait for scan processing to complete for a given code (scan) location/name")
23-
parser.add_argument("scan_location_name", help="The scan location name")
24-
parser.add_argument('-m', '--max_checks', type=int, default=10, help="Set the maximum number of checks before quitting")
25-
parser.add_argument('-t', '--time_between_checks', default=5, help="Set the number of seconds to wait in-between checks")
26-
parser.add_argument('-s', '--snippet_scan', action='store_true', help="Select this option if you want to wait for a snippet scan to complete along with it's corresponding component scan.")
27-
args = parser.parse_args()
22+
class ScanMonitor(object):
23+
def __init__(self, hub, scan_location_name, max_checks=10, check_delay=5, snippet_scan=False):
24+
self.hub = hub
25+
self.scan_location_name = scan_location_name
26+
self.max_checks = max_checks
27+
self.check_delay = check_delay
28+
self.snippet_scan = snippet_scan
2829

29-
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
30-
logging.getLogger("requests").setLevel(logging.WARNING)
31-
logging.getLogger("urllib3").setLevel(logging.WARNING)
30+
def wait_for_scan_completion(self):
31+
now = arrow.now()
3232

33-
hub = HubInstance()
33+
scan_locations = self.hub.get_codelocations(parameters={'q':f'name:{args.scan_location_name}'}).get('items', [])
3434

35-
def get_project_and_version_name(project_version_url):
36-
# Get the project and version name and return them concatenated for use to lookup relevant jobs
37-
response = hub.execute_get(project_version_url)
38-
if response.status_code == 200:
39-
project_version_obj = response.json()
40-
else:
41-
raise Exception("Unable to retrieve project version at URL {}".format(
42-
project_version_url))
35+
scan_location = scan_locations[0]
4336

44-
version_name = project_version_obj['versionName']
45-
project_url = hub.get_link(project_version_obj, "project")
46-
response = hub.execute_get(project_url)
47-
if response.status_code == 200:
48-
project_obj = response.json()
49-
else:
50-
raise Exception("Could not find the project associated with the project version")
37+
remaining_checks = self.max_checks
38+
scans_url = self.hub.get_link(scan_location, "scans")
39+
latest_scan_url = self.hub.get_link(scan_location, "latest-scan")
5140

52-
project_name = project_obj['name']
53-
return "{} {}".format(project_name, version_name)
54-
55-
def get_jobs_from_scan(scan_location, scan_summary_ids, project_version_id, project_version_url, later_than):
56-
'''Get the most recent jobs pertaining to the processing of scans for the given scan location
57-
and the BOM processing for the given the project-version id
58-
'''
59-
60-
# TODO: Refactor
61-
62-
assert isinstance(later_than, str), "later_than should be a date/time string parseable by timestring"
63-
later_than = timestring.Date(later_than).date
64-
65-
# See https://jira.dc1.lan/browse/HUB-14263 - Rest API for Job status
66-
# The below is using a private endpoint that could change on any release and break this code
67-
jobs_url = hub.get_urlbase() + "/api/v1/jobs?limit={}".format(100)
68-
response = hub.execute_get(jobs_url)
69-
jobs = []
70-
if response.status_code == 200:
71-
jobs = response.json().get('items', [])
72-
else:
73-
logging.error("Failed to retrieve jobs, status code: {}".format(response.status_code))
74-
75-
jobs_with_job_descriptions = list(filter(lambda j: 'jobEntityDescription' in j, jobs))
76-
77-
#
78-
# Get jobs related to the project-version this scan is mapped to
79-
#
80-
if project_version_url:
81-
pv_name = get_project_and_version_name(project_version_url)
82-
83-
pv_jobs = list(filter(
84-
lambda j: pv_name in j.get('jobEntityDescription', []) and timestring.Date(j['createdAt']).date > later_than,
85-
jobs_with_job_descriptions))
86-
pv_job_types = set([pj['jobSpec']['jobType'] for pj in pv_jobs])
87-
else:
88-
pv_jobs = []
89-
pv_job_types = set()
90-
91-
#
92-
# Get most recent scanning jobs
93-
#
94-
95-
# SnippetScanAutoBomJob does not have the code (scan) location name associated with it
96-
# so we have to pass in the scan id's and find job that way
97-
# For the other scan jobs they have a description that includes the code location name
98-
#
99-
100-
# TODO: filter for later_than?
101-
102-
s_jobs = list(filter(
103-
lambda j: scan_location in j.get('jobEntityDescription', []) or j['jobSpec']['entityKey']['entityId'] in scan_summary_ids,
104-
jobs))
105-
s_job_types = set([sj['jobSpec']['jobType'] for sj in s_jobs])
106-
107-
# Get most recent job for each job type to trim off the historic jobs
108-
most_recent_pv_jobs = get_most_recent_jobs(pv_job_types, pv_jobs)
109-
most_recent_s_jobs = get_most_recent_jobs(s_job_types, s_jobs)
110-
111-
return most_recent_pv_jobs + most_recent_s_jobs
112-
113-
def get_most_recent_jobs(job_types, jobs):
114-
most_recent_l = list()
115-
for job_type in job_types:
116-
filtered_to_type = list(filter(lambda j: j['jobSpec']['jobType'] == job_type, jobs))
117-
most_recent = sorted(filtered_to_type, key = lambda j: j['startedAt'])[-1]
118-
most_recent_l.append(most_recent)
119-
return most_recent_l
120-
121-
def get_scan_summaries(scan_location, snippet_scan=False):
122-
'''Find and return scan summary information and project-version information for the given scan location (name)
123-
'''
124-
scan_locations = hub.get_codelocations(parameters={'q':'name:{}'.format(
125-
scan_location)})
126-
all_scan_summaries = []
127-
most_recent_scan_summaries = []
128-
all_project_version_ids = set()
129-
all_project_version_urls = set()
130-
for scan_location in scan_locations.get('items', []):
131-
mapped_project_version = scan_location.get('mappedProjectVersion')
132-
133-
if mapped_project_version:
134-
mapped_project_version_id = mapped_project_version.split('/')[-1]
135-
all_project_version_ids.add(mapped_project_version_id)
136-
all_project_version_urls.add(mapped_project_version)
137-
138-
scan_location_id = object_id(scan_location)
139-
140-
scan_summaries = hub.get_codelocation_scan_summaries(scan_location_id)
141-
scan_summaries = scan_summaries.get('items', [])
142-
scan_summaries = sorted(scan_summaries, key=lambda ss: ss['updatedAt'])
143-
144-
all_scan_summaries.extend(scan_summaries)
145-
if snippet_scan:
146-
# When using a snippet scan we need to look at the two most recent
147-
most_recent = scan_summaries[-2:]
148-
else:
149-
# Otherwise, we can look at the single most recent
150-
most_recent = scan_summaries[-1:]
151-
most_recent_scan_summaries.extend(most_recent)
152-
153-
all_scan_summary_ids = list(set(
154-
[object_id(ss) for ss in all_scan_summaries]
155-
))
156-
most_recent_scan_summary_ids = list(set(
157-
[object_id(ss) for ss in most_recent_scan_summaries]
158-
))
159-
160-
if all_project_version_ids:
161-
assert len(all_project_version_ids) == 1, "The must be one, and only one, project-version this scan location is mapped to"
162-
163-
project_version_id = list(all_project_version_ids)[0]
164-
else:
165-
project_version_id = None
166-
167-
if all_project_version_urls:
168-
assert len(all_project_version_urls) == 1, "The must be one, and only one, project-version this scan location is mapped to"
169-
project_version_url = list(all_project_version_urls)[0]
170-
else:
171-
project_version_url = None
172-
173-
# To find the right jobs we use the "oldest" createdAt dt from the
174-
# pertinent scan summaries
175-
later_than = min([ss['createdAt'] for ss in most_recent_scan_summaries])
176-
177-
return {
178-
'all_scan_summaries': all_scan_summaries,
179-
'all_scan_summary_ids': all_scan_summary_ids,
180-
'most_recent_scan_summaries': most_recent_scan_summaries,
181-
'most_recent_scan_summary_ids': most_recent_scan_summary_ids,
182-
'project_version_id': project_version_id,
183-
'project_version_url': project_version_url,
184-
'later_than': later_than
185-
}
186-
187-
def exit_status(scan_location, snippet_scan=False):
188-
'''Determine the exit status value. If all of the related jobs or scan summaries
189-
are 'complete' (successful) we return 0, otherwise 1
190-
'''
191-
summary_info = get_scan_summaries(scan_location, snippet_scan)
192-
scan_summaries = summary_info['most_recent_scan_summaries']
193-
scan_ids = summary_info['all_scan_summary_ids']
194-
pv_id = summary_info['project_version_id']
195-
pv_url = summary_info['project_version_url']
196-
later_than = summary_info['later_than']
197-
log_scan_summary_info(scan_summaries)
198-
199-
related_jobs = get_jobs_from_scan(
200-
scan_location, scan_ids, pv_id, pv_url, later_than)
201-
log_related_jobs_info(related_jobs)
202-
if all([ss['status'] == 'COMPLETE' for ss in scan_summaries]) and all([j['status'] == 'COMPLETED' for j in related_jobs]):
203-
return 0
204-
else:
205-
return 1
206-
207-
def log_related_jobs_info(related_jobs):
208-
jobs_status_view = [
209-
{
210-
'status': j['status'],
211-
'createdAt': j['createdAt'],
212-
'jobType': j['jobSpec']['jobType'],
213-
'jobEntityDescription': j.get('jobEntityDescription')
214-
} for j in related_jobs
215-
]
216-
logging.debug("Related job statuses: {}".format(jobs_status_view))
217-
218-
def log_scan_summary_info(scan_summaries):
219-
scan_status_view = [
220-
{
221-
'status': ss['status'],
222-
'createdAt': ss['createdAt'],
223-
'statusMessage': ss.get('statusMessage'),
224-
'scanId': object_id(ss),
225-
} for ss in scan_summaries
226-
]
227-
logging.debug("Scan statuses: {}".format(scan_status_view))
228-
229-
#
230-
# Main
231-
#
232-
not_completed = True
233-
something_has_run = False
234-
completed = False
235-
236-
max_checks = args.max_checks
237-
while max_checks > 0:
238-
summary_info = get_scan_summaries(args.scan_location_name, args.snippet_scan)
239-
scan_summaries = summary_info['most_recent_scan_summaries']
240-
scan_ids = summary_info['all_scan_summary_ids']
241-
pv_id = summary_info['project_version_id']
242-
pv_url = summary_info['project_version_url']
243-
later_than = summary_info['later_than']
244-
245-
related_jobs = get_jobs_from_scan(
246-
args.scan_location_name, scan_ids, pv_id, pv_url, later_than)
247-
248-
log_scan_summary_info(scan_summaries)
249-
log_related_jobs_info(related_jobs)
250-
251-
if not something_has_run:
252-
logging.info("Checking if anything has run yet. If we missed it, we will stop checking after {} more retries".format(max_checks))
253-
something_has_run = any(
254-
j['status'] == 'RUNNING' for j in related_jobs
255-
)
256-
logging.debug('Something has run: {}'.format(something_has_run))
257-
if something_has_run:
258-
continue
259-
else:
260-
logging.info("Waiting {} seconds before checking again for a running job".format(
261-
args.time_between_checks))
262-
time.sleep(args.time_between_checks)
263-
else:
264-
# TODO: Find out what a 'failed' scan summary produces and incorporate that below
26541
if args.snippet_scan:
266-
logging.debug("snippet scan")
267-
rj_completed = all([j['status'] == 'COMPLETED' or j['status'] == 'FAILED' for j in related_jobs])
268-
logging.debug("rj_completed: {}".format(rj_completed))
269-
ss_completed = all([ss['status'] == 'COMPLETE' for ss in scan_summaries])
270-
logging.debug("ss_completed: {}".format(ss_completed))
271-
completed = rj_completed and ss_completed
42+
logging.debug("Looking for snippet scan which means there will be 2 expected scans")
43+
number_expected_newer_scans = 2
27244
else:
273-
logging.debug("component scan")
274-
completed = all([ss['status'] == 'COMPLETE' for ss in scan_summaries])
275-
logging.debug("completed: {}".format(completed))
276-
if completed:
277-
break
278-
logging.info("Waiting {} seconds before checking status again".format(
279-
args.time_between_checks))
280-
time.sleep(args.time_between_checks)
281-
max_checks -= 1
282-
logging.debug('checks remaining {}'.format(max_checks))
283-
284-
exit_status_value = exit_status(args.scan_location_name, args.snippet_scan)
285-
logging.debug("Setting exit status to {}".format(exit_status_value))
286-
sys.exit(exit_status_value)
287-
288-
289-
290-
45+
logging.debug("Not looking for snippet scan which means there will be 1 expected scans")
46+
number_expected_newer_scans = 1
47+
48+
while remaining_checks > 0:
49+
scans = self.hub.execute_get(scans_url).json().get('items', [])
50+
51+
newer_scans = list(filter(lambda s: arrow.get(s['createdAt']) > now, scans))
52+
logging.debug(f"Found {len(newer_scans)} newer scans")
53+
54+
expected_scans_seen = len(newer_scans) == number_expected_newer_scans
55+
logging.debug(f"expected_scans_seen: {expected_scans_seen}")
56+
57+
if expected_scans_seen and all([s['status'] == 'COMPLETE' for s in newer_scans]):
58+
logging.info("Scans have finished processing")
59+
break
60+
else:
61+
remaining_checks -= 1
62+
logging.debug(f"Sleeping for {args.time_between_checks} seconds before checking again. {remaining_checks} remaining")
63+
time.sleep(args.time_between_checks)
64+
65+
66+
if __name__ == "__main__":
67+
parser = argparse.ArgumentParser("Wait for scan processing to complete for a given code (scan) location/name")
68+
parser.add_argument("scan_location_name", help="The scan location name")
69+
parser.add_argument('-m', '--max_checks', type=int, default=10, help="Set the maximum number of checks before quitting")
70+
parser.add_argument('-t', '--time_between_checks', type=int, default=5, help="Set the number of seconds to wait in-between checks")
71+
parser.add_argument('-s', '--snippet_scan', action='store_true', help="Select this option if you want to wait for a snippet scan to complete along with it's corresponding component scan.")
72+
args = parser.parse_args()
73+
74+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
75+
logging.getLogger("requests").setLevel(logging.WARNING)
76+
logging.getLogger("urllib3").setLevel(logging.WARNING)
77+
78+
hub = HubInstance()
79+
80+
scan_monitor = ScanMonitor(hub, args.scan_location_name, args.max_checks, args.time_between_checks, args.snippet_scan)
81+
scan_monitor.wait_for_scan_completion()

0 commit comments

Comments
 (0)