21785e1464
This handles the old and new data format for the builds/ and build/ endpoints. Builds will soon include multiple refs. Change-Id: I1db5e9fc3fd38864f72b914e09629d3d0637f355
357 lines
13 KiB
Python
357 lines
13 KiB
Python
# 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
|