Skip to content

Commit da92fc9

Browse files
committed
batch deletion of project versions
1 parent 9ae9202 commit da92fc9

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
'''
2+
Created on April 4, 2023
3+
@author: kumykov
4+
5+
Copyright (C) 2023 Synopsys, Inc.
6+
http://www.blackducksoftware.com/
7+
8+
Licensed to the Apache Software Foundation (ASF) under one
9+
or more contributor license agreements. See the NOTICE file
10+
distributed with this work for additional information
11+
regarding copyright ownership. The ASF licenses this file
12+
to you under the Apache License, Version 2.0 (the
13+
"License"); you may not use this file except in compliance
14+
with the License. You may obtain a copy of the License at
15+
16+
http://www.apache.org/licenses/LICENSE-2.0
17+
18+
Unless required by applicable law or agreed to in writing,
19+
software distributed under the License is distributed on an
20+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21+
KIND, either express or implied. See the License for the
22+
specific language governing permissions and limitations
23+
under the License.
24+
25+
This script will perform bulk deletion of Project versions based on
26+
the content of an EXCEL file. Each row of a file is expected to contain
27+
a field for Project Name Project Version.
28+
Script will iterate through the rows of a spreadsheet and issue an API
29+
call per row.
30+
If the project version is the last in the project, entire project
31+
will be deleted.
32+
33+
Requirements
34+
35+
- python3 version 3.8 or newer recommended
36+
- the following packages are used by the script and should be installed
37+
prior to use:
38+
argparse
39+
blackduck
40+
csv
41+
logging
42+
re
43+
openpyxl
44+
requests
45+
sys
46+
- Blackduck instance
47+
- API token with sufficient privileges to perform project version phase
48+
change.
49+
50+
Install python packages with the following command:
51+
52+
pip3 install argparse blackduck csv logging re openpyxl requests sys
53+
54+
Using
55+
56+
place the token into a file (token in this example) then execute:
57+
58+
python3 batch_delete_project_version.py -u https://blackduck-host -t token -nv -i excel-file-with-data
59+
60+
Projects and project versions that are listed in the file but are not
61+
present on the blackduck instance will be flagged in the summary.
62+
63+
usage: python3.10 examples/client/batch_delete_project_version.py [-h] -u BASE_URL -t TOKEN_FILE -i INPUT_FILE [-nv] [--dry-run]
64+
65+
options:
66+
-h, --help show this help message and exit
67+
-u BASE_URL, --base-url BASE_URL
68+
Hub server URL e.g. https://your.blackduck.url
69+
-t TOKEN_FILE, --token-file TOKEN_FILE
70+
File containing access token
71+
-i INPUT_FILE, --input-file INPUT_FILE
72+
Project Name
73+
-nv, --no-verify Disable TLS certificate verification
74+
--dry-run Do not delete, dry run
75+
76+
77+
'''
78+
79+
import csv
80+
import sys
81+
import argparse
82+
import logging
83+
import re
84+
import openpyxl
85+
86+
from blackduck import Client
87+
from blackduck.constants import VERSION_PHASES
88+
89+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
90+
logging.getLogger("requests").setLevel(logging.WARNING)
91+
logging.getLogger("urllib3").setLevel(logging.WARNING)
92+
logging.getLogger("blackduck").setLevel(logging.DEBUG)
93+
94+
# Values for the variables below should match corresponding column headers
95+
project_name_column = 'Project Name'
96+
project_version_column = 'Version Name'
97+
98+
summary_report = '''
99+
100+
Summary
101+
102+
'''
103+
104+
def append_to_summary(message):
105+
global summary_report
106+
summary_report += message + '\n'
107+
108+
def process_csv_file(args):
109+
file = open(args.input_file)
110+
type(file)
111+
csvreader = csv.reader(file)
112+
project_name_idx = None
113+
project_version_idx = None
114+
for row in csvreader:
115+
row_number = csvreader.line_num
116+
if not (project_name_idx and project_version_idx):
117+
project_name_idx = row.index(project_name_column)
118+
project_version_idx = row.index(project_version_column)
119+
elif project_name_idx and project_version_idx:
120+
project_name = row[project_name_idx].strip() if project_name_idx < len(row) else ''
121+
version_name = row[project_version_idx].strip() if project_version_idx < len(row) else ''
122+
if project_name and version_name:
123+
logging.info(f"Processing row {row_number:4}: {row[project_name_idx]} : {row[project_version_idx]}")
124+
process_project_version(project_name, version_name, args)
125+
else:
126+
message = f"Processing row {row_number:4}. Project '{project_name}' version '{version_name}' is not present, skipping"
127+
logging.info(message)
128+
append_to_summary(message)
129+
continue
130+
else:
131+
logging.info("Could not parse input file")
132+
sys.exit(1)
133+
134+
135+
def process_excel_file(args):
136+
wb = openpyxl.load_workbook(args.input_file)
137+
ws = wb.active
138+
project_name_idx = None
139+
project_version_idx = None
140+
row_number = 0
141+
for row in ws.values:
142+
row_number += 1
143+
if not (project_name_idx and project_version_idx):
144+
project_name_idx = row.index(project_name_column)
145+
project_version_idx = row.index(project_version_column)
146+
elif project_name_idx and project_version_idx:
147+
project_name = row[project_name_idx] if project_name_idx < len(row) else ''
148+
version_name = row[project_version_idx] if project_version_idx < len(row) else ''
149+
if project_name and version_name:
150+
logging.info(f"Processing row {row_number:4}: {row[project_name_idx]} : {row[project_version_idx]}")
151+
process_project_version(project_name.strip(), version_name.strip(), args)
152+
else:
153+
message = f"Processing row {row_number:}. Project '{project_name}' version '{version_name}' is not present, skipping"
154+
logging.info(message)
155+
append_to_summary(message)
156+
continue
157+
else:
158+
logging.info("Could not parse input file")
159+
sys.exit(1)
160+
161+
def process_project_version(project_name, version_name, args):
162+
params = {
163+
'q': [f"name:{project_name}"]
164+
}
165+
try:
166+
projects = [p for p in bd.get_resource('projects', params=params) if p['name'] == project_name]
167+
assert len(projects) == 1, f"There should be one, and only one project named {project_name}. We found {len(projects)}"
168+
project = projects[0]
169+
except AssertionError:
170+
message = f"Project named '{project_name}' not found. Skipping"
171+
logging.warning(message)
172+
append_to_summary(message)
173+
return
174+
175+
params = {
176+
'q': [f"versionName:{version_name}"]
177+
}
178+
179+
num_versions = bd.get_resource('versions', project, items=False)['totalCount']
180+
print(num_versions)
181+
182+
try:
183+
versions = [v for v in bd.get_resource('versions', project, params=params) if v['versionName'] == version_name]
184+
assert len(versions) == 1, f"There should be one, and only one version named {version_name}. We found {len(versions)}"
185+
version = versions[0]
186+
except AssertionError:
187+
message = f"Version name '{version_name}' for project {project_name} was not found, skipping"
188+
logging.warning(message)
189+
append_to_summary(message)
190+
return
191+
logging.debug(f"Found {project['name']}:{version['versionName']}")
192+
if args.dry_run:
193+
logging.debug(f"Would be deleting {project['name']}:{version['versionName']}")
194+
else:
195+
logging.debug(f"Deleting {project['name']}:{version['versionName']}")
196+
try:
197+
if num_versions == 1:
198+
r = bd.session.delete(project['_meta']['href'])
199+
else:
200+
r = bd.session.delete(version['_meta']['href'])
201+
r.raise_for_status()
202+
logging.debug(f"Deleted {project['name']}:{version['versionName']}")
203+
except requests.HTTPError as err:
204+
bd.http_error_handler(err)
205+
206+
207+
def parse_command_args():
208+
209+
parser = argparse.ArgumentParser("batch_delete_project_version.py")
210+
parser.add_argument("-u", "--base-url", required=True, help="Hub server URL e.g. https://your.blackduck.url")
211+
parser.add_argument("-t", "--token-file", required=True, help="File containing access token")
212+
parser.add_argument("-i", "--input-file", required=True, help="Project Name")
213+
parser.add_argument("-nv", "--no-verify", action='store_false', help="Disable TLS certificate verification")
214+
parser.add_argument("--dry-run", action='store_true', help="Do not delete, dry run")
215+
return parser.parse_args()
216+
217+
def main():
218+
args = parse_command_args()
219+
with open(args.token_file, 'r') as tf:
220+
access_token = tf.readline().strip()
221+
global bd
222+
bd = Client(base_url=args.base_url, token=access_token, verify=args.no_verify, timeout=60.0, retries=4)
223+
224+
if re.match(".+xlsx?$", args.input_file):
225+
logging.info(f"Processing EXCEL file {args.input_file}")
226+
process_excel_file(args)
227+
else:
228+
logging.info(f"Processing CSV file {args.input_file}")
229+
process_csv_file(args)
230+
231+
print (summary_report)
232+
233+
if __name__ == "__main__":
234+
sys.exit(main())

0 commit comments

Comments
 (0)