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

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
55 def find_gae_sdk(sdk_name=PYTHON_GAE_SDK, search_dir=TOOLS_DIR): 56 def find_gae_sdk(sdk_name=PYTHON_GAE_SDK, search_dir=TOOLS_DIR):
56 """Returns the path to GAE SDK if found, else None.""" 57 """Returns the path to GAE SDK if found, else None."""
57 # First search up the directories up to root. 58 # First search up the directories up to root.
58 while True: 59 while True:
59 attempt = os.path.join(search_dir, sdk_name) 60 attempt = os.path.join(search_dir, sdk_name)
60 if os.path.isfile(os.path.join(attempt, 'dev_appserver.py')): 61 if os.path.isfile(os.path.join(attempt, 'dev_appserver.py')):
61 return attempt 62 return attempt
62 prev_dir = search_dir 63 prev_dir = search_dir
63 search_dir = os.path.dirname(search_dir) 64 search_dir = os.path.dirname(search_dir)
64 if search_dir == prev_dir: 65 if search_dir == prev_dir:
(...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after
183 yaml = yaml_module 184 yaml = yaml_module
184 185
185 186
186 def gae_sdk_path(): 187 def gae_sdk_path():
187 """Checks that 'setup_gae_sdk' was called and returns a path to GAE SDK.""" 188 """Checks that 'setup_gae_sdk' was called and returns a path to GAE SDK."""
188 if not _GAE_SDK_PATH: 189 if not _GAE_SDK_PATH:
189 raise ValueError('setup_gae_sdk wasn\'t called') 190 raise ValueError('setup_gae_sdk wasn\'t called')
190 return _GAE_SDK_PATH 191 return _GAE_SDK_PATH
191 192
192 193
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']) 194 ModuleFile = collections.namedtuple('ModuleFile', ['path', 'data'])
198 195
199 196
200 class Application(object): 197 class Application(object):
201 """Configurable GAE application. 198 """Configurable GAE application.
202 199
203 Can be used to query and change GAE application configuration (default 200 Can be used to query and change GAE application configuration (default
204 serving version, uploaded versions, etc.). Built on top of appcfg.py calls. 201 serving version, uploaded versions, etc.). Built on top of appcfg.py calls.
205 """ 202 """
206 203
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
276 cmd = [ 273 cmd = [
277 sys.executable, 274 sys.executable,
278 os.path.join(gae_sdk_path(), 'appcfg.py'), 275 os.path.join(gae_sdk_path(), 'appcfg.py'),
279 '--application', self.app_id, 276 '--application', self.app_id,
280 '--noauth_local_webserver', 277 '--noauth_local_webserver',
281 'list_versions', 278 'list_versions',
282 ] 279 ]
283 return subprocess.call(cmd, cwd=self._app_dir) == 0 280 return subprocess.call(cmd, cwd=self._app_dir) == 0
284 281
285 def run_cmd(self, cmd, cwd=None): 282 def run_cmd(self, cmd, cwd=None):
286 """Runs subprocess, capturing the output.""" 283 """Runs subprocess, capturing the output.
284
285 Doesn't close stdin, since gcloud may me asking for user input. If this is
Vadim Sh. 2017/05/23 19:36:11 this is actually needed for http://shortn/_h5Bh8n3
iannucci 2017/05/23 19:38:08 s/may me/may be
Vadim Sh. 2017/05/23 19:46:36 Done.
286 undesirable (e.g when gae.py is used from scripts), close 'stdin' of gae.py
287 process itself.
288 """
287 logging.debug('Running %s', cmd) 289 logging.debug('Running %s', cmd)
288 proc = subprocess.Popen( 290 proc = subprocess.Popen(
289 cmd, 291 cmd,
290 cwd=cwd or self._app_dir, 292 cwd=cwd or self._app_dir,
291 stdout=subprocess.PIPE, 293 stdout=subprocess.PIPE)
292 stdin=subprocess.PIPE) 294 output, _ = proc.communicate()
293 output, _ = proc.communicate(None)
294 if proc.returncode: 295 if proc.returncode:
295 sys.stderr.write('\n' + output + '\n') 296 sys.stderr.write('\n' + output + '\n')
296 raise subprocess.CalledProcessError(proc.returncode, cmd, output) 297 raise subprocess.CalledProcessError(proc.returncode, cmd, output)
297 return output 298 return output
298 299
299 def run_appcfg(self, args): 300 def run_appcfg(self, args):
300 """Runs appcfg.py <args>, deserializes its output and returns it.""" 301 """Runs appcfg.py <args>, deserializes its output and returns it."""
301 if not is_oauth_token_cached(): 302 if not is_oauth_token_cached():
302 raise LoginRequiredError('Login first using \'login\' subcommand.') 303 raise LoginRequiredError('Login first using \'login\' subcommand.')
303 cmd = [ 304 cmd = [
304 sys.executable, 305 sys.executable,
305 os.path.join(gae_sdk_path(), 'appcfg.py'), 306 os.path.join(gae_sdk_path(), 'appcfg.py'),
306 '--application', self.app_id, 307 '--application', self.app_id,
307 ] 308 ]
308 if self._verbose: 309 if self._verbose:
309 cmd.append('--verbose') 310 cmd.append('--verbose')
310 cmd.extend(args) 311 cmd.extend(args)
311 return yaml.safe_load(self.run_cmd(cmd)) 312 return yaml.safe_load(self.run_cmd(cmd))
312 313
313 def run_gcloud(self, args): 314 def run_gcloud(self, args):
314 """Runs gcloud <args>.""" 315 """Runs gcloud <args>."""
316 gcloud = find_gcloud()
315 if not is_gcloud_oauth2_token_cached(): 317 if not is_gcloud_oauth2_token_cached():
316 raise LoginRequiredError('Login first using \'gcloud auth login\'') 318 raise LoginRequiredError('Login first using \'gcloud auth login\'')
317 check_tool_in_path('gcloud') 319 return self.run_cmd([gcloud] + args)
318 return self.run_cmd(['gcloud'] + args)
319 320
320 def list_versions(self): 321 def list_versions(self):
321 """List all uploaded versions. 322 """List all uploaded versions.
322 323
323 Returns: 324 Returns:
324 Dict {module name -> [list of uploaded versions]}. 325 Dict {module name -> [list of uploaded versions]}.
325 """ 326 """
326 return self.run_appcfg(['list_versions']) 327 return self.run_appcfg(['list_versions'])
327 328
328 def set_default_version(self, version, modules=None): 329 def set_default_version(self, version, modules=None):
(...skipping 13 matching lines...) Expand all
342 'delete_version', 343 'delete_version',
343 '--module', module, 344 '--module', module,
344 '--version', version, 345 '--version', version,
345 ]) 346 ])
346 347
347 def update_modules(self, version, modules=None): 348 def update_modules(self, version, modules=None):
348 """Deploys new version of the given module names. 349 """Deploys new version of the given module names.
349 350
350 Supports deploying modules both with Managed VMs and AppEngine v1 runtime. 351 Supports deploying modules both with Managed VMs and AppEngine v1 runtime.
351 """ 352 """
352 reg_modules = [] 353 mods = []
353 mvm_modules = []
354 try: 354 try:
355 for m in sorted(modules or self.modules): 355 for m in sorted(modules or self.modules):
356 mod = self._modules[m] 356 mod = self._modules[m]
357 if mod.data.get('vm'): 357 if mod.data.get('vm'):
358 mvm_modules.append(mod) 358 raise UnsupportedModuleError('MVM is not supported: %s' % m)
359 else: 359 if mod.data.get('env') == 'flex':
360 reg_modules.append(mod) 360 raise UnsupportedModuleError('Flex is not supported yet: %s' % m)
361 if mod.data.get('runtime') == 'go' and not os.environ.get('GOROOT'): 361 if mod.data.get('runtime') == 'go' and not os.environ.get('GOROOT'):
362 raise BadEnvironmentConfig('GOROOT must be set when deploying Go app') 362 raise BadEnvironmentError('GOROOT must be set when deploying Go app')
363 mods.append(mod)
363 except KeyError as e: 364 except KeyError as e:
364 raise ValueError('Unknown module: %s' % e) 365 raise ValueError('Unknown module: %s' % e)
365 if reg_modules: 366 # Always make 'default' the first module to be uploaded.
366 # Always make 'default' the first module to be uploaded. 367 mods.sort(key=lambda x: '' if x == 'default' else x)
367 reg_modules.sort(key=lambda x: '' if x == 'default' else x) 368 self.run_appcfg(
368 self.run_appcfg( 369 ['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 370
374 def update_indexes(self): 371 def update_indexes(self):
375 """Deploys new index.yaml.""" 372 """Deploys new index.yaml."""
376 if os.path.isfile(os.path.join(self.default_module_dir, 'index.yaml')): 373 if os.path.isfile(os.path.join(self.default_module_dir, 'index.yaml')):
377 self.run_appcfg(['update_indexes', self.default_module_dir]) 374 self.run_appcfg(['update_indexes', self.default_module_dir])
378 375
379 def update_queues(self): 376 def update_queues(self):
380 """Deploys new queue.yaml.""" 377 """Deploys new queue.yaml."""
381 if os.path.isfile(os.path.join(self.default_module_dir, 'queue.yaml')): 378 if os.path.isfile(os.path.join(self.default_module_dir, 'queue.yaml')):
382 self.run_appcfg(['update_queues', self.default_module_dir]) 379 self.run_appcfg(['update_queues', self.default_module_dir])
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after
451 448
452 # Sort by version number (best effort, nonconforming version names will 449 # Sort by version number (best effort, nonconforming version names will
453 # appear first in the list). 450 # appear first in the list).
454 def extract_version_num(version): 451 def extract_version_num(version):
455 try: 452 try:
456 return int(version.split('-', 1)[0]) 453 return int(version.split('-', 1)[0])
457 except ValueError: 454 except ValueError:
458 return -1 455 return -1
459 return sorted(actual_versions, key=extract_version_num) 456 return sorted(actual_versions, key=extract_version_num)
460 457
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): 458 def get_actives(self, modules=None):
483 """Returns active version(s).""" 459 """Returns active version(s)."""
484 args = [ 460 args = [
485 'app', 'versions', 'list', 461 'app', 'versions', 'list',
486 '--project', self.app_id, 462 '--project', self.app_id,
487 '--format', 'json', 463 '--format', 'json',
488 '--hide-no-traffic', 464 '--hide-no-traffic',
489 ] 465 ]
490 raw = self.run_gcloud(args) 466 raw = self.run_gcloud(args)
491 try: 467 try:
492 data = json.loads(raw) 468 data = json.loads(raw)
493 except ValueError: 469 except ValueError:
494 sys.stderr.write('Failed to decode %r as JSON\n' % raw) 470 sys.stderr.write('Failed to decode %r as JSON\n' % raw)
495 raise 471 raise
496 # TODO(maruel): Handle when traffic_split != 1.0. 472 # TODO(maruel): Handle when traffic_split != 1.0.
497 # TODO(maruel): There's a lot more data, decide what is generally useful in 473 # TODO(maruel): There's a lot more data, decide what is generally useful in
498 # there. 474 # there.
499 return [ 475 return [
500 { 476 {
501 'creationTime': service['version']['createTime'], 477 'creationTime': service['version']['createTime'],
502 'deployer': service['version']['createdBy'], 478 'deployer': service['version']['createdBy'],
503 'id': service['id'], 479 'id': service['id'],
504 'service': service['service'], 480 'service': service['service'],
505 } for service in data if not modules or service['service'] in modules 481 } for service in data if not modules or service['service'] in modules
506 ] 482 ]
507 483
508 484
509 def check_tool_in_path(tool): 485 def find_gcloud():
510 """Raises BadEnvironmentConfig error if no such executable in PATH.""" 486 """Searches for 'gcloud' binary in PATH and returns absolute path to it.
487
488 Raises BadEnvironmentError error if it's not there.
489 """
511 for path in os.environ['PATH'].split(os.pathsep): 490 for path in os.environ['PATH'].split(os.pathsep):
512 exe_file = os.path.join(path, tool) 491 exe_file = os.path.join(path, 'gcloud')
513 if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK): 492 if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
514 return 493 return exe_file
515 msg = 'Can\'t find "%s" in PATH.' % tool 494 raise BadEnvironmentError(
516 if tool in KNOWN_TOOLS: 495 'Can\'t find "gcloud" in PATH. Install the Google Cloud SDK from '
517 msg += ' ' + KNOWN_TOOLS[tool] 496 'https://cloud.google.com/sdk/')
518 raise BadEnvironmentConfig(msg)
519 497
520 498
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'
(...skipping 42 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