Merge "Add output formatters when relevant"

This commit is contained in:
Zuul 2021-05-20 13:42:20 +00:00 committed by Gerrit Code Review
commit 7fb17c3e86
4 changed files with 288 additions and 58 deletions

View File

@ -1,4 +1,5 @@
pbr>=1.1.0 pbr>=1.1.0
python-dateutil>=2.7.0
requests requests
setuptools setuptools
urllib3!=1.25.4,!=1.25.5 # https://github.com/urllib3/urllib3/pull/1684 urllib3!=1.25.4,!=1.25.5 # https://github.com/urllib3/urllib3/pull/1684

View File

@ -301,7 +301,11 @@ verify_ssl=True"""
'node_expiration': 0, 'node_expiration': 0,
'expired': 0, 'expired': 0,
'reason': 'some_reason', 'reason': 'some_reason',
'nodes': ['node1', 'node2']}) 'nodes': [{'build': 'alalala',
'nodes': ['node1',
'node2']}
],
})
session.get = MagicMock( session.get = MagicMock(
side_effect=mock_get(rv) side_effect=mock_get(rv)

View File

@ -16,16 +16,15 @@ import argparse
import configparser import configparser
import logging import logging
import os import os
import prettytable
import shutil import shutil
import sys import sys
import tempfile import tempfile
import textwrap import textwrap
import time
from zuulclient.api import ZuulRESTClient from zuulclient.api import ZuulRESTClient
from zuulclient.utils import get_default from zuulclient.utils import get_default
from zuulclient.utils import encrypt_with_openssl from zuulclient.utils import encrypt_with_openssl
from zuulclient.utils import formatters
class ArgumentException(Exception): class ArgumentException(Exception):
@ -76,6 +75,9 @@ class ZuulClient():
action='store_false', action='store_false',
help='Do not verify SSL connection to Zuul ' help='Do not verify SSL connection to Zuul '
'(Defaults to False)') '(Defaults to False)')
parser.add_argument('--format', choices=['JSON', 'text'],
default='text', required=False,
help='The output format, when applicable')
self.createCommandParsers(parser) self.createCommandParsers(parser)
return parser return parser
@ -121,6 +123,15 @@ class ZuulClient():
raise ArgumentException( raise ArgumentException(
"The 'change' and 'ref' arguments are mutually exclusive.") "The 'change' and 'ref' arguments are mutually exclusive.")
@property
def formatter(self):
if self.args.format == 'JSON':
return formatters.JSONFormatter
elif self.args.format == 'text':
return formatters.PrettyTableFormatter
else:
raise Exception('Unsupported formatter: %s' % self.args.format)
def readConfig(self): def readConfig(self):
safe_env = { safe_env = {
k: v for k, v in os.environ.items() k: v for k, v in os.environ.items()
@ -270,17 +281,8 @@ class ZuulClient():
print("Autohold request not found") print("Autohold request not found")
return False return False
print("ID: %s" % request['id']) formatted_result = self.formatter('AutoholdQuery')(request)
print("Tenant: %s" % request['tenant']) print(formatted_result)
print("Project: %s" % request['project'])
print("Job: %s" % request['job'])
print("Ref Filter: %s" % request['ref_filter'])
print("Max Count: %s" % request['max_count'])
print("Current Count: %s" % request['current_count'])
print("Node Expiration: %s" % request['node_expiration'])
print("Request Expiration: %s" % time.ctime(request['expired']))
print("Reason: %s" % request['reason'])
print("Held Nodes: %s" % request['nodes'])
return True return True
@ -295,30 +297,15 @@ class ZuulClient():
def autohold_list(self): def autohold_list(self):
client = self.get_client() client = self.get_client()
self._check_tenant_scope(client) self._check_tenant_scope(client)
autohold_requests = client.autohold_list(tenant=self.tenant()) requests = client.autohold_list(tenant=self.tenant())
if not autohold_requests: if not requests:
print("No autohold requests found") print("No autohold requests found")
return True return True
table = prettytable.PrettyTable( formatted_result = self.formatter('AutoholdQueries')(requests)
field_names=[ print(formatted_result)
'ID', 'Tenant', 'Project', 'Job', 'Ref Filter',
'Max Count', 'Reason'
])
for request in autohold_requests:
table.add_row([
request['id'],
request['tenant'],
request['project'],
request['job'],
request['ref_filter'],
request['max_count'],
request['reason'],
])
print(table)
return True return True
def add_enqueue_subparser(self, subparsers): def add_enqueue_subparser(self, subparsers):
@ -676,31 +663,11 @@ class ZuulClient():
if self.args.held: if self.args.held:
filters['held'] = True filters['held'] = True
client = self.get_client() client = self.get_client()
builds = client.builds(tenant=self.tenant(), **filters) request = client.builds(tenant=self.tenant(), **filters)
table = prettytable.PrettyTable(
field_names=[ formatted_result = self.formatter('Builds')(request)
'ID', 'Job', 'Project', 'Branch', 'Pipeline', 'Change or Ref', print(formatted_result)
'Duration (s)', 'Start time', 'Result', 'Event ID'
]
)
for build in builds:
if build['change'] and build['patchset']:
change = str(build['change']) + ',' + str(build['patchset'])
else:
change = build['ref']
table.add_row([
build.get('uuid') or 'N/A',
build['job_name'],
build['project'],
build['branch'],
build['pipeline'],
change,
build['duration'],
build['start_time'],
build['result'],
build.get('event_id') or 'N/A'
])
print(table)
return True return True

View File

@ -0,0 +1,258 @@
# 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
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 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 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')