Merge "Add build-info subcommand"

This commit is contained in:
Zuul 2021-06-09 09:26:49 +00:00 committed by Gerrit Code Review
commit c00be905a6
8 changed files with 226 additions and 5 deletions

View File

@ -64,6 +64,15 @@ Examples::
zuul-client --use-conf sfio builds --tenant mytenant --result NODE_FAILURE
zuul-client --use-conf opendev builds --tenant zuul --project zuul/zuul-client --limit 10
Build-info
^^^^^^^^^^
.. program-output:: zuul-client build-info --help
Examples::
zuul-client build-info --tenant mytenant --uuid aaaaa
zuul-client build-info --tenant mytenant --uuid aaaaa --show-job-output
Dequeue
^^^^^^^

View File

@ -0,0 +1,5 @@
---
features:
- |
Add the **build-info** subcommand, allowing a user to fetch the details of
a given build by its UUID.

View File

@ -4,3 +4,4 @@ requests
setuptools
urllib3!=1.25.4,!=1.25.5 # https://github.com/urllib3/urllib3/pull/1684
PrettyTable
pyyaml

View File

@ -400,3 +400,59 @@ GuS6/ewjS+arA1Iyeg/IxmECAwEAAQ==
'https://fake.zuul/api/key/project1.pub'
)
self.assertEqual(pubkey, key)
def test_build(self):
"""Test build endpoint"""
client = ZuulRESTClient(url='https://fake.zuul/')
# test status checks
self._test_status_check(
client, 'get', client.build, 'tenant1', 'a1a1a1a1')
fakejson = {
'uuid': 'a1a1a1a1',
'job_name': 'tox-py38',
'result': 'SUCCESS',
'held': False,
'start_time': '2020-09-10T14:08:55',
'end_time': '2020-09-10T14:13:35',
'duration': 280.0,
'voting': True,
'log_url': 'https://log.storage/',
'node_name': None,
'error_detail': None,
'final': True,
'artifacts': [
{'name': 'Download all logs',
'url': 'https://log.storage/download-logs.sh',
'metadata': {
'command': 'xxx'}
},
{'name': 'Zuul Manifest',
'url': 'https://log.storage/zuul-manifest.json',
'metadata': {
'type': 'zuul_manifest'
}
},
{'name': 'Unit Test Report',
'url': 'https://log.storage/testr_results.html',
'metadata': {
'type': 'unit_test_report'
}
}],
'provides': [],
'project': 'project1',
'branch': 'master',
'pipeline': 'check',
'change': 1234,
'patchset': '1',
'ref': 'refs/changes/34/1234/1',
'newrev': None,
'ref_url': 'https://gerrit/1234',
'event_id': '6b28762adfce415ba47e440c365ae624',
'buildset': {'uuid': 'b1b1b1'}}
req = FakeRequestResponse(200, fakejson)
client.session.get = MagicMock(return_value=req)
ahl = client.build(tenant='tenant1', uuid='a1a1a1a1')
client.session.get.assert_any_call(
'https://fake.zuul/api/tenant/tenant1/build/a1a1a1a1')
self.assertEqual(fakejson, ahl)

View File

@ -161,7 +161,7 @@ verify_ssl=True"""
ZC._main(['--zuul-url', 'https://fake.zuul',
'--auth-token', 'aiaiaiai', ] + args)
session.get = MagicMock(
side_effect=mock_get(info={'tenant': 'scoped'})
side_effect=mock_get(info={'info': {'tenant': 'scoped'}})
)
with self.assertRaisesRegex(Exception,
'scoped to tenant "scoped"'):
@ -198,7 +198,7 @@ verify_ssl=True"""
self.assertEqual(0, exit_code)
# test scoped
session.get = MagicMock(
side_effect=mock_get(info={'tenant': 'scoped'})
side_effect=mock_get(info={'info': {'tenant': 'scoped'}})
)
exit_code = ZC._main(
['--zuul-url', 'https://scoped.zuul',
@ -552,7 +552,7 @@ verify_ssl=True"""
'--pipeline', 'gate',
'--tenant', 'tenant1',
'--change', '1234', '--job', 'job1', '--held'])
session.get.assert_called_with(
session.get.assert_any_call(
'https://fake.zuul/api/tenant/tenant1/builds',
params={'pipeline': 'gate',
'change': '1234',
@ -562,3 +562,68 @@ verify_ssl=True"""
'limit': 50}
)
self.assertEqual(0, exit_code)
def test_build_info(self):
"""Test build-info subcommand"""
ZC = ZuulClient()
with self.assertRaisesRegex(Exception,
'--show-artifacts, --show-job-output and '
'--show-inventory are mutually exclusive'):
exit_code = ZC._main(
['--zuul-url', 'https://fake.zuul',
'build-info', '--tenant', 'tenant1',
'--uuid', 'a1a1a1a1',
'--show-artifacts', '--show-job-output'])
with patch('requests.Session') as mock_sesh:
session = mock_sesh.return_value
fakejson = {
'uuid': 'a1a1a1a1',
'job_name': 'tox-py38',
'result': 'SUCCESS',
'held': False,
'start_time': '2020-09-10T14:08:55',
'end_time': '2020-09-10T14:13:35',
'duration': 280.0,
'voting': True,
'log_url': 'https://log.storage/',
'node_name': None,
'error_detail': None,
'final': True,
'artifacts': [
{'name': 'Download all logs',
'url': 'https://log.storage/download-logs.sh',
'metadata': {
'command': 'xxx'}
},
{'name': 'Zuul Manifest',
'url': 'https://log.storage/zuul-manifest.json',
'metadata': {
'type': 'zuul_manifest'
}
},
{'name': 'Unit Test Report',
'url': 'https://log.storage/testr_results.html',
'metadata': {
'type': 'unit_test_report'
}
}],
'provides': [],
'project': 'project1',
'branch': 'master',
'pipeline': 'check',
'change': 1234,
'patchset': '1',
'ref': 'refs/changes/34/1234/1',
'newrev': None,
'ref_url': 'https://gerrit/1234',
'event_id': '6b28762adfce415ba47e440c365ae624',
'buildset': {'uuid': 'b1b1b1'}}
session.get = MagicMock(
return_value=FakeRequestResponse(200, fakejson))
exit_code = ZC._main(
['--zuul-url', 'https://fake.zuul',
'build-info', '--tenant', 'tenant1',
'--uuid', 'a1a1a1a1'])
session.get.assert_any_call(
'https://fake.zuul/api/tenant/tenant1/build/a1a1a1a1')
self.assertEqual(0, exit_code)

View File

@ -15,6 +15,7 @@
import requests
import urllib.parse
import yaml
class ZuulRESTException(Exception):
@ -63,7 +64,7 @@ class ZuulRESTClient(object):
'info')
req = self.session.get(url)
self._check_request_status(req)
self.info_ = req.json()
self.info_ = req.json().get('info', {})
return self.info_
def _check_request_status(self, req):
@ -270,3 +271,25 @@ class ZuulRESTClient(object):
req = self.session.get(url, params=kwargs)
self._check_request_status(req)
return req.json()
def build(self, tenant, uuid):
if self.info.get("tenant"):
self._check_scope(tenant)
suffix = "build/%s" % uuid
else:
suffix = "tenant/%s/build/%s" % (tenant, uuid)
url = urllib.parse.urljoin(self.base_url, suffix)
req = self.session.get(url)
self._check_request_status(req)
build_info = req.json()
build_info['job_output_url'] = urllib.parse.urljoin(
build_info['log_url'], 'job-output.txt')
inventory_url = urllib.parse.urljoin(
build_info['log_url'], 'zuul-info/inventory.yaml')
try:
raw_inventory = self.session.get(inventory_url)
build_info['inventory'] = yaml.load(raw_inventory.text,
Loader=yaml.SafeLoader)
except Exception as e:
build_info['inventory'] = {'error': str(e)}
return build_info

View File

@ -106,6 +106,7 @@ class ZuulClient():
self.add_promote_subparser(subparsers)
self.add_encrypt_subparser(subparsers)
self.add_builds_list_subparser(subparsers)
self.add_build_info_subparser(subparsers)
return subparsers
@ -646,11 +647,63 @@ class ZuulClient():
os.unlink(pubkey_file.name)
return return_code
def add_build_info_subparser(self, subparsers):
cmd_build_info = subparsers.add_parser(
'build-info', help='Get info on a specific build')
cmd_build_info.add_argument(
'--tenant', help='tenant name', required=False, default='')
cmd_build_info.add_argument(
'--uuid', help='build UUID', required=True)
cmd_build_info.add_argument(
'--show-job-output', default=False, action='store_true',
help='Only download the job\'s output to the console')
cmd_build_info.add_argument(
'--show-artifacts', default=False, action='store_true',
help='Display only artifacts information for the build')
cmd_build_info.add_argument(
'--show-inventory', default=False, action='store_true',
help='Display only ansible inventory information for the build')
cmd_build_info.set_defaults(func=self.build_info)
self.cmd_build_info = cmd_build_info
def build_info(self):
if sum(map(lambda x: x and 1 or 0,
[self.args.show_artifacts,
self.args.show_job_output,
self.args.show_inventory])
) > 1:
raise Exception(
'--show-artifacts, --show-job-output and '
'--show-inventory are mutually exclusive'
)
client = self.get_client()
self._check_tenant_scope(client)
build = client.build(self.tenant(), self.args.uuid)
if not build:
print('Build not found')
return False
if self.args.show_job_output:
output = client.session.get(build['job_output_url'])
client._check_request_status(output)
formatted_result = output.text
elif self.args.show_artifacts:
formatted_result = self.formatter('Artifacts')(
build.get('artifacts', [])
)
elif self.args.show_inventory:
formatted_result = self.formatter('Inventory')(
build.get('inventory', {})
)
else:
formatted_result = self.formatter('Build')(build)
print(formatted_result)
return True
def add_builds_list_subparser(self, subparsers):
cmd_builds = subparsers.add_parser(
'builds', help='List builds matching search criteria')
cmd_builds.add_argument(
'--tenant', help='tenant name', required=True)
'--tenant', help='tenant name', required=False, default='')
cmd_builds.add_argument(
'--project', help='project name')
cmd_builds.add_argument(
@ -690,6 +743,7 @@ class ZuulClient():
'--skip', help='how many results to skip',
default=0, type=int)
cmd_builds.set_defaults(func=self.builds)
self.cmd_builds = cmd_builds
def builds(self):
if self.args.voting and self.args.non_voting:
@ -725,6 +779,7 @@ class ZuulClient():
if self.args.held:
filters['held'] = True
client = self.get_client()
self._check_tenant_scope(client)
request = client.builds(tenant=self.tenant(), **filters)
formatted_result = self.formatter('Builds')(request)

View File

@ -18,6 +18,7 @@ from dateutil.parser import isoparse
import prettytable
import json
import yaml
class BaseFormatter:
@ -47,6 +48,9 @@ class BaseFormatter:
def formatArtifacts(self, data):
raise NotImplementedError
def formatInventory(self, data):
raise NotImplementedError
def formatBuild(self, data):
raise NotImplementedError
@ -178,6 +182,9 @@ class PrettyTableFormatter(BaseFormatter):
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 = ''