|
10 | 10 | '''
|
11 | 11 |
|
12 | 12 | import argparse
|
| 13 | +import arrow |
13 | 14 | from datetime import datetime
|
14 | 15 | import json
|
15 | 16 | import logging
|
16 | 17 | import sys
|
17 | 18 | import time
|
18 |
| -import timestring |
19 | 19 |
|
20 | 20 | from blackduck.HubRestApi import HubInstance, object_id
|
21 | 21 |
|
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 |
28 | 29 |
|
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() |
32 | 32 |
|
33 |
| -hub = HubInstance() |
| 33 | + scan_locations = self.hub.get_codelocations(parameters={'q':f'name:{args.scan_location_name}'}).get('items', []) |
34 | 34 |
|
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] |
43 | 36 |
|
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") |
51 | 40 |
|
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 |
265 | 41 | 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 |
272 | 44 | 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