# Copyright 2021 Red Hat, inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import time from dateutil.parser import isoparse import pprint import prettytable import json import yaml class BaseFormatter: def __init__(self, data_type): self.data_type = data_type def __call__(self, data): """Format data according to the type of data being displayed.""" try: return getattr(self, 'format' + self.data_type)(data) except Exception: raise Exception('Unsupported data type "%s"' % self.data_type) def formatBuildNodes(self, data): raise NotImplementedError def formatAutoholdQueries(self, data): raise NotImplementedError def formatAutoholdQuery(self, data): raise NotImplementedError def formatJobResource(self, data): raise NotImplementedError def formatArtifacts(self, data): raise NotImplementedError def formatInventory(self, data): raise NotImplementedError def formatBuild(self, data): raise NotImplementedError def formatBuildSet(self, data): raise NotImplementedError def formatBuilds(self, data): raise NotImplementedError def formatBuildSets(self, data): raise NotImplementedError def formatJobGraph(self, data): raise NotImplementedError def formatFreezeJob(self, data): raise NotImplementedError class JSONFormatter(BaseFormatter): def __call__(self, data) -> str: # Simply format the raw dictionary returned by the API return json.dumps(data, sort_keys=True, indent=2) class PrettyTableFormatter(BaseFormatter): """Format Zuul data in a nice human-readable way for the CLI.""" def formatAutoholdQuery(self, data) -> str: text = "" text += "ID: %s\n" % data.get('id', 'N/A') text += "Tenant: %s\n" % data.get('tenant', 'N/A') text += "Project: %s\n" % data.get('project', 'N/A') text += "Job: %s\n" % data.get('job', 'N/A') text += "Ref Filter: %s\n" % data.get('ref_filter', 'N/A') text += "Max Count: %s\n" % (data.get('max_count', None) or data.get('count', 'N/A')) text += "Current Count: %s\n" % data.get('current_count', 'N/A') text += "Node Expiration: %s\n" % ( data.get('node_expiration', None) or data.get('node_hold_expiration', 'N/A') ) text += "Request Expiration: %s\n" % ( data.get('expired', None) and time.ctime(data['expired']) or 'N/A' ) text += "Reason: %s\n" % data.get('reason', 'N/A') text += "Held Nodes:\n" for buildnodes in data.get('nodes', []): text += self.formatBuildNodes(buildnodes) return text def formatBuildNodes(self, data) -> str: table = prettytable.PrettyTable(field_names=['Build ID', 'Node ID']) for node in data.get('nodes', []): table.add_row([data.get('build', 'N/A'), node]) return str(table) def formatAutoholdQueries(self, data) -> str: table = prettytable.PrettyTable( field_names=[ 'ID', 'Tenant', 'Project', 'Job', 'Ref Filter', 'Max Count', 'Reason' ]) for request in data: table.add_row([ request.get('id', 'N/A'), request.get('tenant', 'N/A'), request.get('project', 'N/A'), request.get('job', 'N/A'), request.get('ref_filter', 'N/A'), request.get('max_count', None) or request.get('count', 'N/A'), request.get('reason', 'N/A'), ]) return str(table) def formatBuild(self, data) -> str: if 'project' in data: ref = data else: ref = data['ref'] output = '' # This is based on the web UI output += 'UUID: %s\n' % data.get('uuid', 'N/A') output += '=' * len('UUID: %s' % data.get('uuid', 'N/A')) + '\n' output += 'Result: %s\n' % data.get('result', 'N/A') output += 'Pipeline: %s\n' % data.get('pipeline', 'N/A') output += 'Project: %s\n' % ref.get('project', 'N/A') output += 'Job: %s\n' % data.get('job_name', 'N/A') if ref.get('newrev'): output += 'Ref: %s\n' % ref.get('ref', 'N/A') output += 'New Rev: %s\n' % ref['newrev'] if ref.get('change') and ref.get('patchset'): output += 'Change: %s\n' % (str(ref['change']) + ',' + str(ref['patchset'])) output += 'Branch: %s\n' % ref.get('branch', 'N/A') output += 'Ref URL: %s\n' % ref.get('ref_url', 'N/A') output += 'Event ID: %s\n' % data.get('event_id', 'N/A') output += 'Buildset ID: %s\n' % data.get('buildset', {}).get('uuid', 'N/A') output += 'Start time: %s\n' % ( data.get('start_time') and isoparse(data['start_time']) or 'N/A' ) output += 'End time: %s\n' % ( data.get('end_time') and isoparse(data['end_time']) or 'N/A' ) output += 'Duration: %s\n' % data.get('duration', 'N/A') output += 'Voting: %s\n' % (data.get('voting') and 'Yes' or 'No') output += 'Log URL: %s\n' % data.get('log_url', 'N/A') output += 'Node: %s\n' % data.get('node_name', 'N/A') provides = data.get('provides', []) if provides: output += 'Provides:\n' for resource in provides: output += '- %s\n' % self.formatJobResource(resource) if data.get('final', None) is not None: output += 'Final: %s\n' % (data['final'] and 'Yes' or 'No') else: output += 'Final: N/A\n' if data.get('held', None) is not None: output += 'Held: %s' % (data['held'] and 'Yes' or 'No') else: output += 'Held: N/A' return output def formatArtifacts(self, data) -> str: table = prettytable.PrettyTable( field_names=['name', 'url'] ) for artifact in data: table.add_row([artifact.get('name', 'N/A'), artifact.get('url', 'N/A')]) return str(table) def formatInventory(self, data) -> str: return yaml.dump(data, default_flow_style=False) def formatBuildSet(self, data) -> str: # This is based on the web UI output = '' output += 'UUID: %s\n' % data.get('uuid', 'N/A') output += '=' * len('UUID: %s' % data.get('uuid', 'N/A')) + '\n' output += 'Result: %s\n' % data.get('result', 'N/A') if data.get('newrev'): output += 'Ref: %s\n' % data.get('ref', 'N/A') output += 'New Rev: %s\n' % data['newrev'] if data.get('change') and data.get('patchset'): output += 'Change: %s\n' % (str(data['change']) + ',' + str(data['patchset'])) output += 'Project: %s\n' % data.get('project', 'N/A') output += 'Branch: %s\n' % data.get('branch', 'N/A') output += 'Pipeline: %s\n' % data.get('pipeline', 'N/A') output += 'Event ID: %s\n' % data.get('event_id', 'N/A') output += 'Message: %s' % data.get('message', 'N/A') return output def formatBuildSets(self, data) -> str: table = prettytable.PrettyTable( field_names=[ 'ID', 'Project', 'Branch', 'Pipeline', 'Change or Ref', 'Result', 'Event ID' ] ) for buildset in data: if buildset.get('change') and buildset.get('patchset'): change = ( str(buildset['change']) + ',' + str(buildset['patchset']) ) else: change = buildset.get('ref', 'N/A') table.add_row([ buildset.get('uuid', 'N/A'), buildset.get('project', 'N/A'), buildset.get('branch', 'N/A'), buildset.get('pipeline', 'N/A'), change, buildset.get('result', 'N/A'), buildset.get('event_id', 'N/A') ]) return str(table) def formatBuilds(self, data) -> str: table = prettytable.PrettyTable( field_names=[ 'ID', 'Job', 'Project', 'Branch', 'Pipeline', 'Change or Ref', 'Duration (s)', 'Start time', 'Result', 'Event ID' ] ) for build in data: if 'project' in build: ref = build else: ref = build['ref'] if ref.get('change') and ref.get('patchset'): change = str(ref['change']) + ',' + str(ref['patchset']) else: change = ref.get('ref', 'N/A') start_time = ( build.get('start_time') and isoparse(build['start_time']) or 'N/A' ) table.add_row([ build.get('uuid', 'N/A'), build.get('job_name', 'N/A'), ref.get('project', 'N/A'), ref.get('branch', 'N/A'), build.get('pipeline', 'N/A'), change, build.get('duration', 'N/A'), start_time, build.get('result', 'N/A'), build.get('event_id', 'N/A') ]) return str(table) def formatJobResource(self, data) -> str: return data.get('name', 'N/A') def formatJobGraph(self, data) -> str: table = prettytable.PrettyTable( field_names=['Job', 'Dependencies'] ) table.align = 'l' for job in data: deps = [] for dep in job.get('dependencies', []): d = dep['name'] if dep['soft']: d += ' (soft)' deps.append(d) table.add_row([ job.get('name', 'N/A'), ', '.join(deps), ]) return str(table) def formatFreezeJob(self, data) -> str: printer = pprint.PrettyPrinter(indent=2) ret = '' for label, key in [ ('Job', 'job'), ('Branch', 'branch'), ('Ansible Version', 'ansible_version'), ('Timeout', 'timeout'), ('Post Timeout', 'post_timeout'), ('Workspace Scheme', 'workspace_scheme'), ('Override Checkout', 'override_checkout'), ]: value = data.get(key) if value is not None: ret += f'{label}: {value}\n' if data['nodeset']['name']: ret += f"Nodeset: {data['nodeset']['name']}\n" for label, key in [ ('Pre-run Playbooks', 'pre_playbooks'), ('Run Playbooks', 'playbooks'), ('Post-run Playbooks', 'post_playbooks'), ('Cleanup Playbooks', 'cleanup_playbooks'), ]: pbs = data.get(key) if not pbs: continue ret += f"{label}:\n" for pb in pbs: trusted = ' [trusted]' if pb['trusted'] else '' ret += (f" {pb['connection']}:{pb['project']}:" f"{pb['path']}@{pb['branch']}{trusted}\n") ret += 'Vars:\n' ret += printer.pformat(data['vars']) return ret class DotFormatter(BaseFormatter): """Format for graphviz""" def formatJobGraph(self, data) -> str: ret = 'digraph job_graph {\n' ret += ' rankdir=LR;\n' ret += ' node [shape=box];\n' for job in data: name = job['name'] deps = job.get('dependencies', []) if deps: for dep in deps: if dep['soft']: soft = ' [style=dashed dir=back]' else: soft = ' [dir=back]' ret += f""" "{dep['name']}" -> "{name}"{soft};\n""" else: ret += f' "{name}";\n' ret += '}\n' return ret