
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
266 lines
9.7 KiB
Python
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')
|