Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2014 The LUCI Authors. All rights reserved. | 1 # Copyright 2014 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 """Swarming bot code. Includes bootstrap and swarming_bot.zip. | 5 """Swarming bot code. Includes bootstrap and swarming_bot.zip. |
| 6 | 6 |
| 7 It includes everything that is AppEngine specific. The non-GAE code is in | 7 It includes everything that is AppEngine specific. The non-GAE code is in |
| 8 bot_archive.py. | 8 bot_archive.py. |
| 9 """ | 9 """ |
| 10 | 10 |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 21 from components import auth | 21 from components import auth |
| 22 from components import config | 22 from components import config |
| 23 from components import datastore_utils | 23 from components import datastore_utils |
| 24 from components import utils | 24 from components import utils |
| 25 from server import bot_archive | 25 from server import bot_archive |
| 26 from server import config as local_config | 26 from server import config as local_config |
| 27 | 27 |
| 28 | 28 |
| 29 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 29 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 30 | 30 |
| 31 MAX_MEMCACHED_SIZE_BYTES = 1000000 | |
|
Vadim Sh.
2017/06/23 18:26:50
how many parts do we get?
aludwin
2017/06/26 17:12:56
Two currently - though in the unit test, we get 11
| |
| 31 | 32 |
| 32 ### Models. | 33 ### Models. |
| 33 | 34 |
| 34 | 35 |
| 35 File = collections.namedtuple('File', ('content', 'who', 'when', 'version')) | 36 File = collections.namedtuple('File', ('content', 'who', 'when', 'version')) |
| 36 | 37 |
| 37 | 38 |
| 38 class VersionedFile(ndb.Model): | 39 class VersionedFile(ndb.Model): |
| 39 """Versionned entity. | 40 """Versionned entity. |
| 40 | 41 |
| (...skipping 174 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 215 return version, additionals | 216 return version, additionals |
| 216 | 217 |
| 217 | 218 |
| 218 def get_swarming_bot_zip(host): | 219 def get_swarming_bot_zip(host): |
| 219 """Returns a zipped file of all the files a bot needs to run. | 220 """Returns a zipped file of all the files a bot needs to run. |
| 220 | 221 |
| 221 Returns: | 222 Returns: |
| 222 A string representing the zipped file's contents. | 223 A string representing the zipped file's contents. |
| 223 """ | 224 """ |
| 224 version, additionals = get_bot_version(host) | 225 version, additionals = get_bot_version(host) |
| 225 content = memcache.get('code-' + version, namespace='bot_code') | 226 content = get_cached_swarming_bot_zip(version) |
| 226 if content: | 227 if content: |
| 227 logging.debug('memcached bot code %s; %d bytes', version, len(content)) | 228 logging.debug('memcached bot code %s; %d bytes', version, len(content)) |
| 228 return content | 229 return content |
| 229 | 230 |
| 230 # Get the start bot script from the database, if present. Pass an empty | 231 # Get the start bot script from the database, if present. Pass an empty |
| 231 # file if the files isn't present. | 232 # file if the files isn't present. |
| 232 additionals = additionals or { | 233 additionals = additionals or { |
| 233 'config/bot_config.py': get_bot_config().content, | 234 'config/bot_config.py': get_bot_config().content, |
| 234 } | 235 } |
| 235 bot_dir = os.path.join(ROOT_DIR, 'swarming_bot') | 236 bot_dir = os.path.join(ROOT_DIR, 'swarming_bot') |
| 236 content, version = bot_archive.get_swarming_bot_zip( | 237 content, version = bot_archive.get_swarming_bot_zip( |
| 237 bot_dir, host, utils.get_app_version(), additionals, | 238 bot_dir, host, utils.get_app_version(), additionals, |
| 238 local_config.settings().enable_ts_monitoring) | 239 local_config.settings().enable_ts_monitoring) |
| 239 # This is immutable so not no need to set expiration time. | |
| 240 memcache.set('code-' + version, content, namespace='bot_code') | |
| 241 logging.info('generated bot code %s; %d bytes', version, len(content)) | 240 logging.info('generated bot code %s; %d bytes', version, len(content)) |
| 241 cache_swarming_bot_zip(version, content) | |
| 242 return content | 242 return content |
| 243 | 243 |
|
Vadim Sh.
2017/06/23 18:26:50
nit: \n (two blank lines between top-level code un
aludwin
2017/06/26 17:12:56
Done.
| |
| 244 class MemcacheMissingException(Exception): | |
| 245 pass | |
| 246 | |
|
Vadim Sh.
2017/06/23 18:26:50
nit: \n
aludwin
2017/06/26 17:12:56
Done.
| |
| 247 def get_cached_swarming_bot_zip(version): | |
| 248 """Returns the bot contents if its been cached, or None if missing.""" | |
| 249 try: | |
| 250 num_parts = get_cached_bot_entry(version, 'parts') | |
|
Vadim Sh.
2017/06/23 18:26:50
'parts' and 'signature' should be part of one memc
aludwin
2017/06/26 17:12:56
Done.
aludwin
2017/06/26 17:12:56
Done.
| |
| 251 true_sig = get_cached_bot_entry(version, 'signature') | |
| 252 content = '' | |
| 253 for p in range(0, num_parts): | |
| 254 content += get_cached_bot_entry(version, 'content', p) | |
|
Vadim Sh.
2017/06/23 18:26:50
please use asynchronous memcache API to fetch part
aludwin
2017/06/26 17:12:56
Done.
| |
| 255 h = hashlib.sha256() | |
| 256 h.update(content) | |
| 257 if h.hexdigest() != true_sig: | |
| 258 logging.error('bot %s had signature %s instead of expected %s', version, | |
| 259 h.hexdigest(), true_sig) | |
| 260 return None | |
| 261 return content | |
| 262 except MemcacheMissingException: | |
| 263 return None | |
| 264 | |
| 265 | |
| 266 def cache_swarming_bot_zip(version, content): | |
| 267 """Caches the bot code to memcache.""" | |
| 268 h = hashlib.sha256() | |
| 269 h.update(content) | |
| 270 p = 0 | |
| 271 while len(content) > 0: | |
|
Vadim Sh.
2017/06/23 18:26:50
same here, it should be done in parallel
aludwin
2017/06/26 17:12:56
Done.
| |
| 272 chunk_size = min(MAX_MEMCACHED_SIZE_BYTES, len(content)) | |
| 273 set_cached_bot_entry(content[0:chunk_size], version, 'content', p) | |
| 274 content=content[chunk_size:] | |
| 275 p += 1 | |
| 276 set_cached_bot_entry(h.hexdigest(), version, 'signature') | |
| 277 set_cached_bot_entry(p, version, 'parts') | |
| 278 logging.info('bot %s with sig %s saved in memcached in %d chunks', | |
| 279 version, h.hexdigest(), p) | |
| 280 | |
| 281 | |
| 282 def get_cached_bot_entry(version, desc, part=None): | |
| 283 """Gets a bot entry from memcached. | |
| 284 | |
| 285 Raise MemcacheMissingException if the entry is None. | |
| 286 """ | |
| 287 res = memcache.get(cached_bot_key(version, desc, part), namespace='bot_code') | |
| 288 if res is None: | |
| 289 raise MemcacheMissingException() | |
| 290 return res | |
| 291 | |
| 292 | |
| 293 def set_cached_bot_entry(content, version, desc, part=None): | |
| 294 """Sets a bot entry in memcached.""" | |
| 295 memcache.set(cached_bot_key(version, desc, part), content, | |
| 296 namespace='bot_code') | |
| 297 | |
| 298 | |
| 299 def cached_bot_key(version, desc, part): | |
| 300 """Returns a memcache key for bot entries.""" | |
| 301 key = 'code-%s-%s' % (version, desc) | |
| 302 if part is not None: | |
| 303 key = '%s-%d' % (key, part) | |
| 304 return key | |
| 244 | 305 |
|
Vadim Sh.
2017/06/23 18:26:50
nit: \n
aludwin
2017/06/26 17:12:56
Missed this one but checked in locally for next up
| |
| 245 ### Bootstrap token. | 306 ### Bootstrap token. |
| 246 | 307 |
| 247 | 308 |
| 248 class BootstrapToken(auth.TokenKind): | 309 class BootstrapToken(auth.TokenKind): |
| 249 expiration_sec = 3600 | 310 expiration_sec = 3600 |
| 250 secret_key = auth.SecretKey('bot_bootstrap_token') | 311 secret_key = auth.SecretKey('bot_bootstrap_token') |
| 251 version = 1 | 312 version = 1 |
| 252 | 313 |
| 253 | 314 |
| 254 def generate_bootstrap_token(): | 315 def generate_bootstrap_token(): |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 306 | 367 |
| 307 ## Config validators | 368 ## Config validators |
| 308 | 369 |
| 309 | 370 |
| 310 @config.validation.self_rule('regex:scripts/.+\\.py') | 371 @config.validation.self_rule('regex:scripts/.+\\.py') |
| 311 def _validate_scripts(content, ctx): | 372 def _validate_scripts(content, ctx): |
| 312 try: | 373 try: |
| 313 ast.parse(content) | 374 ast.parse(content) |
| 314 except (SyntaxError, TypeError) as e: | 375 except (SyntaxError, TypeError) as e: |
| 315 ctx.error('invalid %s: %s' % (ctx.path, e)) | 376 ctx.error('invalid %s: %s' % (ctx.path, e)) |
| OLD | NEW |