|
| 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