| OLD | NEW |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 """Functions to fetch and interpret bots.cfg file with list of bot groups.""" | 5 """Functions to fetch and interpret bots.cfg file with list of bot groups.""" |
| 6 | 6 |
| 7 import collections | 7 import collections |
| 8 import hashlib | 8 import hashlib |
| 9 import logging | 9 import logging |
| 10 import os |
| 10 import re | 11 import re |
| 11 | 12 |
| 12 from components import auth | 13 from components import auth |
| 13 from components import config | 14 from components import config |
| 14 from components import utils | 15 from components import utils |
| 15 from components.config import validation | 16 from components.config import validation |
| 16 | 17 |
| 17 from proto import bots_pb2 | 18 from proto import bots_pb2 |
| 18 from server import config as local_config | 19 from server import config as local_config |
| 19 | 20 |
| (...skipping 20 matching lines...) Expand all Loading... |
| 40 'ip_whitelist', | 41 'ip_whitelist', |
| 41 | 42 |
| 42 # Tuple with emails of bot owners. | 43 # Tuple with emails of bot owners. |
| 43 'owners', | 44 'owners', |
| 44 | 45 |
| 45 # Dict {key => list of values}. Always contains all the keys specified by | 46 # Dict {key => list of values}. Always contains all the keys specified by |
| 46 # 'trusted_dimensions' set in BotsCfg. If BotGroup doesn't define some | 47 # 'trusted_dimensions' set in BotsCfg. If BotGroup doesn't define some |
| 47 # dimension from that set, the list of value for it will be empty. Key and | 48 # dimension from that set, the list of value for it will be empty. Key and |
| 48 # values are unicode strings. | 49 # values are unicode strings. |
| 49 'dimensions', | 50 'dimensions', |
| 51 |
| 52 # Name of the supplemental bot_config.py to inject to the bot during |
| 53 # handshake. |
| 54 'bot_config_script', |
| 55 |
| 56 # Content of the supplemental bot_config.py to inject to the bot during |
| 57 # handshake. |
| 58 'bot_config_script_content', |
| 50 ]) | 59 ]) |
| 51 | 60 |
| 52 | 61 |
| 53 # Post-processed and validated read-only form of bots.cfg config. Its structure | 62 # Post-processed and validated read-only form of bots.cfg config. Its structure |
| 54 # is optimized for fast lookup of BotGroupConfig by bot_id. | 63 # is optimized for fast lookup of BotGroupConfig by bot_id. |
| 55 _BotGroups = collections.namedtuple('_BotGroups', [ | 64 _BotGroups = collections.namedtuple('_BotGroups', [ |
| 56 'direct_matches', # dict bot_id => BotGroupConfig | 65 'direct_matches', # dict bot_id => BotGroupConfig |
| 57 'prefix_matches', # list of pairs (bot_id_prefix, BotGroupConfig) | 66 'prefix_matches', # list of pairs (bot_id_prefix, BotGroupConfig) |
| 58 'machine_types', # dict machine_type.name => BotGroupConfig | 67 'machine_types', # dict machine_type.name => BotGroupConfig |
| 59 'default_group', # fallback BotGroupConfig or None if not defined | 68 'default_group', # fallback BotGroupConfig or None if not defined |
| 60 ]) | 69 ]) |
| 61 | 70 |
| 62 | 71 |
| 63 # Default config to use on unconfigured server. | 72 # Default config to use on unconfigured server. |
| 64 _DEFAULT_BOT_GROUPS = _BotGroups( | 73 _DEFAULT_BOT_GROUPS = _BotGroups( |
| 65 direct_matches={}, | 74 direct_matches={}, |
| 66 prefix_matches=[], | 75 prefix_matches=[], |
| 67 machine_types={}, | 76 machine_types={}, |
| 68 default_group=BotGroupConfig( | 77 default_group=BotGroupConfig( |
| 69 version='default', | 78 version='default', |
| 70 require_luci_machine_token=False, | 79 require_luci_machine_token=False, |
| 71 require_service_account=None, | 80 require_service_account=None, |
| 72 ip_whitelist=auth.BOTS_IP_WHITELIST, | 81 ip_whitelist=auth.BOTS_IP_WHITELIST, |
| 73 owners=(), | 82 owners=(), |
| 74 dimensions={})) | 83 dimensions={}, |
| 84 bot_config_script='', |
| 85 bot_config_script_content='')) |
| 75 | 86 |
| 76 | 87 |
| 77 def _gen_version(fields): | 88 def _gen_version(fields): |
| 78 """Looks at BotGroupConfig fields and derives a digest that summarizes them. | 89 """Looks at BotGroupConfig fields and derives a digest that summarizes them. |
| 79 | 90 |
| 80 This digest is going to be sent to the bot in /handshake, and bot would | 91 This digest is going to be sent to the bot in /handshake, and bot would |
| 81 include it in its state (and thus send it with each /poll). If server detects | 92 include it in its state (and thus send it with each /poll). If server detects |
| 82 that the bot is using older version of the config, it would ask the bot | 93 that the bot is using older version of the config, it would ask the bot |
| 83 to restart. | 94 to restart. |
| 84 | 95 |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 133 # In validated config 'dim_kv_pair' is always 'key:value', but be cautious. | 144 # In validated config 'dim_kv_pair' is always 'key:value', but be cautious. |
| 134 parts = unicode(dim_kv_pair).split(':', 1) | 145 parts = unicode(dim_kv_pair).split(':', 1) |
| 135 if len(parts) != 2: | 146 if len(parts) != 2: |
| 136 logging.error('Invalid dimension in bots.cfg - "%s"', dim_kv_pair) | 147 logging.error('Invalid dimension in bots.cfg - "%s"', dim_kv_pair) |
| 137 continue | 148 continue |
| 138 k, v = parts[0], parts[1] | 149 k, v = parts[0], parts[1] |
| 139 dimensions.setdefault(k, set()).add(v) | 150 dimensions.setdefault(k, set()).add(v) |
| 140 | 151 |
| 141 auth_cfg = msg.auth or bots_pb2.BotAuth() | 152 auth_cfg = msg.auth or bots_pb2.BotAuth() |
| 142 | 153 |
| 154 content = '' |
| 155 if msg.bot_config_script: |
| 156 rev, content = config.get_self_config( |
| 157 'scripts/' + msg.bot_config_script, |
| 158 store_last_good=True) |
| 159 if not rev or not content: |
| 160 # The entry is invalid. It points to a non existing file. It could be |
| 161 # because of a typo in the file name. An empty file is an invalid file, |
| 162 # log an error to alert the admins. |
| 163 logging.error( |
| 164 'Configuration referenced non existing bot_config file %r\n%s', |
| 165 msg.bot_config_script, msg) |
| 143 return _make_bot_group_config( | 166 return _make_bot_group_config( |
| 144 require_luci_machine_token=auth_cfg.require_luci_machine_token, | 167 require_luci_machine_token=auth_cfg.require_luci_machine_token, |
| 145 require_service_account=auth_cfg.require_service_account, | 168 require_service_account=auth_cfg.require_service_account, |
| 146 ip_whitelist=auth_cfg.ip_whitelist, | 169 ip_whitelist=auth_cfg.ip_whitelist, |
| 147 owners=tuple(msg.owners), | 170 owners=tuple(msg.owners), |
| 148 dimensions={k: sorted(v) for k, v in dimensions.iteritems()}) | 171 dimensions={k: sorted(v) for k, v in dimensions.iteritems()}, |
| 172 bot_config_script=msg.bot_config_script or '', |
| 173 bot_config_script_content=content or '') |
| 149 | 174 |
| 150 | 175 |
| 151 def _expand_bot_id_expr(expr): | 176 def _expand_bot_id_expr(expr): |
| 152 """Expands string with bash-like sets (if they are there). | 177 """Expands string with bash-like sets (if they are there). |
| 153 | 178 |
| 154 E.g. takes "vm{1..3}-m1" and yields "vm1-m1", "vm2-m1", "vm3-m1". Also | 179 E.g. takes "vm{1..3}-m1" and yields "vm1-m1", "vm2-m1", "vm3-m1". Also |
| 155 supports list syntax ({1,2,3}). Either one should be used, but not both, e.g. | 180 supports list syntax ({1,2,3}). Either one should be used, but not both, e.g. |
| 156 following WILL NOT work: {1..3,4,5}. | 181 following WILL NOT work: {1..3,4,5}. |
| 157 | 182 |
| 158 Yields original string if it doesn't have '{...}' section. | 183 Yields original string if it doesn't have '{...}' section. |
| (...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 385 try: | 410 try: |
| 386 auth.Identity(auth.IDENTITY_USER, own) | 411 auth.Identity(auth.IDENTITY_USER, own) |
| 387 except ValueError: | 412 except ValueError: |
| 388 ctx.error('invalid owner email "%s"', own) | 413 ctx.error('invalid owner email "%s"', own) |
| 389 | 414 |
| 390 # Validate 'dimensions'. | 415 # Validate 'dimensions'. |
| 391 for dim in entry.dimensions: | 416 for dim in entry.dimensions: |
| 392 if not local_config.validate_flat_dimension(dim): | 417 if not local_config.validate_flat_dimension(dim): |
| 393 ctx.error('bad dimension %r', dim) | 418 ctx.error('bad dimension %r', dim) |
| 394 | 419 |
| 420 # Validate 'bot_config_script': the supplemental bot_config.py. |
| 421 if entry.bot_config_script: |
| 422 # Another check in bot_code.py confirms that the script itself is valid |
| 423 # python. |
| 424 if not entry.bot_config_script.endswith('.py'): |
| 425 ctx.error('Invalid bot_config_script name: must end with .py') |
| 426 if os.path.basename(entry.bot_config_script) != entry.bot_config_script: |
| 427 ctx.error( |
| 428 'Invalid bot_config_script name: must not contain path entry') |
| 429 # We can't validate that the file exists here. It'll fail in |
| 430 # _bot_group_proto_to_tuple() which is called by _fetch_bot_groups() and |
| 431 # cached for 60 seconds. |
| 432 |
| 395 # Now verify bot_id_prefix is never a prefix of other prefix. It causes | 433 # Now verify bot_id_prefix is never a prefix of other prefix. It causes |
| 396 # ambiguities. | 434 # ambiguities. |
| 397 for smaller, s_idx in bot_id_prefixes.iteritems(): | 435 for smaller, s_idx in bot_id_prefixes.iteritems(): |
| 398 for larger, l_idx in bot_id_prefixes.iteritems(): | 436 for larger, l_idx in bot_id_prefixes.iteritems(): |
| 399 if smaller == larger: | 437 if smaller == larger: |
| 400 continue # we've already checked prefixes have no duplicated | 438 continue # we've already checked prefixes have no duplicated |
| 401 if larger.startswith(smaller): | 439 if larger.startswith(smaller): |
| 402 ctx.error( | 440 ctx.error( |
| 403 'bot_id_prefix "%s", defined in group #%d, is subprefix of "%s", ' | 441 'bot_id_prefix "%s", defined in group #%d, is subprefix of "%s", ' |
| 404 'defined in group #%d; it makes group assigned for bots with ' | 442 'defined in group #%d; it makes group assigned for bots with ' |
| 405 'prefix "%s" ambigious', smaller, s_idx, larger, l_idx, larger) | 443 'prefix "%s" ambigious', smaller, s_idx, larger, l_idx, larger) |
| OLD | NEW |