| 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 def _default_bot_groups(): | 73 def _default_bot_groups(): |
| 65 return _BotGroups( | 74 return _BotGroups( |
| 66 direct_matches={}, | 75 direct_matches={}, |
| 67 prefix_matches=[], | 76 prefix_matches=[], |
| 68 machine_types={}, | 77 machine_types={}, |
| 69 default_group=BotGroupConfig( | 78 default_group=BotGroupConfig( |
| 70 version='default', | 79 version='default', |
| 71 require_luci_machine_token=False, | 80 require_luci_machine_token=False, |
| 72 require_service_account=None, | 81 require_service_account=None, |
| 73 ip_whitelist=auth.bots_ip_whitelist(), | 82 ip_whitelist=auth.bots_ip_whitelist(), |
| 74 owners=(), | 83 owners=(), |
| 75 dimensions={})) | 84 dimensions={}, |
| 85 bot_config_script='', |
| 86 bot_config_script_content='')) |
| 76 | 87 |
| 77 | 88 |
| 78 def _gen_version(fields): | 89 def _gen_version(fields): |
| 79 """Looks at BotGroupConfig fields and derives a digest that summarizes them. | 90 """Looks at BotGroupConfig fields and derives a digest that summarizes them. |
| 80 | 91 |
| 81 This digest is going to be sent to the bot in /handshake, and bot would | 92 This digest is going to be sent to the bot in /handshake, and bot would |
| 82 include it in its state (and thus send it with each /poll). If server detects | 93 include it in its state (and thus send it with each /poll). If server detects |
| 83 that the bot is using older version of the config, it would ask the bot | 94 that the bot is using older version of the config, it would ask the bot |
| 84 to restart. | 95 to restart. |
| 85 | 96 |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 134 # In validated config 'dim_kv_pair' is always 'key:value', but be cautious. | 145 # In validated config 'dim_kv_pair' is always 'key:value', but be cautious. |
| 135 parts = unicode(dim_kv_pair).split(':', 1) | 146 parts = unicode(dim_kv_pair).split(':', 1) |
| 136 if len(parts) != 2: | 147 if len(parts) != 2: |
| 137 logging.error('Invalid dimension in bots.cfg - "%s"', dim_kv_pair) | 148 logging.error('Invalid dimension in bots.cfg - "%s"', dim_kv_pair) |
| 138 continue | 149 continue |
| 139 k, v = parts[0], parts[1] | 150 k, v = parts[0], parts[1] |
| 140 dimensions.setdefault(k, set()).add(v) | 151 dimensions.setdefault(k, set()).add(v) |
| 141 | 152 |
| 142 auth_cfg = msg.auth or bots_pb2.BotAuth() | 153 auth_cfg = msg.auth or bots_pb2.BotAuth() |
| 143 | 154 |
| 155 content = '' |
| 156 if msg.bot_config_script: |
| 157 rev, content = config.get_self_config( |
| 158 'scripts/' + msg.bot_config_script, |
| 159 store_last_good=True) |
| 160 if not rev or not content: |
| 161 # The entry is invalid. It points to a non existing file. It could be |
| 162 # because of a typo in the file name. An empty file is an invalid file, |
| 163 # log an error to alert the admins. |
| 164 logging.error( |
| 165 'Configuration referenced non existing bot_config file %r\n%s', |
| 166 msg.bot_config_script, msg) |
| 144 return _make_bot_group_config( | 167 return _make_bot_group_config( |
| 145 require_luci_machine_token=auth_cfg.require_luci_machine_token, | 168 require_luci_machine_token=auth_cfg.require_luci_machine_token, |
| 146 require_service_account=auth_cfg.require_service_account, | 169 require_service_account=auth_cfg.require_service_account, |
| 147 ip_whitelist=auth_cfg.ip_whitelist, | 170 ip_whitelist=auth_cfg.ip_whitelist, |
| 148 owners=tuple(msg.owners), | 171 owners=tuple(msg.owners), |
| 149 dimensions={k: sorted(v) for k, v in dimensions.iteritems()}) | 172 dimensions={k: sorted(v) for k, v in dimensions.iteritems()}, |
| 173 bot_config_script=msg.bot_config_script or '', |
| 174 bot_config_script_content=content or '') |
| 150 | 175 |
| 151 | 176 |
| 152 def _expand_bot_id_expr(expr): | 177 def _expand_bot_id_expr(expr): |
| 153 """Expands string with bash-like sets (if they are there). | 178 """Expands string with bash-like sets (if they are there). |
| 154 | 179 |
| 155 E.g. takes "vm{1..3}-m1" and yields "vm1-m1", "vm2-m1", "vm3-m1". Also | 180 E.g. takes "vm{1..3}-m1" and yields "vm1-m1", "vm2-m1", "vm3-m1". Also |
| 156 supports list syntax ({1,2,3}). Either one should be used, but not both, e.g. | 181 supports list syntax ({1,2,3}). Either one should be used, but not both, e.g. |
| 157 following WILL NOT work: {1..3,4,5}. | 182 following WILL NOT work: {1..3,4,5}. |
| 158 | 183 |
| 159 Yields original string if it doesn't have '{...}' section. | 184 Yields original string if it doesn't have '{...}' section. |
| (...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 386 try: | 411 try: |
| 387 auth.Identity(auth.IDENTITY_USER, own) | 412 auth.Identity(auth.IDENTITY_USER, own) |
| 388 except ValueError: | 413 except ValueError: |
| 389 ctx.error('invalid owner email "%s"', own) | 414 ctx.error('invalid owner email "%s"', own) |
| 390 | 415 |
| 391 # Validate 'dimensions'. | 416 # Validate 'dimensions'. |
| 392 for dim in entry.dimensions: | 417 for dim in entry.dimensions: |
| 393 if not local_config.validate_flat_dimension(dim): | 418 if not local_config.validate_flat_dimension(dim): |
| 394 ctx.error('bad dimension %r', dim) | 419 ctx.error('bad dimension %r', dim) |
| 395 | 420 |
| 421 # Validate 'bot_config_script': the supplemental bot_config.py. |
| 422 if entry.bot_config_script: |
| 423 # Another check in bot_code.py confirms that the script itself is valid |
| 424 # python. |
| 425 if not entry.bot_config_script.endswith('.py'): |
| 426 ctx.error('Invalid bot_config_script name: must end with .py') |
| 427 if os.path.basename(entry.bot_config_script) != entry.bot_config_script: |
| 428 ctx.error( |
| 429 'Invalid bot_config_script name: must not contain path entry') |
| 430 # We can't validate that the file exists here. It'll fail in |
| 431 # _bot_group_proto_to_tuple() which is called by _fetch_bot_groups() and |
| 432 # cached for 60 seconds. |
| 433 |
| 396 # Now verify bot_id_prefix is never a prefix of other prefix. It causes | 434 # Now verify bot_id_prefix is never a prefix of other prefix. It causes |
| 397 # ambiguities. | 435 # ambiguities. |
| 398 for smaller, s_idx in bot_id_prefixes.iteritems(): | 436 for smaller, s_idx in bot_id_prefixes.iteritems(): |
| 399 for larger, l_idx in bot_id_prefixes.iteritems(): | 437 for larger, l_idx in bot_id_prefixes.iteritems(): |
| 400 if smaller == larger: | 438 if smaller == larger: |
| 401 continue # we've already checked prefixes have no duplicated | 439 continue # we've already checked prefixes have no duplicated |
| 402 if larger.startswith(smaller): | 440 if larger.startswith(smaller): |
| 403 ctx.error( | 441 ctx.error( |
| 404 'bot_id_prefix "%s", defined in group #%d, is subprefix of "%s", ' | 442 'bot_id_prefix "%s", defined in group #%d, is subprefix of "%s", ' |
| 405 'defined in group #%d; it makes group assigned for bots with ' | 443 'defined in group #%d; it makes group assigned for bots with ' |
| 406 'prefix "%s" ambigious', smaller, s_idx, larger, l_idx, larger) | 444 'prefix "%s" ambigious', smaller, s_idx, larger, l_idx, larger) |
| OLD | NEW |