blob: 314e21b1025979b2039872d66e41c74fbfd5cfed [file] [log] [blame]
Takuto Ikuta8070a9e2022-08-26 01:23:13 +00001#!/usr/bin/env vpython3
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005"""Client tool to trigger tasks or retrieve results from a Swarming server."""
6
Takuto Ikuta8070a9e2022-08-26 01:23:13 +00007# This spec is for the case swarming.py is used via
8# https://chromium.googlesource.com/infra/luci/client-py
9#
Takuto Ikutad93070f2022-08-26 06:08:23 +000010# [VPYTHON:BEGIN]
Takuto Ikuta8070a9e2022-08-26 01:23:13 +000011# wheel: <
12# name: "infra/python/wheels/pyobjc/${vpython_platform}"
13# version: "version:7.3.chromium.1"
14# match_tag: <
15# platform: "macosx_10_10_intel"
16# >
17# match_tag: <
18# platform: "macosx_11_0_arm64"
19# >
20# >
21# [VPYTHON:END]
22
Lei Leife202df2019-06-11 17:33:34 +000023__version__ = '1.0'
maruel@chromium.org0437a732013-08-27 16:05:52 +000024
maruel@chromium.org0437a732013-08-27 16:05:52 +000025import json
26import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040027import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000028import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000029import sys
maruel11e31af2017-02-15 07:30:50 -080030import textwrap
Junji Watanabe7a677e92022-01-13 06:07:31 +000031import urllib.parse
maruel@chromium.org0437a732013-08-27 16:05:52 +000032
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000033from utils import tools
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000034tools.force_local_third_party()
maruel@chromium.org0437a732013-08-27 16:05:52 +000035
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000036# third_party/
37import colorama
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000038from depot_tools import fix_encoding
39from depot_tools import subcommand
40
41# pylint: disable=ungrouped-imports
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080042import auth
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000043from utils import fs
44from utils import logging_utils
45from utils import net
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000046from utils import subprocess42
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050047
48
49class Failure(Exception):
50 """Generic failure."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050051
52
maruel77f720b2015-09-15 12:35:22 -070053### API management.
54
55
56class APIError(Exception):
57 pass
58
59
60def endpoints_api_discovery_apis(host):
61 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
62 the APIs exposed by a host.
63
64 https://developers.google.com/discovery/v1/reference/apis/list
65 """
maruel380e3262016-08-31 16:10:06 -070066 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
67 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -070068 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
69 if data is None:
70 raise APIError('Failed to discover APIs on %s' % host)
71 out = {}
72 for api in data['items']:
73 if api['id'] == 'discovery:v1':
74 continue
75 # URL is of the following form:
76 # url = host + (
77 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
78 api_data = net.url_read_json(api['discoveryRestUrl'])
79 if api_data is None:
80 raise APIError('Failed to discover %s on %s' % (api['id'], host))
81 out[api['id']] = api_data
82 return out
83
84
maruelaf6b06c2017-06-08 06:26:53 -070085def get_yielder(base_url, limit):
86 """Returns the first query and a function that yields following items."""
87 CHUNK_SIZE = 250
88
89 url = base_url
90 if limit:
91 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
92 data = net.url_read_json(url)
93 if data is None:
94 # TODO(maruel): Do basic diagnostic.
95 raise Failure('Failed to access %s' % url)
96 org_cursor = data.pop('cursor', None)
97 org_total = len(data.get('items') or [])
98 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
99 if not org_cursor or not org_total:
100 # This is not an iterable resource.
101 return data, lambda: []
102
103 def yielder():
104 cursor = org_cursor
105 total = org_total
106 # Some items support cursors. Try to get automatically if cursors are needed
107 # by looking at the 'cursor' items.
108 while cursor and (not limit or total < limit):
109 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000110 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -0700111 if limit:
112 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
113 new = net.url_read_json(url)
114 if new is None:
115 raise Failure('Failed to access %s' % url)
116 cursor = new.get('cursor')
117 new_items = new.get('items')
118 nb_items = len(new_items or [])
119 total += nb_items
120 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
121 yield new_items
122
123 return data, yielder
124
125
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500126### Commands.
127
128
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -0400129@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400130def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -0700131 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
132 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400133
134 Examples:
maruelaf6b06c2017-06-08 06:26:53 -0700135 Raw task request and results:
136 swarming.py query -S server-url.com task/123456/request
137 swarming.py query -S server-url.com task/123456/result
138
maruel77f720b2015-09-15 12:35:22 -0700139 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -0700140 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400141
maruelaf6b06c2017-06-08 06:26:53 -0700142 Listing last 10 tasks on a specific bot named 'bot1':
143 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -0700144
maruelaf6b06c2017-06-08 06:26:53 -0700145 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -0700146 quoting is important!:
147 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -0700148 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400149 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400150 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000151 '-L',
152 '--limit',
153 type='int',
154 default=200,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400155 help='Limit to enforce on limitless items (like number of tasks); '
Junji Watanabecb054042020-07-21 08:43:26 +0000156 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +0100157 parser.add_option(
158 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -0700159 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000160 '--progress',
161 action='store_true',
maruel77f720b2015-09-15 12:35:22 -0700162 help='Prints a dot at each request to show progress')
163 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -0700164 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -0700165 parser.error(
166 'Must specify only method name and optionally query args properly '
167 'escaped.')
smut281c3902018-05-30 17:50:05 -0700168 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -0700169 try:
170 data, yielder = get_yielder(base_url, options.limit)
171 for items in yielder():
172 if items:
173 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -0700174 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -0700175 sys.stderr.write('.')
176 sys.stderr.flush()
177 except Failure as e:
178 sys.stderr.write('\n%s\n' % e)
179 return 1
maruel77f720b2015-09-15 12:35:22 -0700180 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -0700181 sys.stderr.write('\n')
182 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +0100183 if options.json:
Junji Watanabe7a677e92022-01-13 06:07:31 +0000184 options.json = os.path.abspath(options.json)
maruel1ceb3872015-10-14 06:10:44 -0700185 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +0100186 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -0400187 try:
maruel77f720b2015-09-15 12:35:22 -0700188 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -0400189 sys.stdout.write('\n')
190 except IOError:
191 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400192 return 0
193
194
maruel77f720b2015-09-15 12:35:22 -0700195def CMDquery_list(parser, args):
196 """Returns list of all the Swarming APIs that can be used with command
197 'query'.
198 """
199 parser.add_option(
200 '--json', help='Path to JSON output file (otherwise prints to stdout)')
201 options, args = parser.parse_args(args)
202 if args:
203 parser.error('No argument allowed.')
204
205 try:
206 apis = endpoints_api_discovery_apis(options.swarming)
207 except APIError as e:
208 parser.error(str(e))
209 if options.json:
Junji Watanabe7a677e92022-01-13 06:07:31 +0000210 options.json = os.path.abspath(options.json)
maruel1ceb3872015-10-14 06:10:44 -0700211 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -0700212 json.dump(apis, f)
213 else:
214 help_url = (
Junji Watanabecb054042020-07-21 08:43:26 +0000215 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
216 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000217 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -0800218 if i:
219 print('')
Lei Leife202df2019-06-11 17:33:34 +0000220 print(api_id)
221 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -0800222 if 'resources' in api:
223 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000224 # TODO(maruel): Remove.
225 # pylint: disable=too-many-nested-blocks
Junji Watanabecb054042020-07-21 08:43:26 +0000226 for j, (resource_name,
227 resource) in enumerate(sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -0800228 if j:
229 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000230 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -0800231 # Only list the GET ones.
232 if method['httpMethod'] != 'GET':
233 continue
Junji Watanabecb054042020-07-21 08:43:26 +0000234 print('- %s.%s: %s' % (resource_name, method_name, method['path']))
235 print('\n'.join(' ' + l for l in textwrap.wrap(
236 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +0000237 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -0800238 else:
239 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000240 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -0700241 # Only list the GET ones.
242 if method['httpMethod'] != 'GET':
243 continue
Lei Leife202df2019-06-11 17:33:34 +0000244 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -0800245 print('\n'.join(
246 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +0000247 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -0700248 return 0
249
250
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400251class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +0000252
maruel@chromium.org0437a732013-08-27 16:05:52 +0000253 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400254 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000255 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400256 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500257 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000258 '-S',
259 '--swarming',
260 metavar='URL',
261 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000262 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500263 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800264 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000265
266 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400267 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000268 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -0500269 auth.process_auth_options(self, options)
270 user = self._process_swarming(options)
271 if hasattr(options, 'user') and not options.user:
272 options.user = user
273 return options, args
274
275 def _process_swarming(self, options):
276 """Processes the --swarming option and aborts if not specified.
277
278 Returns the identity as determined by the server.
279 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000280 if not options.swarming:
281 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -0500282 try:
283 options.swarming = net.fix_url(options.swarming)
284 except ValueError as e:
285 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +0000286
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500287 try:
288 user = auth.ensure_logged_in(options.swarming)
289 except ValueError as e:
290 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -0500291 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +0000292
293
294def main(args):
295 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400296 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000297
298
299if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700300 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +0000301 fix_encoding.fix_encoding()
302 tools.disable_buffering()
303 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +0000304 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000305 sys.exit(main(sys.argv[1:]))