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 |