Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(856)

Side by Side Diff: appengine/components/tool_support/gae_sdk_utils.py

Issue 2898293002: gae.py: Remove support for MVMs (they are deprecated), simplify code a bit. (Closed)
Patch Set: nit Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | appengine/components/tools/gae.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2013 The LUCI Authors. All rights reserved. 1 # Copyright 2013 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 """Set of functions to work with GAE SDK tools.""" 5 """Set of functions to work with GAE SDK tools."""
6 6
7 import collections 7 import collections
8 import glob 8 import glob
9 import json 9 import json
10 import logging 10 import logging
(...skipping 15 matching lines...) Expand all
26 26
27 # Name of a directory with Go GAE SDK. 27 # Name of a directory with Go GAE SDK.
28 GO_GAE_SDK = 'go_appengine' 28 GO_GAE_SDK = 'go_appengine'
29 29
30 # Value of 'runtime: ...' in app.yaml -> SDK to use. 30 # Value of 'runtime: ...' in app.yaml -> SDK to use.
31 RUNTIME_TO_SDK = { 31 RUNTIME_TO_SDK = {
32 'go': GO_GAE_SDK, 32 'go': GO_GAE_SDK,
33 'python27': PYTHON_GAE_SDK, 33 'python27': PYTHON_GAE_SDK,
34 } 34 }
35 35
36 # Exe name => instructions how to install it.
37 KNOWN_TOOLS = {
38 'gcloud':
39 'Download and install the Google Cloud SDK from '
40 'https://cloud.google.com/sdk/',
41 'aedeploy':
42 'Install with: go install '
43 'google.golang.org/appengine/cmd/aedeploy',
44 }
45
46
47 # Path to a current SDK, set in setup_gae_sdk, accessible by gae_sdk_path. 36 # Path to a current SDK, set in setup_gae_sdk, accessible by gae_sdk_path.
48 _GAE_SDK_PATH = None 37 _GAE_SDK_PATH = None
49 38
50 39
51 class BadEnvironmentConfig(Exception): 40 class Error(Exception):
41 """Base class for a fatal error."""
42
43
44 class BadEnvironmentError(Error):
52 """Raised when required tools or environment are missing.""" 45 """Raised when required tools or environment are missing."""
53 46
54 47
48 class UnsupportedModuleError(Error):
49 """Raised when trying to deploy MVM or Flex module."""
50
51
52 class LoginRequiredError(Error):
53 """Raised by Application methods if use has to go through login flow."""
54
55
56 def find_gcloud():
57 """Searches for 'gcloud' binary in PATH and returns absolute path to it.
58
59 Raises BadEnvironmentError error if it's not there.
60 """
61 for path in os.environ['PATH'].split(os.pathsep):
62 exe_file = os.path.join(path, 'gcloud')
63 if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
64 return exe_file
65 raise BadEnvironmentError(
66 'Can\'t find "gcloud" in PATH. Install the Google Cloud SDK from '
67 'https://cloud.google.com/sdk/')
68
69
55 def find_gae_sdk(sdk_name=PYTHON_GAE_SDK, search_dir=TOOLS_DIR): 70 def find_gae_sdk(sdk_name=PYTHON_GAE_SDK, search_dir=TOOLS_DIR):
56 """Returns the path to GAE SDK if found, else None.""" 71 """Returns the path to GAE SDK if found, else None."""
57 # First search up the directories up to root. 72 # First search up the directories up to root.
58 while True: 73 while True:
59 attempt = os.path.join(search_dir, sdk_name) 74 attempt = os.path.join(search_dir, sdk_name)
60 if os.path.isfile(os.path.join(attempt, 'dev_appserver.py')): 75 if os.path.isfile(os.path.join(attempt, 'dev_appserver.py')):
61 return attempt 76 return attempt
62 prev_dir = search_dir 77 prev_dir = search_dir
63 search_dir = os.path.dirname(search_dir) 78 search_dir = os.path.dirname(search_dir)
64 if search_dir == prev_dir: 79 if search_dir == prev_dir:
(...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after
183 yaml = yaml_module 198 yaml = yaml_module
184 199
185 200
186 def gae_sdk_path(): 201 def gae_sdk_path():
187 """Checks that 'setup_gae_sdk' was called and returns a path to GAE SDK.""" 202 """Checks that 'setup_gae_sdk' was called and returns a path to GAE SDK."""
188 if not _GAE_SDK_PATH: 203 if not _GAE_SDK_PATH:
189 raise ValueError('setup_gae_sdk wasn\'t called') 204 raise ValueError('setup_gae_sdk wasn\'t called')
190 return _GAE_SDK_PATH 205 return _GAE_SDK_PATH
191 206
192 207
193 class LoginRequiredError(Exception):
194 """Raised by Application methods if use has to go through login flow."""
195
196
197 ModuleFile = collections.namedtuple('ModuleFile', ['path', 'data']) 208 ModuleFile = collections.namedtuple('ModuleFile', ['path', 'data'])
198 209
199 210
200 class Application(object): 211 class Application(object):
201 """Configurable GAE application. 212 """Configurable GAE application.
202 213
203 Can be used to query and change GAE application configuration (default 214 Can be used to query and change GAE application configuration (default
204 serving version, uploaded versions, etc.). Built on top of appcfg.py calls. 215 serving version, uploaded versions, etc.). Built on top of appcfg.py calls.
205 """ 216 """
206 217
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
276 cmd = [ 287 cmd = [
277 sys.executable, 288 sys.executable,
278 os.path.join(gae_sdk_path(), 'appcfg.py'), 289 os.path.join(gae_sdk_path(), 'appcfg.py'),
279 '--application', self.app_id, 290 '--application', self.app_id,
280 '--noauth_local_webserver', 291 '--noauth_local_webserver',
281 'list_versions', 292 'list_versions',
282 ] 293 ]
283 return subprocess.call(cmd, cwd=self._app_dir) == 0 294 return subprocess.call(cmd, cwd=self._app_dir) == 0
284 295
285 def run_cmd(self, cmd, cwd=None): 296 def run_cmd(self, cmd, cwd=None):
286 """Runs subprocess, capturing the output.""" 297 """Runs subprocess, capturing the output.
298
299 Doesn't close stdin, since gcloud may be asking for user input. If this is
300 undesirable (e.g when gae.py is used from scripts), close 'stdin' of gae.py
301 process itself.
302 """
287 logging.debug('Running %s', cmd) 303 logging.debug('Running %s', cmd)
288 proc = subprocess.Popen( 304 proc = subprocess.Popen(
289 cmd, 305 cmd,
290 cwd=cwd or self._app_dir, 306 cwd=cwd or self._app_dir,
291 stdout=subprocess.PIPE, 307 stdout=subprocess.PIPE)
292 stdin=subprocess.PIPE) 308 output, _ = proc.communicate()
293 output, _ = proc.communicate(None)
294 if proc.returncode: 309 if proc.returncode:
295 sys.stderr.write('\n' + output + '\n') 310 sys.stderr.write('\n' + output + '\n')
296 raise subprocess.CalledProcessError(proc.returncode, cmd, output) 311 raise subprocess.CalledProcessError(proc.returncode, cmd, output)
297 return output 312 return output
298 313
299 def run_appcfg(self, args): 314 def run_appcfg(self, args):
300 """Runs appcfg.py <args>, deserializes its output and returns it.""" 315 """Runs appcfg.py <args>, deserializes its output and returns it."""
301 if not is_oauth_token_cached(): 316 if not is_oauth_token_cached():
302 raise LoginRequiredError('Login first using \'login\' subcommand.') 317 raise LoginRequiredError('Login first using \'login\' subcommand.')
303 cmd = [ 318 cmd = [
304 sys.executable, 319 sys.executable,
305 os.path.join(gae_sdk_path(), 'appcfg.py'), 320 os.path.join(gae_sdk_path(), 'appcfg.py'),
306 '--application', self.app_id, 321 '--application', self.app_id,
307 ] 322 ]
308 if self._verbose: 323 if self._verbose:
309 cmd.append('--verbose') 324 cmd.append('--verbose')
310 cmd.extend(args) 325 cmd.extend(args)
311 return yaml.safe_load(self.run_cmd(cmd)) 326 return yaml.safe_load(self.run_cmd(cmd))
312 327
313 def run_gcloud(self, args): 328 def run_gcloud(self, args):
314 """Runs gcloud <args>.""" 329 """Runs gcloud <args>."""
330 gcloud = find_gcloud()
315 if not is_gcloud_oauth2_token_cached(): 331 if not is_gcloud_oauth2_token_cached():
316 raise LoginRequiredError('Login first using \'gcloud auth login\'') 332 raise LoginRequiredError('Login first using \'gcloud auth login\'')
317 check_tool_in_path('gcloud') 333 return self.run_cmd([gcloud] + args)
318 return self.run_cmd(['gcloud'] + args)
319 334
320 def list_versions(self): 335 def list_versions(self):
321 """List all uploaded versions. 336 """List all uploaded versions.
322 337
323 Returns: 338 Returns:
324 Dict {module name -> [list of uploaded versions]}. 339 Dict {module name -> [list of uploaded versions]}.
325 """ 340 """
326 return self.run_appcfg(['list_versions']) 341 return self.run_appcfg(['list_versions'])
327 342
328 def set_default_version(self, version, modules=None): 343 def set_default_version(self, version, modules=None):
(...skipping 13 matching lines...) Expand all
342 'delete_version', 357 'delete_version',
343 '--module', module, 358 '--module', module,
344 '--version', version, 359 '--version', version,
345 ]) 360 ])
346 361
347 def update_modules(self, version, modules=None): 362 def update_modules(self, version, modules=None):
348 """Deploys new version of the given module names. 363 """Deploys new version of the given module names.
349 364
350 Supports deploying modules both with Managed VMs and AppEngine v1 runtime. 365 Supports deploying modules both with Managed VMs and AppEngine v1 runtime.
351 """ 366 """
352 reg_modules = [] 367 mods = []
353 mvm_modules = []
354 try: 368 try:
355 for m in sorted(modules or self.modules): 369 for m in sorted(modules or self.modules):
356 mod = self._modules[m] 370 mod = self._modules[m]
357 if mod.data.get('vm'): 371 if mod.data.get('vm'):
358 mvm_modules.append(mod) 372 raise UnsupportedModuleError('MVM is not supported: %s' % m)
359 else: 373 if mod.data.get('env') == 'flex':
360 reg_modules.append(mod) 374 raise UnsupportedModuleError('Flex is not supported yet: %s' % m)
361 if mod.data.get('runtime') == 'go' and not os.environ.get('GOROOT'): 375 if mod.data.get('runtime') == 'go' and not os.environ.get('GOROOT'):
362 raise BadEnvironmentConfig('GOROOT must be set when deploying Go app') 376 raise BadEnvironmentError('GOROOT must be set when deploying Go app')
377 mods.append(mod)
363 except KeyError as e: 378 except KeyError as e:
364 raise ValueError('Unknown module: %s' % e) 379 raise ValueError('Unknown module: %s' % e)
365 if reg_modules: 380 # Always make 'default' the first module to be uploaded.
366 # Always make 'default' the first module to be uploaded. 381 mods.sort(key=lambda x: '' if x == 'default' else x)
367 reg_modules.sort(key=lambda x: '' if x == 'default' else x) 382 self.run_appcfg(
368 self.run_appcfg( 383 ['update'] + [m.path for m in mods] + ['--version', version])
369 ['update'] + [m.path for m in reg_modules] + ['--version', version])
370 # Go modules have to be deployed one at a time, based on docs.
371 for m in mvm_modules:
372 self.deploy_mvm_module(m, version)
373 384
374 def update_indexes(self): 385 def update_indexes(self):
375 """Deploys new index.yaml.""" 386 """Deploys new index.yaml."""
376 if os.path.isfile(os.path.join(self.default_module_dir, 'index.yaml')): 387 if os.path.isfile(os.path.join(self.default_module_dir, 'index.yaml')):
377 self.run_appcfg(['update_indexes', self.default_module_dir]) 388 self.run_appcfg(['update_indexes', self.default_module_dir])
378 389
379 def update_queues(self): 390 def update_queues(self):
380 """Deploys new queue.yaml.""" 391 """Deploys new queue.yaml."""
381 if os.path.isfile(os.path.join(self.default_module_dir, 'queue.yaml')): 392 if os.path.isfile(os.path.join(self.default_module_dir, 'queue.yaml')):
382 self.run_appcfg(['update_queues', self.default_module_dir]) 393 self.run_appcfg(['update_queues', self.default_module_dir])
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after
451 462
452 # Sort by version number (best effort, nonconforming version names will 463 # Sort by version number (best effort, nonconforming version names will
453 # appear first in the list). 464 # appear first in the list).
454 def extract_version_num(version): 465 def extract_version_num(version):
455 try: 466 try:
456 return int(version.split('-', 1)[0]) 467 return int(version.split('-', 1)[0])
457 except ValueError: 468 except ValueError:
458 return -1 469 return -1
459 return sorted(actual_versions, key=extract_version_num) 470 return sorted(actual_versions, key=extract_version_num)
460 471
461 def deploy_mvm_module(self, mod, version):
462 """Uses gcloud to upload MVM module using remote docker build.
463
464 Assumes 'gcloud' and 'aedeploy' are in PATH.
465 """
466 # 'aedeploy' requires cwd to be set to module path.
467 check_tool_in_path('gcloud')
468 cmd = [
469 'gcloud', 'app', 'deploy', os.path.basename(mod.path),
470 '--project', self.app_id,
471 '--version', version,
472 '--docker-build', 'remote',
473 '--no-promote', '--force',
474 ]
475 if self._verbose:
476 cmd.extend(['--verbosity', 'debug'])
477 if mod.data.get('runtime') == 'go':
478 check_tool_in_path('aedeploy')
479 cmd = ['aedeploy'] + cmd
480 self.run_cmd(cmd, cwd=os.path.dirname(mod.path))
481
482 def get_actives(self, modules=None): 472 def get_actives(self, modules=None):
483 """Returns active version(s).""" 473 """Returns active version(s)."""
484 args = [ 474 args = [
485 'app', 'versions', 'list', 475 'app', 'versions', 'list',
486 '--project', self.app_id, 476 '--project', self.app_id,
487 '--format', 'json', 477 '--format', 'json',
488 '--hide-no-traffic', 478 '--hide-no-traffic',
489 ] 479 ]
490 raw = self.run_gcloud(args) 480 raw = self.run_gcloud(args)
491 try: 481 try:
492 data = json.loads(raw) 482 data = json.loads(raw)
493 except ValueError: 483 except ValueError:
494 sys.stderr.write('Failed to decode %r as JSON\n' % raw) 484 sys.stderr.write('Failed to decode %r as JSON\n' % raw)
495 raise 485 raise
496 # TODO(maruel): Handle when traffic_split != 1.0. 486 # TODO(maruel): Handle when traffic_split != 1.0.
497 # TODO(maruel): There's a lot more data, decide what is generally useful in 487 # TODO(maruel): There's a lot more data, decide what is generally useful in
498 # there. 488 # there.
499 return [ 489 return [
500 { 490 {
501 'creationTime': service['version']['createTime'], 491 'creationTime': service['version']['createTime'],
502 'deployer': service['version']['createdBy'], 492 'deployer': service['version']['createdBy'],
503 'id': service['id'], 493 'id': service['id'],
504 'service': service['service'], 494 'service': service['service'],
505 } for service in data if not modules or service['service'] in modules 495 } for service in data if not modules or service['service'] in modules
506 ] 496 ]
507 497
508 498
509 def check_tool_in_path(tool):
510 """Raises BadEnvironmentConfig error if no such executable in PATH."""
511 for path in os.environ['PATH'].split(os.pathsep):
512 exe_file = os.path.join(path, tool)
513 if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
514 return
515 msg = 'Can\'t find "%s" in PATH.' % tool
516 if tool in KNOWN_TOOLS:
517 msg += ' ' + KNOWN_TOOLS[tool]
518 raise BadEnvironmentConfig(msg)
519
520
521 def setup_env(app_dir, app_id, version, module_id, remote_api=False): 499 def setup_env(app_dir, app_id, version, module_id, remote_api=False):
522 """Setups os.environ so GAE code works.""" 500 """Setups os.environ so GAE code works."""
523 # GCS library behaves differently when running under remote_api. It uses 501 # GCS library behaves differently when running under remote_api. It uses
524 # SERVER_SOFTWARE to figure this out. See cloudstorage/common.py, local_run(). 502 # SERVER_SOFTWARE to figure this out. See cloudstorage/common.py, local_run().
525 if remote_api: 503 if remote_api:
526 os.environ['SERVER_SOFTWARE'] = 'remote_api' 504 os.environ['SERVER_SOFTWARE'] = 'remote_api'
527 else: 505 else:
528 os.environ['SERVER_SOFTWARE'] = 'Development yo dawg/1.0' 506 os.environ['SERVER_SOFTWARE'] = 'Development yo dawg/1.0'
529 if app_dir: 507 if app_dir:
530 app_id = app_id or Application(app_dir).app_id 508 app_id = app_id or Application(app_dir).app_id
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
571 New instance of Application configured based on passed options. 549 New instance of Application configured based on passed options.
572 """ 550 """
573 logging.basicConfig(level=logging.DEBUG if options.verbose else logging.ERROR) 551 logging.basicConfig(level=logging.DEBUG if options.verbose else logging.ERROR)
574 552
575 if not app_dir and not options.app_dir: 553 if not app_dir and not options.app_dir:
576 parser.error('--app-dir option is required') 554 parser.error('--app-dir option is required')
577 app_dir = os.path.abspath(app_dir or options.app_dir) 555 app_dir = os.path.abspath(app_dir or options.app_dir)
578 556
579 try: 557 try:
580 runtime = get_app_runtime(find_app_yamls(app_dir)) 558 runtime = get_app_runtime(find_app_yamls(app_dir))
581 except (BadEnvironmentConfig, ValueError) as exc: 559 except (Error, ValueError) as exc:
582 parser.error(str(exc)) 560 parser.error(str(exc))
583 561
584 sdk_path = options.sdk_path or find_gae_sdk(RUNTIME_TO_SDK[runtime], app_dir) 562 sdk_path = options.sdk_path or find_gae_sdk(RUNTIME_TO_SDK[runtime], app_dir)
585 if not sdk_path: 563 if not sdk_path:
586 parser.error('Failed to find the AppEngine SDK. Pass --sdk-path argument.') 564 parser.error('Failed to find the AppEngine SDK. Pass --sdk-path argument.')
587 565
588 setup_gae_sdk(sdk_path) 566 setup_gae_sdk(sdk_path)
589 567
590 try: 568 try:
591 return Application(app_dir, options.app_id, options.verbose) 569 return Application(app_dir, options.app_id, options.verbose)
592 except (BadEnvironmentConfig, ValueError) as e: 570 except (Error, ValueError) as e:
593 parser.error(str(e)) 571 parser.error(str(e))
594 572
595 573
596 def confirm(text, app, version, modules=None, default_yes=False): 574 def confirm(text, app, version, modules=None, default_yes=False):
597 """Asks a user to confirm the action related to GAE app. 575 """Asks a user to confirm the action related to GAE app.
598 576
599 Args: 577 Args:
600 text: actual text of the prompt. 578 text: actual text of the prompt.
601 app: instance of Application. 579 app: instance of Application.
602 version: version or a list of versions to operate upon. 580 version: version or a list of versions to operate upon.
(...skipping 26 matching lines...) Expand all
629 with open(p) as f: 607 with open(p) as f:
630 return len(json.load(f)['data']) != 0 608 return len(json.load(f)['data']) != 0
631 except (KeyError, IOError, OSError, ValueError): 609 except (KeyError, IOError, OSError, ValueError):
632 return False 610 return False
633 611
634 612
635 def setup_gae_env(): 613 def setup_gae_env():
636 """Sets up App Engine Python test environment.""" 614 """Sets up App Engine Python test environment."""
637 sdk_path = find_gae_sdk(PYTHON_GAE_SDK) 615 sdk_path = find_gae_sdk(PYTHON_GAE_SDK)
638 if not sdk_path: 616 if not sdk_path:
639 raise BadEnvironmentConfig('Couldn\'t find GAE SDK.') 617 raise BadEnvironmentError('Couldn\'t find GAE SDK.')
640 setup_gae_sdk(sdk_path) 618 setup_gae_sdk(sdk_path)
OLDNEW
« no previous file with comments | « no previous file | appengine/components/tools/gae.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698