import csv import os import re from datetime import datetime import urllib3 from urllib3 import Timeout JENKINS_USER = os.getenv('JENKINS_USER', 'jdenniss') JENKINS_API_TOKEN = os.getenv('JENKINS_API_TOKEN') JENKINS_URL = os.getenv('JENKINS_INSTANCE', 'https://scheduler.theorchard.io') TOOL_TO_FIND = os.getenv('TOOL_TO_FIND', 'python3.8') DOCKER_BUILD_REGEX = r'^(\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] )?#\d+.*' def main(): job_info = get_json(f'{JENKINS_URL}/api/json') found_jobs = [] for job in job_info['jobs']: found_jobs.extend(handle_job(job)) with open(f'{TOOL_TO_FIND}.csv', 'w') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=['name', 'url', 'tool', 'last_built', 'in_job_config', 'in_last_build_output']) writer.writeheader() for job in found_jobs: writer.writerow(job) def get_data(url): response = get_request(url) return response.data.decode('utf8') def get_json(url): response = get_request(url) return response.json() def get_request(url): headers = urllib3.make_headers(basic_auth=f'{JENKINS_USER}:{JENKINS_API_TOKEN}') return urllib3.request('GET', url, headers=headers, timeout=Timeout(connect=5.0, read=30.0)) def handle_job(job): found_jobs = [] job_type = job['_class'] if job_type == 'org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject': found_jobs.extend(handle_multibranch_project(job)) elif job_type == 'jenkins.branch.OrganizationFolder': found_jobs.extend(handle_org_folder(job)) else: found_jobs.extend(handle_standalone_project(job)) return found_jobs def handle_org_folder(job): found_jobs = [] print(f'Processing org folder {job["name"]}') job_info = get_json(f'{job["url"]}/api/json') for job in job_info['jobs']: found_jobs.extend(handle_job(job)) return found_jobs def handle_multibranch_project(job): print(f'Processing multibranch project {job["name"]}') job_info = get_json(f'{job["url"]}/api/json') for job in job_info['jobs']: if job['name'] == 'master': in_last_build_output = check_last_build(job) if in_last_build_output: return [{ 'name': parse_name(job['url']), 'url': job['url'], 'tool': TOOL_TO_FIND, 'last_built': get_last_built(job), 'in_job_config': False, 'in_last_build_output': True }] return [] def handle_standalone_project(job): print(f'Processing standalone job {job["name"]}') job_config = get_data(f'{job["url"]}/config.xml') in_job_config = False if TOOL_TO_FIND in job_config: in_job_config = True in_last_build_output = check_last_build(job) if in_job_config or in_last_build_output: return [{ 'name': parse_name(job['url']), 'url': job['url'], 'tool': TOOL_TO_FIND, 'last_built': get_last_built(job), 'in_job_config': in_job_config, 'in_last_build_output': in_last_build_output }] else: return [] def check_last_build(job): job_info = get_json(f'{job["url"]}/api/json') last_successful_build = job_info['lastSuccessfulBuild'] if last_successful_build: try: last_build_output = get_data(f'{last_successful_build["url"]}/consoleText') for line in last_build_output.splitlines(): # Only consider lines that have a space after the tool name, to filter out references which are not actually invocations of the tool # Also filter out lines that look like Docker build output if f'{TOOL_TO_FIND} ' in line and not re.match(DOCKER_BUILD_REGEX, line): return True except Exception as e: print(f'Error processing build {last_successful_build["url"]}: {e}') return False def get_last_built(job): job_info = get_json(f'{job["url"]}/api/json') last_build = job_info['lastBuild'] if last_build: last_build_info = get_json(f'{last_build["url"]}/api/json') timestamp = last_build_info['timestamp'] return datetime.fromtimestamp(timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S') return 'N/A' def parse_name(url): name = url.removeprefix(f'{JENKINS_URL}/job/') name = name.replace('/job', '') name = name.removesuffix('/') return name if __name__ == '__main__': main()