Merge "Add build-info subcommand"
This commit is contained in:
commit
c00be905a6
@ -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
|
||||
^^^^^^^
|
||||
|
||||
|
@ -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
|
||||
urllib3!=1.25.4,!=1.25.5 # https://github.com/urllib3/urllib3/pull/1684
|
||||
PrettyTable
|
||||
pyyaml
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 = ''
|
||||
|
Loading…
x
Reference in New Issue
Block a user