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
This commit is contained in:
parent
4e4db91c13
commit
978340a694
@ -64,6 +64,15 @@ Examples::
|
|||||||
zuul-client --use-conf sfio builds --tenant mytenant --result NODE_FAILURE
|
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
|
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
|
Dequeue
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add the **build-info** subcommand, allowing a user to fetch the details of
|
||||||
|
a given build by its UUID.
|
@ -4,3 +4,4 @@ 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
|
||||||
PrettyTable
|
PrettyTable
|
||||||
|
pyyaml
|
||||||
|
@ -400,3 +400,59 @@ GuS6/ewjS+arA1Iyeg/IxmECAwEAAQ==
|
|||||||
'https://fake.zuul/api/key/project1.pub'
|
'https://fake.zuul/api/key/project1.pub'
|
||||||
)
|
)
|
||||||
self.assertEqual(pubkey, key)
|
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)
|
||||||
|
@ -161,7 +161,7 @@ verify_ssl=True"""
|
|||||||
ZC._main(['--zuul-url', 'https://fake.zuul',
|
ZC._main(['--zuul-url', 'https://fake.zuul',
|
||||||
'--auth-token', 'aiaiaiai', ] + args)
|
'--auth-token', 'aiaiaiai', ] + args)
|
||||||
session.get = MagicMock(
|
session.get = MagicMock(
|
||||||
side_effect=mock_get(info={'tenant': 'scoped'})
|
side_effect=mock_get(info={'info': {'tenant': 'scoped'}})
|
||||||
)
|
)
|
||||||
with self.assertRaisesRegex(Exception,
|
with self.assertRaisesRegex(Exception,
|
||||||
'scoped to tenant "scoped"'):
|
'scoped to tenant "scoped"'):
|
||||||
@ -198,7 +198,7 @@ verify_ssl=True"""
|
|||||||
self.assertEqual(0, exit_code)
|
self.assertEqual(0, exit_code)
|
||||||
# test scoped
|
# test scoped
|
||||||
session.get = MagicMock(
|
session.get = MagicMock(
|
||||||
side_effect=mock_get(info={'tenant': 'scoped'})
|
side_effect=mock_get(info={'info': {'tenant': 'scoped'}})
|
||||||
)
|
)
|
||||||
exit_code = ZC._main(
|
exit_code = ZC._main(
|
||||||
['--zuul-url', 'https://scoped.zuul',
|
['--zuul-url', 'https://scoped.zuul',
|
||||||
@ -552,7 +552,7 @@ verify_ssl=True"""
|
|||||||
'--pipeline', 'gate',
|
'--pipeline', 'gate',
|
||||||
'--tenant', 'tenant1',
|
'--tenant', 'tenant1',
|
||||||
'--change', '1234', '--job', 'job1', '--held'])
|
'--change', '1234', '--job', 'job1', '--held'])
|
||||||
session.get.assert_called_with(
|
session.get.assert_any_call(
|
||||||
'https://fake.zuul/api/tenant/tenant1/builds',
|
'https://fake.zuul/api/tenant/tenant1/builds',
|
||||||
params={'pipeline': 'gate',
|
params={'pipeline': 'gate',
|
||||||
'change': '1234',
|
'change': '1234',
|
||||||
@ -562,3 +562,68 @@ verify_ssl=True"""
|
|||||||
'limit': 50}
|
'limit': 50}
|
||||||
)
|
)
|
||||||
self.assertEqual(0, exit_code)
|
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)
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
class ZuulRESTException(Exception):
|
class ZuulRESTException(Exception):
|
||||||
@ -63,7 +64,7 @@ class ZuulRESTClient(object):
|
|||||||
'info')
|
'info')
|
||||||
req = self.session.get(url)
|
req = self.session.get(url)
|
||||||
self._check_request_status(req)
|
self._check_request_status(req)
|
||||||
self.info_ = req.json()
|
self.info_ = req.json().get('info', {})
|
||||||
return self.info_
|
return self.info_
|
||||||
|
|
||||||
def _check_request_status(self, req):
|
def _check_request_status(self, req):
|
||||||
@ -270,3 +271,25 @@ class ZuulRESTClient(object):
|
|||||||
req = self.session.get(url, params=kwargs)
|
req = self.session.get(url, params=kwargs)
|
||||||
self._check_request_status(req)
|
self._check_request_status(req)
|
||||||
return req.json()
|
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
|
||||||
|
@ -95,6 +95,7 @@ class ZuulClient():
|
|||||||
self.add_promote_subparser(subparsers)
|
self.add_promote_subparser(subparsers)
|
||||||
self.add_encrypt_subparser(subparsers)
|
self.add_encrypt_subparser(subparsers)
|
||||||
self.add_builds_list_subparser(subparsers)
|
self.add_builds_list_subparser(subparsers)
|
||||||
|
self.add_build_info_subparser(subparsers)
|
||||||
|
|
||||||
return subparsers
|
return subparsers
|
||||||
|
|
||||||
@ -634,11 +635,63 @@ class ZuulClient():
|
|||||||
os.unlink(pubkey_file.name)
|
os.unlink(pubkey_file.name)
|
||||||
return return_code
|
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):
|
def add_builds_list_subparser(self, subparsers):
|
||||||
cmd_builds = subparsers.add_parser(
|
cmd_builds = subparsers.add_parser(
|
||||||
'builds', help='List builds matching search criteria')
|
'builds', help='List builds matching search criteria')
|
||||||
cmd_builds.add_argument(
|
cmd_builds.add_argument(
|
||||||
'--tenant', help='tenant name', required=True)
|
'--tenant', help='tenant name', required=False, default='')
|
||||||
cmd_builds.add_argument(
|
cmd_builds.add_argument(
|
||||||
'--project', help='project name')
|
'--project', help='project name')
|
||||||
cmd_builds.add_argument(
|
cmd_builds.add_argument(
|
||||||
@ -678,6 +731,7 @@ class ZuulClient():
|
|||||||
'--skip', help='how many results to skip',
|
'--skip', help='how many results to skip',
|
||||||
default=0, type=int)
|
default=0, type=int)
|
||||||
cmd_builds.set_defaults(func=self.builds)
|
cmd_builds.set_defaults(func=self.builds)
|
||||||
|
self.cmd_builds = cmd_builds
|
||||||
|
|
||||||
def builds(self):
|
def builds(self):
|
||||||
if self.args.voting and self.args.non_voting:
|
if self.args.voting and self.args.non_voting:
|
||||||
@ -713,6 +767,7 @@ class ZuulClient():
|
|||||||
if self.args.held:
|
if self.args.held:
|
||||||
filters['held'] = True
|
filters['held'] = True
|
||||||
client = self.get_client()
|
client = self.get_client()
|
||||||
|
self._check_tenant_scope(client)
|
||||||
request = client.builds(tenant=self.tenant(), **filters)
|
request = client.builds(tenant=self.tenant(), **filters)
|
||||||
|
|
||||||
formatted_result = self.formatter('Builds')(request)
|
formatted_result = self.formatter('Builds')(request)
|
||||||
|
@ -18,6 +18,7 @@ from dateutil.parser import isoparse
|
|||||||
|
|
||||||
import prettytable
|
import prettytable
|
||||||
import json
|
import json
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
class BaseFormatter:
|
class BaseFormatter:
|
||||||
@ -47,6 +48,9 @@ class BaseFormatter:
|
|||||||
def formatArtifacts(self, data):
|
def formatArtifacts(self, data):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def formatInventory(self, data):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def formatBuild(self, data):
|
def formatBuild(self, data):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@ -178,6 +182,9 @@ class PrettyTableFormatter(BaseFormatter):
|
|||||||
artifact.get('url', 'N/A')])
|
artifact.get('url', 'N/A')])
|
||||||
return str(table)
|
return str(table)
|
||||||
|
|
||||||
|
def formatInventory(self, data) -> str:
|
||||||
|
return yaml.dump(data, default_flow_style=False)
|
||||||
|
|
||||||
def formatBuildSet(self, data) -> str:
|
def formatBuildSet(self, data) -> str:
|
||||||
# This is based on the web UI
|
# This is based on the web UI
|
||||||
output = ''
|
output = ''
|
||||||
|
Loading…
Reference in New Issue
Block a user