blob: 973daaee0c29aac49e7c4e1bda63af9158c85177 [file] [log] [blame]
Junji Watanabe7a677e92022-01-13 06:07:31 +00001#!/usr/bin/env python3
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
Lei Leife202df2019-06-11 17:33:34 +00007__version__ = '1.0'
maruel@chromium.org0437a732013-08-27 16:05:52 +00008
maruel@chromium.org0437a732013-08-27 16:05:52 +00009import json
10import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040011import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000013import sys
maruel11e31af2017-02-15 07:30:50 -080014import textwrap
Junji Watanabe7a677e92022-01-13 06:07:31 +000015import urllib.parse
maruel@chromium.org0437a732013-08-27 16:05:52 +000016
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000017from utils import tools
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000018tools.force_local_third_party()
maruel@chromium.org0437a732013-08-27 16:05:52 +000019
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000020# third_party/
21import colorama
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000022from depot_tools import fix_encoding
23from depot_tools import subcommand
24
25# pylint: disable=ungrouped-imports
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080026import auth
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000027from utils import fs
28from utils import logging_utils
29from utils import net
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000030from utils import subprocess42
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050031
32
33class Failure(Exception):
34 """Generic failure."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050035
36
maruel77f720b2015-09-15 12:35:22 -070037### API management.
38
39
40class APIError(Exception):
41 pass
42
43
44def endpoints_api_discovery_apis(host):
45 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
46 the APIs exposed by a host.
47
48 https://developers.google.com/discovery/v1/reference/apis/list
49 """
maruel380e3262016-08-31 16:10:06 -070050 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
51 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -070052 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
53 if data is None:
54 raise APIError('Failed to discover APIs on %s' % host)
55 out = {}
56 for api in data['items']:
57 if api['id'] == 'discovery:v1':
58 continue
59 # URL is of the following form:
60 # url = host + (
61 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
62 api_data = net.url_read_json(api['discoveryRestUrl'])
63 if api_data is None:
64 raise APIError('Failed to discover %s on %s' % (api['id'], host))
65 out[api['id']] = api_data
66 return out
67
68
maruelaf6b06c2017-06-08 06:26:53 -070069def get_yielder(base_url, limit):
70 """Returns the first query and a function that yields following items."""
71 CHUNK_SIZE = 250
72
73 url = base_url
74 if limit:
75 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
76 data = net.url_read_json(url)
77 if data is None:
78 # TODO(maruel): Do basic diagnostic.
79 raise Failure('Failed to access %s' % url)
80 org_cursor = data.pop('cursor', None)
81 org_total = len(data.get('items') or [])
82 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
83 if not org_cursor or not org_total:
84 # This is not an iterable resource.
85 return data, lambda: []
86
87 def yielder():
88 cursor = org_cursor
89 total = org_total
90 # Some items support cursors. Try to get automatically if cursors are needed
91 # by looking at the 'cursor' items.
92 while cursor and (not limit or total < limit):
93 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +000094 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -070095 if limit:
96 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
97 new = net.url_read_json(url)
98 if new is None:
99 raise Failure('Failed to access %s' % url)
100 cursor = new.get('cursor')
101 new_items = new.get('items')
102 nb_items = len(new_items or [])
103 total += nb_items
104 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
105 yield new_items
106
107 return data, yielder
108
109
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110### Commands.
111
112
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -0400113@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400114def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -0700115 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
116 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400117
118 Examples:
maruelaf6b06c2017-06-08 06:26:53 -0700119 Raw task request and results:
120 swarming.py query -S server-url.com task/123456/request
121 swarming.py query -S server-url.com task/123456/result
122
maruel77f720b2015-09-15 12:35:22 -0700123 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -0700124 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400125
maruelaf6b06c2017-06-08 06:26:53 -0700126 Listing last 10 tasks on a specific bot named 'bot1':
127 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -0700128
maruelaf6b06c2017-06-08 06:26:53 -0700129 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -0700130 quoting is important!:
131 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -0700132 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400133 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400134 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000135 '-L',
136 '--limit',
137 type='int',
138 default=200,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400139 help='Limit to enforce on limitless items (like number of tasks); '
Junji Watanabecb054042020-07-21 08:43:26 +0000140 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +0100141 parser.add_option(
142 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -0700143 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000144 '--progress',
145 action='store_true',
maruel77f720b2015-09-15 12:35:22 -0700146 help='Prints a dot at each request to show progress')
147 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -0700148 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -0700149 parser.error(
150 'Must specify only method name and optionally query args properly '
151 'escaped.')
smut281c3902018-05-30 17:50:05 -0700152 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -0700153 try:
154 data, yielder = get_yielder(base_url, options.limit)
155 for items in yielder():
156 if items:
157 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -0700158 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -0700159 sys.stderr.write('.')
160 sys.stderr.flush()
161 except Failure as e:
162 sys.stderr.write('\n%s\n' % e)
163 return 1
maruel77f720b2015-09-15 12:35:22 -0700164 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -0700165 sys.stderr.write('\n')
166 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +0100167 if options.json:
Junji Watanabe7a677e92022-01-13 06:07:31 +0000168 options.json = os.path.abspath(options.json)
maruel1ceb3872015-10-14 06:10:44 -0700169 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +0100170 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -0400171 try:
maruel77f720b2015-09-15 12:35:22 -0700172 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -0400173 sys.stdout.write('\n')
174 except IOError:
175 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400176 return 0
177
178
maruel77f720b2015-09-15 12:35:22 -0700179def CMDquery_list(parser, args):
180 """Returns list of all the Swarming APIs that can be used with command
181 'query'.
182 """
183 parser.add_option(
184 '--json', help='Path to JSON output file (otherwise prints to stdout)')
185 options, args = parser.parse_args(args)
186 if args:
187 parser.error('No argument allowed.')
188
189 try:
190 apis = endpoints_api_discovery_apis(options.swarming)
191 except APIError as e:
192 parser.error(str(e))
193 if options.json:
Junji Watanabe7a677e92022-01-13 06:07:31 +0000194 options.json = os.path.abspath(options.json)
maruel1ceb3872015-10-14 06:10:44 -0700195 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -0700196 json.dump(apis, f)
197 else:
198 help_url = (
Junji Watanabecb054042020-07-21 08:43:26 +0000199 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
200 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000201 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -0800202 if i:
203 print('')
Lei Leife202df2019-06-11 17:33:34 +0000204 print(api_id)
205 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -0800206 if 'resources' in api:
207 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000208 # TODO(maruel): Remove.
209 # pylint: disable=too-many-nested-blocks
Junji Watanabecb054042020-07-21 08:43:26 +0000210 for j, (resource_name,
211 resource) in enumerate(sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -0800212 if j:
213 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000214 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -0800215 # Only list the GET ones.
216 if method['httpMethod'] != 'GET':
217 continue
Junji Watanabecb054042020-07-21 08:43:26 +0000218 print('- %s.%s: %s' % (resource_name, method_name, method['path']))
219 print('\n'.join(' ' + l for l in textwrap.wrap(
220 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +0000221 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -0800222 else:
223 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000224 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -0700225 # Only list the GET ones.
226 if method['httpMethod'] != 'GET':
227 continue
Lei Leife202df2019-06-11 17:33:34 +0000228 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -0800229 print('\n'.join(
230 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +0000231 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -0700232 return 0
233
234
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400235class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +0000236
maruel@chromium.org0437a732013-08-27 16:05:52 +0000237 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400238 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000239 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400240 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500241 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000242 '-S',
243 '--swarming',
244 metavar='URL',
245 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000246 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500247 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800248 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000249
250 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400251 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000252 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -0500253 auth.process_auth_options(self, options)
254 user = self._process_swarming(options)
255 if hasattr(options, 'user') and not options.user:
256 options.user = user
257 return options, args
258
259 def _process_swarming(self, options):
260 """Processes the --swarming option and aborts if not specified.
261
262 Returns the identity as determined by the server.
263 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000264 if not options.swarming:
265 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -0500266 try:
267 options.swarming = net.fix_url(options.swarming)
268 except ValueError as e:
269 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +0000270
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500271 try:
272 user = auth.ensure_logged_in(options.swarming)
273 except ValueError as e:
274 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -0500275 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +0000276
277
278def main(args):
279 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400280 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000281
282
283if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700284 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +0000285 fix_encoding.fix_encoding()
286 tools.disable_buffering()
287 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +0000288 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000289 sys.exit(main(sys.argv[1:]))