zuul-client/zuulclient/utils/formatters.py
Matthieu Huin 978340a694 Add build-info subcommand
The build-info subcommand fetches detailed information on a build given
its UUID. It is possible to also either list the build's artifacts, the
Ansible inventory used for the job, or download the build's console output.

Fix incorrect info fetching.

Change-Id: I1707ab083e4964a8ac410a7421f64acaffe06023
2021-05-21 12:38:51 +02:00

266 lines
9.7 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 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
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:
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' % data.get('project', 'N/A')
output += 'Job: %s\n' % data.get('job_name', '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 += 'Branch: %s\n' % data.get('branch', 'N/A')
output += 'Ref URL: %s\n' % data.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 build.get('change') and build.get('patchset'):
change = str(build['change']) + ',' + str(build['patchset'])
else:
change = build.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'),
build.get('project', 'N/A'),
build.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')