Merge "Add output formatters when relevant"
This commit is contained in:
commit
7fb17c3e86
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
258
zuulclient/utils/formatters.py
Normal file
258
zuulclient/utils/formatters.py
Normal 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')
|
Loading…
Reference in New Issue
Block a user