Junji Watanabe | 7a677e9 | 2022-01-13 06:07:31 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
maruel | ea586f3 | 2016-04-05 11:11:33 -0700 | [diff] [blame] | 2 | # Copyright 2013 The LUCI Authors. All rights reserved. |
maruel | f1f5e2a | 2016-05-25 17:10:39 -0700 | [diff] [blame] | 3 | # 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.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 5 | """Client tool to trigger tasks or retrieve results from a Swarming server.""" |
| 6 | |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 7 | __version__ = '1.0' |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 8 | |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 9 | import json |
| 10 | import logging |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 11 | import optparse |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 12 | import os |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 13 | import sys |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 14 | import textwrap |
Junji Watanabe | 7a677e9 | 2022-01-13 06:07:31 +0000 | [diff] [blame] | 15 | import urllib.parse |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 16 | |
vadimsh@chromium.org | 6b70621 | 2013-08-28 15:03:46 +0000 | [diff] [blame] | 17 | from utils import tools |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 18 | tools.force_local_third_party() |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 19 | |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 20 | # third_party/ |
| 21 | import colorama |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 22 | from depot_tools import fix_encoding |
| 23 | from depot_tools import subcommand |
| 24 | |
| 25 | # pylint: disable=ungrouped-imports |
Vadim Shtayura | e34e13a | 2014-02-02 11:23:26 -0800 | [diff] [blame] | 26 | import auth |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 27 | from utils import fs |
| 28 | from utils import logging_utils |
| 29 | from utils import net |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 30 | from utils import subprocess42 |
Marc-Antoine Ruel | efdc528 | 2014-12-12 19:31:00 -0500 | [diff] [blame] | 31 | |
| 32 | |
| 33 | class Failure(Exception): |
| 34 | """Generic failure.""" |
Marc-Antoine Ruel | efdc528 | 2014-12-12 19:31:00 -0500 | [diff] [blame] | 35 | |
| 36 | |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 37 | ### API management. |
| 38 | |
| 39 | |
| 40 | class APIError(Exception): |
| 41 | pass |
| 42 | |
| 43 | |
| 44 | def 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 | """ |
maruel | 380e326 | 2016-08-31 16:10:06 -0700 | [diff] [blame] | 50 | # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud |
| 51 | # Endpoints version is turned down. |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 52 | 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 | |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 69 | def 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 Ruel | ad8cabe | 2019-10-10 23:24:26 +0000 | [diff] [blame] | 94 | url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor)) |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 95 | 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 Ruel | efdc528 | 2014-12-12 19:31:00 -0500 | [diff] [blame] | 110 | ### Commands. |
| 111 | |
| 112 | |
Marc-Antoine Ruel | 833f5eb | 2018-04-25 16:49:40 -0400 | [diff] [blame] | 113 | @subcommand.usage('[method name]') |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 114 | def CMDquery(parser, args): |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 115 | """Returns raw JSON information via an URL endpoint. Use 'query-list' to |
| 116 | gather the list of API methods from the server. |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 117 | |
| 118 | Examples: |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 119 | 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 | |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 123 | Listing all bots: |
maruel | 84e77aa | 2015-10-21 06:37:24 -0700 | [diff] [blame] | 124 | swarming.py query -S server-url.com bots/list |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 125 | |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 126 | Listing last 10 tasks on a specific bot named 'bot1': |
| 127 | swarming.py query -S server-url.com --limit 10 bot/bot1/tasks |
maruel | 84e77aa | 2015-10-21 06:37:24 -0700 | [diff] [blame] | 128 | |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 129 | Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that |
maruel | 84e77aa | 2015-10-21 06:37:24 -0700 | [diff] [blame] | 130 | quoting is important!: |
| 131 | swarming.py query -S server-url.com --limit 10 \\ |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 132 | 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome' |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 133 | """ |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 134 | parser.add_option( |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 135 | '-L', |
| 136 | '--limit', |
| 137 | type='int', |
| 138 | default=200, |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 139 | help='Limit to enforce on limitless items (like number of tasks); ' |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 140 | 'default=%default') |
Paweł Hajdan, Jr | 53ef013 | 2015-03-20 17:49:18 +0100 | [diff] [blame] | 141 | parser.add_option( |
| 142 | '--json', help='Path to JSON output file (otherwise prints to stdout)') |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 143 | parser.add_option( |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 144 | '--progress', |
| 145 | action='store_true', |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 146 | help='Prints a dot at each request to show progress') |
| 147 | options, args = parser.parse_args(args) |
maruel | d8aba22 | 2015-09-03 12:21:19 -0700 | [diff] [blame] | 148 | if len(args) != 1: |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 149 | parser.error( |
| 150 | 'Must specify only method name and optionally query args properly ' |
| 151 | 'escaped.') |
smut | 281c390 | 2018-05-30 17:50:05 -0700 | [diff] [blame] | 152 | base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0] |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 153 | try: |
| 154 | data, yielder = get_yielder(base_url, options.limit) |
| 155 | for items in yielder(): |
| 156 | if items: |
| 157 | data['items'].extend(items) |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 158 | if options.progress: |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 159 | sys.stderr.write('.') |
| 160 | sys.stderr.flush() |
| 161 | except Failure as e: |
| 162 | sys.stderr.write('\n%s\n' % e) |
| 163 | return 1 |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 164 | if options.progress: |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 165 | sys.stderr.write('\n') |
| 166 | sys.stderr.flush() |
Paweł Hajdan, Jr | 53ef013 | 2015-03-20 17:49:18 +0100 | [diff] [blame] | 167 | if options.json: |
Junji Watanabe | 7a677e9 | 2022-01-13 06:07:31 +0000 | [diff] [blame] | 168 | options.json = os.path.abspath(options.json) |
maruel | 1ceb387 | 2015-10-14 06:10:44 -0700 | [diff] [blame] | 169 | tools.write_json(options.json, data, True) |
Paweł Hajdan, Jr | 53ef013 | 2015-03-20 17:49:18 +0100 | [diff] [blame] | 170 | else: |
Marc-Antoine Ruel | cda90ee | 2015-03-23 15:13:20 -0400 | [diff] [blame] | 171 | try: |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 172 | tools.write_json(sys.stdout, data, False) |
Marc-Antoine Ruel | cda90ee | 2015-03-23 15:13:20 -0400 | [diff] [blame] | 173 | sys.stdout.write('\n') |
| 174 | except IOError: |
| 175 | pass |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 176 | return 0 |
| 177 | |
| 178 | |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 179 | def 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 Watanabe | 7a677e9 | 2022-01-13 06:07:31 +0000 | [diff] [blame] | 194 | options.json = os.path.abspath(options.json) |
maruel | 1ceb387 | 2015-10-14 06:10:44 -0700 | [diff] [blame] | 195 | with fs.open(options.json, 'wb') as f: |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 196 | json.dump(apis, f) |
| 197 | else: |
| 198 | help_url = ( |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 199 | 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' % |
| 200 | options.swarming) |
Marc-Antoine Ruel | 04903a3 | 2019-10-09 21:09:25 +0000 | [diff] [blame] | 201 | for i, (api_id, api) in enumerate(sorted(apis.items())): |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 202 | if i: |
| 203 | print('') |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 204 | print(api_id) |
| 205 | print(' ' + api['description'].strip()) |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 206 | if 'resources' in api: |
| 207 | # Old. |
Marc-Antoine Ruel | 793bff3 | 2019-04-18 17:50:48 +0000 | [diff] [blame] | 208 | # TODO(maruel): Remove. |
| 209 | # pylint: disable=too-many-nested-blocks |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 210 | for j, (resource_name, |
| 211 | resource) in enumerate(sorted(api['resources'].items())): |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 212 | if j: |
| 213 | print('') |
Marc-Antoine Ruel | 04903a3 | 2019-10-09 21:09:25 +0000 | [diff] [blame] | 214 | for method_name, method in sorted(resource['methods'].items()): |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 215 | # Only list the GET ones. |
| 216 | if method['httpMethod'] != 'GET': |
| 217 | continue |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 218 | 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 Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 221 | print(' %s%s%s' % (help_url, api['servicePath'], method['id'])) |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 222 | else: |
| 223 | # New. |
Marc-Antoine Ruel | 04903a3 | 2019-10-09 21:09:25 +0000 | [diff] [blame] | 224 | for method_name, method in sorted(api['methods'].items()): |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 225 | # Only list the GET ones. |
| 226 | if method['httpMethod'] != 'GET': |
| 227 | continue |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 228 | print('- %s: %s' % (method['id'], method['path'])) |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 229 | print('\n'.join( |
| 230 | ' ' + l for l in textwrap.wrap(method['description'], 78))) |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 231 | print(' %s%s%s' % (help_url, api['servicePath'], method['id'])) |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 232 | return 0 |
| 233 | |
| 234 | |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 235 | class OptionParserSwarming(logging_utils.OptionParserWithLogging): |
Junji Watanabe | 38b28b0 | 2020-04-23 10:23:30 +0000 | [diff] [blame] | 236 | |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 237 | def __init__(self, **kwargs): |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 238 | logging_utils.OptionParserWithLogging.__init__( |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 239 | self, prog='swarming.py', **kwargs) |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 240 | self.server_group = optparse.OptionGroup(self, 'Server') |
Marc-Antoine Ruel | 5471e3d | 2013-11-11 19:10:32 -0500 | [diff] [blame] | 241 | self.server_group.add_option( |
Junji Watanabe | 38b28b0 | 2020-04-23 10:23:30 +0000 | [diff] [blame] | 242 | '-S', |
| 243 | '--swarming', |
| 244 | metavar='URL', |
| 245 | default=os.environ.get('SWARMING_SERVER', ''), |
maruel@chromium.org | e9403ab | 2013-09-20 18:03:49 +0000 | [diff] [blame] | 246 | help='Swarming server to use') |
Marc-Antoine Ruel | 5471e3d | 2013-11-11 19:10:32 -0500 | [diff] [blame] | 247 | self.add_option_group(self.server_group) |
Vadim Shtayura | e34e13a | 2014-02-02 11:23:26 -0800 | [diff] [blame] | 248 | auth.add_auth_options(self) |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 249 | |
| 250 | def parse_args(self, *args, **kwargs): |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 251 | options, args = logging_utils.OptionParserWithLogging.parse_args( |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 252 | self, *args, **kwargs) |
Marc-Antoine Ruel | 012067b | 2014-12-10 15:45:42 -0500 | [diff] [blame] | 253 | 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.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 264 | if not options.swarming: |
| 265 | self.error('--swarming is required.') |
Marc-Antoine Ruel | 012067b | 2014-12-10 15:45:42 -0500 | [diff] [blame] | 266 | try: |
| 267 | options.swarming = net.fix_url(options.swarming) |
| 268 | except ValueError as e: |
| 269 | self.error('--swarming %s' % e) |
Takuto Ikuta | ae767b3 | 2020-05-11 01:22:19 +0000 | [diff] [blame] | 270 | |
Marc-Antoine Ruel | f7d737d | 2014-12-10 15:36:29 -0500 | [diff] [blame] | 271 | try: |
| 272 | user = auth.ensure_logged_in(options.swarming) |
| 273 | except ValueError as e: |
| 274 | self.error(str(e)) |
Marc-Antoine Ruel | 012067b | 2014-12-10 15:45:42 -0500 | [diff] [blame] | 275 | return user |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 276 | |
| 277 | |
| 278 | def main(args): |
| 279 | dispatcher = subcommand.CommandDispatcher(__name__) |
Marc-Antoine Ruel | cfb6085 | 2014-07-02 15:22:00 -0400 | [diff] [blame] | 280 | return dispatcher.execute(OptionParserSwarming(version=__version__), args) |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 281 | |
| 282 | |
| 283 | if __name__ == '__main__': |
maruel | 8e4e40c | 2016-05-30 06:21:07 -0700 | [diff] [blame] | 284 | subprocess42.inhibit_os_error_reporting() |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 285 | fix_encoding.fix_encoding() |
| 286 | tools.disable_buffering() |
| 287 | colorama.init() |
Takuto Ikuta | 7c843c8 | 2020-04-15 05:42:54 +0000 | [diff] [blame] | 288 | net.set_user_agent('swarming.py/' + __version__) |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 289 | sys.exit(main(sys.argv[1:])) |