Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2013 The LUCI Authors. All rights reserved. | 2 # Copyright 2013 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
| 5 | 5 |
| 6 """Client tool to trigger tasks or retrieve results from a Swarming server.""" | 6 """Client tool to trigger tasks or retrieve results from a Swarming server.""" |
| 7 | 7 |
| 8 __version__ = '0.9.0' | 8 __version__ = '0.9.1' |
|
iannucci
2017/05/09 21:40:23
maybe 1.0.0 since the CLI is changing in a backwar
M-A Ruel
2017/05/09 22:49:28
I prefer to not jump too fast. :)
| |
| 9 | 9 |
| 10 import collections | 10 import collections |
| 11 import datetime | 11 import datetime |
| 12 import json | 12 import json |
| 13 import logging | 13 import logging |
| 14 import optparse | 14 import optparse |
| 15 import os | 15 import os |
| 16 import subprocess | 16 import subprocess |
| 17 import sys | 17 import sys |
| 18 import textwrap | 18 import textwrap |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 43 | 43 |
| 44 ROOT_DIR = os.path.dirname(os.path.abspath( | 44 ROOT_DIR = os.path.dirname(os.path.abspath( |
| 45 __file__.decode(sys.getfilesystemencoding()))) | 45 __file__.decode(sys.getfilesystemencoding()))) |
| 46 | 46 |
| 47 | 47 |
| 48 class Failure(Exception): | 48 class Failure(Exception): |
| 49 """Generic failure.""" | 49 """Generic failure.""" |
| 50 pass | 50 pass |
| 51 | 51 |
| 52 | 52 |
| 53 ### Isolated file handling. | 53 def default_task_name(options): |
| 54 | 54 """Returns a default task name if not specified.""" |
| 55 | |
| 56 def isolated_handle_options(options, args): | |
| 57 """Handles '--isolated <isolated>' and '-- <args...>' arguments. | |
| 58 | |
| 59 Returns: | |
| 60 tuple(command, inputs_ref). | |
| 61 """ | |
| 62 isolated_cmd_args = [] | |
| 63 if not options.isolated: | |
| 64 if '--' in args: | |
| 65 index = args.index('--') | |
| 66 isolated_cmd_args = args[index+1:] | |
| 67 args = args[:index] | |
| 68 else: | |
| 69 # optparse eats '--' sometimes. | |
| 70 isolated_cmd_args = args[1:] | |
| 71 args = args[:1] | |
| 72 if len(args) != 1: | |
| 73 raise ValueError( | |
| 74 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called ' | |
| 75 'process.') | |
| 76 elif args: | |
| 77 if '--' in args: | |
| 78 index = args.index('--') | |
| 79 isolated_cmd_args = args[index+1:] | |
| 80 if index != 0: | |
| 81 raise ValueError('Unexpected arguments.') | |
| 82 else: | |
| 83 # optparse eats '--' sometimes. | |
| 84 isolated_cmd_args = args | |
| 85 | |
| 86 if not options.task_name: | 55 if not options.task_name: |
| 87 options.task_name = u'%s/%s/%s' % ( | 56 task_name = u'%s/%s' % ( |
| 88 options.user, | 57 options.user, |
| 89 '_'.join( | 58 '_'.join( |
| 90 '%s=%s' % (k, v) | 59 '%s=%s' % (k, v) |
| 91 for k, v in sorted(options.dimensions.iteritems())), | 60 for k, v in sorted(options.dimensions.iteritems()))) |
| 92 options.isolated) | 61 if options.isolated: |
| 93 | 62 task_name += u'/' + options.isolated |
| 94 inputs_ref = FilesRef( | 63 return task_name |
| 95 isolated=options.isolated, | 64 return options.task_name |
| 96 isolatedserver=options.isolate_server, | |
| 97 namespace=options.namespace) | |
| 98 return isolated_cmd_args, inputs_ref | |
| 99 | 65 |
| 100 | 66 |
| 101 ### Triggering. | 67 ### Triggering. |
| 102 | 68 |
| 103 | 69 |
| 104 # See ../appengine/swarming/swarming_rpcs.py. | 70 # See ../appengine/swarming/swarming_rpcs.py. |
| 105 CipdPackage = collections.namedtuple( | 71 CipdPackage = collections.namedtuple( |
| 106 'CipdPackage', | 72 'CipdPackage', |
| 107 [ | 73 [ |
| 108 'package_name', | 74 'package_name', |
| (...skipping 787 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 896 'this task.') | 862 'this task.') |
| 897 group.add_option( | 863 group.add_option( |
| 898 '--hard-timeout', type='int', default=60*60, | 864 '--hard-timeout', type='int', default=60*60, |
| 899 help='Seconds to allow the task to complete.') | 865 help='Seconds to allow the task to complete.') |
| 900 group.add_option( | 866 group.add_option( |
| 901 '--io-timeout', type='int', default=20*60, | 867 '--io-timeout', type='int', default=20*60, |
| 902 help='Seconds to allow the task to be silent.') | 868 help='Seconds to allow the task to be silent.') |
| 903 group.add_option( | 869 group.add_option( |
| 904 '--raw-cmd', action='store_true', default=False, | 870 '--raw-cmd', action='store_true', default=False, |
| 905 help='When set, the command after -- is used as-is without run_isolated. ' | 871 help='When set, the command after -- is used as-is without run_isolated. ' |
| 906 'In this case, no .isolated file is expected.') | 872 'In this case, the .isolated file is expected to not have a command') |
| 907 group.add_option( | 873 group.add_option( |
| 908 '--cipd-package', action='append', default=[], | 874 '--cipd-package', action='append', default=[], |
| 909 help='CIPD packages to install on the Swarming bot. Uses the format: ' | 875 help='CIPD packages to install on the Swarming bot. Uses the format: ' |
| 910 'path:package_name:version') | 876 'path:package_name:version') |
| 911 group.add_option( | 877 group.add_option( |
| 912 '--named-cache', action='append', nargs=2, default=[], | 878 '--named-cache', action='append', nargs=2, default=[], |
| 913 help='"<name> <relpath>" items to keep a persistent bot managed cache') | 879 help='"<name> <relpath>" items to keep a persistent bot managed cache') |
| 914 group.add_option( | 880 group.add_option( |
| 915 '--service-account', | 881 '--service-account', |
| 916 help='Name of a service account to run the task as. Only literal "bot" ' | 882 help='Name of a service account to run the task as. Only literal "bot" ' |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 951 parser.add_option_group(group) | 917 parser.add_option_group(group) |
| 952 | 918 |
| 953 | 919 |
| 954 def process_trigger_options(parser, options, args): | 920 def process_trigger_options(parser, options, args): |
| 955 """Processes trigger options and does preparatory steps. | 921 """Processes trigger options and does preparatory steps. |
| 956 | 922 |
| 957 Generates service account tokens if necessary. | 923 Generates service account tokens if necessary. |
| 958 """ | 924 """ |
| 959 options.dimensions = dict(options.dimensions) | 925 options.dimensions = dict(options.dimensions) |
| 960 options.env = dict(options.env) | 926 options.env = dict(options.env) |
| 927 if args and args[0] == '--': | |
| 928 args = args[1:] | |
| 961 | 929 |
| 962 if not options.dimensions: | 930 if not options.dimensions: |
| 963 parser.error('Please at least specify one --dimension') | 931 parser.error('Please at least specify one --dimension') |
| 932 if not all(len(t.split(':', 1)) == 2 for t in options.tags): | |
| 933 parser.error('--tags must be in the format key:value') | |
| 934 if options.raw_cmd and not args: | |
| 935 parser.error( | |
| 936 'Arguments with --raw-cmd should be passed after -- as command ' | |
| 937 'delimiter.') | |
| 938 if options.isolate_server and not options.namespace: | |
| 939 parser.error( | |
| 940 '--namespace must be a valid value when --isolate-server is used') | |
| 941 if not options.isolated and not options.raw_cmd: | |
| 942 parser.error('Specify at least one of --raw-cmd or --isolated or both') | |
| 943 | |
| 944 # Isolated | |
| 945 # --isolated is required only if --raw-cmd wasn't provided. | |
| 946 # TODO(maruel): --isolate-server may be optional as Swarming may have its own | |
| 947 # preferred server. | |
| 948 isolateserver.process_isolate_server_options( | |
| 949 parser, options, False, not options.raw_cmd) | |
| 950 inputs_ref = None | |
| 951 if options.isolate_server: | |
| 952 inputs_ref = FilesRef( | |
| 953 isolated=options.isolated, | |
| 954 isolatedserver=options.isolate_server, | |
| 955 namespace=options.namespace) | |
| 956 | |
| 957 # Command | |
| 958 command = None | |
| 959 extra_args = None | |
| 964 if options.raw_cmd: | 960 if options.raw_cmd: |
| 965 if not args: | 961 command = args |
| 966 parser.error( | 962 else: |
| 967 'Arguments with --raw-cmd should be passed after -- as command ' | 963 extra_args = args |
| 968 'delimiter.') | |
| 969 if options.isolate_server: | |
| 970 parser.error('Can\'t use both --raw-cmd and --isolate-server.') | |
| 971 | 964 |
| 972 command = args | 965 # CIPD |
| 973 if not options.task_name: | |
| 974 options.task_name = u'%s/%s' % ( | |
| 975 options.user, | |
| 976 '_'.join( | |
| 977 '%s=%s' % (k, v) | |
| 978 for k, v in sorted(options.dimensions.iteritems()))) | |
| 979 inputs_ref = None | |
| 980 else: | |
| 981 isolateserver.process_isolate_server_options(parser, options, False, True) | |
| 982 try: | |
| 983 command, inputs_ref = isolated_handle_options(options, args) | |
| 984 except ValueError as e: | |
| 985 parser.error(str(e)) | |
| 986 | |
| 987 cipd_packages = [] | 966 cipd_packages = [] |
| 988 for p in options.cipd_package: | 967 for p in options.cipd_package: |
| 989 split = p.split(':', 2) | 968 split = p.split(':', 2) |
| 990 if len(split) != 3: | 969 if len(split) != 3: |
| 991 parser.error('CIPD packages must take the form: path:package:version') | 970 parser.error('CIPD packages must take the form: path:package:version') |
| 992 cipd_packages.append(CipdPackage( | 971 cipd_packages.append(CipdPackage( |
| 993 package_name=split[1], | 972 package_name=split[1], |
| 994 path=split[0], | 973 path=split[0], |
| 995 version=split[2])) | 974 version=split[2])) |
| 996 cipd_input = None | 975 cipd_input = None |
| 997 if cipd_packages: | 976 if cipd_packages: |
| 998 cipd_input = CipdInput( | 977 cipd_input = CipdInput( |
| 999 client_package=None, | 978 client_package=None, |
| 1000 packages=cipd_packages, | 979 packages=cipd_packages, |
| 1001 server=None) | 980 server=None) |
| 1002 | 981 |
| 982 # Secrets | |
| 1003 secret_bytes = None | 983 secret_bytes = None |
| 1004 if options.secret_bytes_path: | 984 if options.secret_bytes_path: |
| 1005 with open(options.secret_bytes_path, 'r') as f: | 985 with open(options.secret_bytes_path, 'r') as f: |
| 1006 secret_bytes = f.read().encode('base64') | 986 secret_bytes = f.read().encode('base64') |
| 1007 | 987 |
| 988 # Named caches | |
| 1008 caches = [ | 989 caches = [ |
| 1009 {u'name': unicode(i[0]), u'path': unicode(i[1])} | 990 {u'name': unicode(i[0]), u'path': unicode(i[1])} |
| 1010 for i in options.named_cache | 991 for i in options.named_cache |
| 1011 ] | 992 ] |
| 1012 # If inputs_ref.isolated is used, command is actually extra_args. | 993 |
| 1013 # Otherwise it's an actual command to run. | |
| 1014 isolated_input = inputs_ref and inputs_ref.isolated | |
| 1015 properties = TaskProperties( | 994 properties = TaskProperties( |
| 1016 caches=caches, | 995 caches=caches, |
| 1017 cipd_input=cipd_input, | 996 cipd_input=cipd_input, |
| 1018 command=None if isolated_input else command, | 997 command=command, |
| 1019 dimensions=options.dimensions, | 998 dimensions=options.dimensions, |
| 1020 env=options.env, | 999 env=options.env, |
| 1021 execution_timeout_secs=options.hard_timeout, | 1000 execution_timeout_secs=options.hard_timeout, |
| 1022 extra_args=command if isolated_input else None, | 1001 extra_args=extra_args, |
| 1023 grace_period_secs=30, | 1002 grace_period_secs=30, |
| 1024 idempotent=options.idempotent, | 1003 idempotent=options.idempotent, |
| 1025 inputs_ref=inputs_ref, | 1004 inputs_ref=inputs_ref, |
| 1026 io_timeout_secs=options.io_timeout, | 1005 io_timeout_secs=options.io_timeout, |
| 1027 outputs=options.output, | 1006 outputs=options.output, |
| 1028 secret_bytes=secret_bytes) | 1007 secret_bytes=secret_bytes) |
| 1029 if not all(len(t.split(':', 1)) == 2 for t in options.tags): | |
| 1030 parser.error('--tags must be in the format key:value') | |
| 1031 | 1008 |
| 1032 # Convert a service account email to a signed service account token to pass | 1009 # Convert a service account email to a signed service account token to pass |
| 1033 # to Swarming. | 1010 # to Swarming. |
| 1034 service_account_token = None | 1011 service_account_token = None |
| 1035 if options.service_account in ('bot', 'none'): | 1012 if options.service_account in ('bot', 'none'): |
| 1036 service_account_token = options.service_account | 1013 service_account_token = options.service_account |
| 1037 elif options.service_account: | 1014 elif options.service_account: |
| 1038 # pylint: disable=assignment-from-no-return | 1015 # pylint: disable=assignment-from-no-return |
| 1039 service_account_token = mint_service_account_token(options.service_account) | 1016 service_account_token = mint_service_account_token(options.service_account) |
| 1040 | 1017 |
| 1041 return NewTaskRequest( | 1018 return NewTaskRequest( |
| 1042 expiration_secs=options.expiration, | 1019 expiration_secs=options.expiration, |
| 1043 name=options.task_name, | 1020 name=default_task_name(options), |
| 1044 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''), | 1021 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''), |
| 1045 priority=options.priority, | 1022 priority=options.priority, |
| 1046 properties=properties, | 1023 properties=properties, |
| 1047 service_account_token=service_account_token, | 1024 service_account_token=service_account_token, |
| 1048 tags=options.tags, | 1025 tags=options.tags, |
| 1049 user=options.user) | 1026 user=options.user) |
| 1050 | 1027 |
| 1051 | 1028 |
| 1052 def add_collect_options(parser): | 1029 def add_collect_options(parser): |
| 1053 parser.server_group.add_option( | 1030 parser.server_group.add_option( |
| (...skipping 366 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1420 add_collect_options(parser) | 1397 add_collect_options(parser) |
| 1421 add_sharding_options(parser) | 1398 add_sharding_options(parser) |
| 1422 options, args = parser.parse_args(args) | 1399 options, args = parser.parse_args(args) |
| 1423 task_request = process_trigger_options(parser, options, args) | 1400 task_request = process_trigger_options(parser, options, args) |
| 1424 try: | 1401 try: |
| 1425 tasks = trigger_task_shards( | 1402 tasks = trigger_task_shards( |
| 1426 options.swarming, task_request, options.shards) | 1403 options.swarming, task_request, options.shards) |
| 1427 except Failure as e: | 1404 except Failure as e: |
| 1428 on_error.report( | 1405 on_error.report( |
| 1429 'Failed to trigger %s(%s): %s' % | 1406 'Failed to trigger %s(%s): %s' % |
| 1430 (options.task_name, args[0], e.args[0])) | 1407 (task_request.name, args[0], e.args[0])) |
| 1431 return 1 | 1408 return 1 |
| 1432 if not tasks: | 1409 if not tasks: |
| 1433 on_error.report('Failed to trigger the task.') | 1410 on_error.report('Failed to trigger the task.') |
| 1434 return 1 | 1411 return 1 |
| 1435 print('Triggered task: %s' % options.task_name) | 1412 print('Triggered task: %s' % task_request.name) |
| 1436 task_ids = [ | 1413 task_ids = [ |
| 1437 t['task_id'] | 1414 t['task_id'] |
| 1438 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index']) | 1415 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index']) |
| 1439 ] | 1416 ] |
| 1440 if options.timeout is None: | 1417 if options.timeout is None: |
| 1441 options.timeout = ( | 1418 options.timeout = ( |
| 1442 task_request.properties.execution_timeout_secs + | 1419 task_request.properties.execution_timeout_secs + |
| 1443 task_request.expiration_secs + 10.) | 1420 task_request.expiration_secs + 10.) |
| 1444 try: | 1421 try: |
| 1445 return collect( | 1422 return collect( |
| (...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1597 parser.add_option( | 1574 parser.add_option( |
| 1598 '--dump-json', | 1575 '--dump-json', |
| 1599 metavar='FILE', | 1576 metavar='FILE', |
| 1600 help='Dump details about the triggered task(s) to this file as json') | 1577 help='Dump details about the triggered task(s) to this file as json') |
| 1601 options, args = parser.parse_args(args) | 1578 options, args = parser.parse_args(args) |
| 1602 task_request = process_trigger_options(parser, options, args) | 1579 task_request = process_trigger_options(parser, options, args) |
| 1603 try: | 1580 try: |
| 1604 tasks = trigger_task_shards( | 1581 tasks = trigger_task_shards( |
| 1605 options.swarming, task_request, options.shards) | 1582 options.swarming, task_request, options.shards) |
| 1606 if tasks: | 1583 if tasks: |
| 1607 print('Triggered task: %s' % options.task_name) | 1584 print('Triggered task: %s' % task_request.name) |
| 1608 tasks_sorted = sorted( | 1585 tasks_sorted = sorted( |
| 1609 tasks.itervalues(), key=lambda x: x['shard_index']) | 1586 tasks.itervalues(), key=lambda x: x['shard_index']) |
| 1610 if options.dump_json: | 1587 if options.dump_json: |
| 1611 data = { | 1588 data = { |
| 1612 'base_task_name': options.task_name, | 1589 'base_task_name': task_request.name, |
| 1613 'tasks': tasks, | 1590 'tasks': tasks, |
| 1614 'request': task_request_to_raw_request(task_request, True), | 1591 'request': task_request_to_raw_request(task_request, True), |
| 1615 } | 1592 } |
| 1616 tools.write_json(unicode(options.dump_json), data, True) | 1593 tools.write_json(unicode(options.dump_json), data, True) |
| 1617 print('To collect results, use:') | 1594 print('To collect results, use:') |
| 1618 print(' swarming.py collect -S %s --json %s' % | 1595 print(' swarming.py collect -S %s --json %s' % |
| 1619 (options.swarming, options.dump_json)) | 1596 (options.swarming, options.dump_json)) |
| 1620 else: | 1597 else: |
| 1621 print('To collect results, use:') | 1598 print('To collect results, use:') |
| 1622 print(' swarming.py collect -S %s %s' % | 1599 print(' swarming.py collect -S %s %s' % |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1674 dispatcher = subcommand.CommandDispatcher(__name__) | 1651 dispatcher = subcommand.CommandDispatcher(__name__) |
| 1675 return dispatcher.execute(OptionParserSwarming(version=__version__), args) | 1652 return dispatcher.execute(OptionParserSwarming(version=__version__), args) |
| 1676 | 1653 |
| 1677 | 1654 |
| 1678 if __name__ == '__main__': | 1655 if __name__ == '__main__': |
| 1679 subprocess42.inhibit_os_error_reporting() | 1656 subprocess42.inhibit_os_error_reporting() |
| 1680 fix_encoding.fix_encoding() | 1657 fix_encoding.fix_encoding() |
| 1681 tools.disable_buffering() | 1658 tools.disable_buffering() |
| 1682 colorama.init() | 1659 colorama.init() |
| 1683 sys.exit(main(sys.argv[1:])) | 1660 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |