|
| 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 update of Project version PHASE |
| 26 | +based on the content of an EXCEL file |
| 27 | +
|
| 28 | +Each row of a file is expected to containe a field for Project Name |
| 29 | +Project Version and desired phase |
| 30 | +
|
| 31 | +Sript will iterate through the rows of a spreadsheet and issue |
| 32 | +an API call per row. |
| 33 | +
|
| 34 | +Requirements |
| 35 | +
|
| 36 | +python3 vresion 3.8 or newer recommended |
| 37 | +
|
| 38 | +the following packages are used by the script and should be installed prior to use: |
| 39 | +
|
| 40 | +argparse |
| 41 | +blackduck |
| 42 | +csv |
| 43 | +logging |
| 44 | +re |
| 45 | +openpyxl |
| 46 | +sys |
| 47 | +
|
| 48 | +install them with the following command: |
| 49 | +
|
| 50 | +pip3 install argparse blackduck csv logging re openpyxl sys |
| 51 | +
|
| 52 | +Blackduck instance |
| 53 | +API token with sufficient privileges to perform project version phase change |
| 54 | +
|
| 55 | +Using |
| 56 | +
|
| 57 | +place the token into a file (token in this example) |
| 58 | +
|
| 59 | +execute |
| 60 | +
|
| 61 | +python3 batch_update_project_phase.py -u https://blackduck-host -t token -nv -i excel-file-with-data |
| 62 | +
|
| 63 | +Projects and project versions that are listed in the file but are not present on the blackduck instance will be skipped. |
| 64 | +If a project version is already at a requested phase, no update will be executed |
| 65 | +
|
| 66 | +''' |
| 67 | + |
| 68 | +import csv |
| 69 | +import sys |
| 70 | +import argparse |
| 71 | +import logging |
| 72 | +import re |
| 73 | +import openpyxl |
| 74 | + |
| 75 | +from blackduck import Client |
| 76 | +from blackduck.constants import VERSION_PHASES |
| 77 | + |
| 78 | +logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG) |
| 79 | +logging.getLogger("requests").setLevel(logging.WARNING) |
| 80 | +logging.getLogger("urllib3").setLevel(logging.WARNING) |
| 81 | +logging.getLogger("blackduck").setLevel(logging.DEBUG) |
| 82 | + |
| 83 | +# Values for the variables below should match corresponding column headers |
| 84 | +project_name_column = 'Project Name' |
| 85 | +project_version_column = 'Version Name' |
| 86 | +project_phase_column = 'Version phase' |
| 87 | + |
| 88 | +def process_csv_file(filename): |
| 89 | + file = open(filename) |
| 90 | + type(file) |
| 91 | + csvreader = csv.reader(file) |
| 92 | + project_name_idx = None |
| 93 | + project_version_idx = None |
| 94 | + project_phase_idx = None |
| 95 | + for row in csvreader: |
| 96 | + if not (project_name_idx and project_version_idx and project_phase_idx): |
| 97 | + project_name_idx = row.index(project_name_column) |
| 98 | + project_version_idx = row.index(project_version_column) |
| 99 | + project_phase_idx = row.index(project_phase_column) |
| 100 | + elif project_name_idx and project_version_idx and project_phase_idx: |
| 101 | + logging.info(f"Processing {row[project_name_idx]} : {row[project_version_idx]} with {row[project_phase_idx]}") |
| 102 | + process_project_version(row[project_name_idx], row[project_version_idx], row[project_phase_idx]) |
| 103 | + else: |
| 104 | + logging.info("Could not parse input file") |
| 105 | + sys.exit(1) |
| 106 | + |
| 107 | + |
| 108 | +def process_excel_file(filename): |
| 109 | + wb = openpyxl.load_workbook(filename) |
| 110 | + ws = wb.active |
| 111 | + project_name_idx = None |
| 112 | + project_version_idx = None |
| 113 | + project_phase_idx = None |
| 114 | + for row in ws.values: |
| 115 | + if not (project_name_idx and project_version_idx and project_phase_idx): |
| 116 | + project_name_idx = row.index(project_name_column) |
| 117 | + project_version_idx = row.index(project_version_column) |
| 118 | + project_phase_idx = row.index(project_phase_column) |
| 119 | + elif project_name_idx and project_version_idx and project_phase_idx: |
| 120 | + logging.info(f"Processing {row[project_name_idx]} : {row[project_version_idx]} with {row[project_phase_idx]}") |
| 121 | + process_project_version(row[project_name_idx], row[project_version_idx], row[project_phase_idx]) |
| 122 | + else: |
| 123 | + logging.info("Could not parse input file") |
| 124 | + sys.exit(1) |
| 125 | + |
| 126 | +def process_project_version(project_name, version_name, phase): |
| 127 | + params = { |
| 128 | + 'q': [f"name:{project_name}"] |
| 129 | + } |
| 130 | + try: |
| 131 | + projects = [p for p in bd.get_resource('projects', params=params) if p['name'] == project_name] |
| 132 | + assert len(projects) == 1, f"There should be one, and only one project named {project_name}. We found {len(projects)}" |
| 133 | + project = projects[0] |
| 134 | + except AssertionError: |
| 135 | + logging.warning(f"Project named {project_name} not found. Skipping") |
| 136 | + return |
| 137 | + |
| 138 | + params = { |
| 139 | + 'q': [f"versionName:{version_name}"] |
| 140 | + } |
| 141 | + |
| 142 | + try: |
| 143 | + versions = [v for v in bd.get_resource('versions', project, params=params) if v['versionName'] == version_name] |
| 144 | + assert len(versions) == 1, f"There should be one, and only one version named {version_name}. We found {len(versions)}" |
| 145 | + version = versions[0] |
| 146 | + except AssertionError: |
| 147 | + logging.warning(f"Version name {version_name} for project {project_name} was not found, skipping") |
| 148 | + return |
| 149 | + logging.debug(f"Found {project['name']}:{version['versionName']}") |
| 150 | + |
| 151 | + try: |
| 152 | + assert phase in VERSION_PHASES, f"Invalid version phase {phase}for {project_name} {version_name}" |
| 153 | + except AssertionError: |
| 154 | + logging.warning(f"Invalid version phase {phase}for {project_name} {version_name}. Skipping") |
| 155 | + |
| 156 | + if phase == version['phase' ]: |
| 157 | + logging.info(f"Project {project_name} version {version_name} is already at {phase}. No update") |
| 158 | + return |
| 159 | + |
| 160 | + logging.debug(f"Updating {project['name']}:{version['versionName']} settings to {phase}") |
| 161 | + version['phase'] = phase |
| 162 | + try: |
| 163 | + r = bd.session.put(version['_meta']['href'], json=version) |
| 164 | + r.raise_for_status() |
| 165 | + logging.debug(f"updated version settings to the new phase ({phase})") |
| 166 | + except requests.HTTPError as err: |
| 167 | + bd.http_error_handler(err) |
| 168 | + |
| 169 | + |
| 170 | +def parse_command_args(): |
| 171 | + |
| 172 | + parser = argparse.ArgumentParser("Print copyrights for BOM using upstream origin or prior version if not available.") |
| 173 | + parser.add_argument("-u", "--base-url", required=True, help="Hub server URL e.g. https://your.blackduck.url") |
| 174 | + parser.add_argument("-t", "--token-file", required=True, help="File containing access token") |
| 175 | + parser.add_argument("-i", "--input-file", required=True, help="Project Name") |
| 176 | + parser.add_argument("-nv", "--no-verify", action='store_false', help="Disable TLS certificate verification") |
| 177 | + |
| 178 | + return parser.parse_args() |
| 179 | + |
| 180 | +def main(): |
| 181 | + args = parse_command_args() |
| 182 | + with open(args.token_file, 'r') as tf: |
| 183 | + access_token = tf.readline().strip() |
| 184 | + global bd |
| 185 | + bd = Client(base_url=args.base_url, token=access_token, verify=args.no_verify, timeout=60.0, retries=4) |
| 186 | + |
| 187 | + if re.match(".+xlsx?$", args.input_file): |
| 188 | + logging.info(f"Processing EXCEL file {args.input_file}") |
| 189 | + process_excel_file(args.input_file) |
| 190 | + else: |
| 191 | + logging.info(f"Processing CSV file {args.input_file}") |
| 192 | + process_csv_file(args.input_file) |
| 193 | + |
| 194 | +if __name__ == "__main__": |
| 195 | + sys.exit(main()) |
0 commit comments