App Engine Python SDK version 1.9.12
[gae.git] / python / google / appengine / tools / appcfg.py
blob591527c7b378d9d7a62ff20bcd8461254c64008b
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Tool for deploying apps to an app server.
19 Currently, the application only uploads new appversions. To do this, it first
20 walks the directory tree rooted at the path the user specifies, adding all the
21 files it finds to a list. It then uploads the application configuration
22 (app.yaml) to the server using HTTP, followed by uploading each of the files.
23 It then commits the transaction with another request.
25 The bulk of this work is handled by the AppVersionUpload class, which exposes
26 methods to add to the list of files, fetch a list of modified files, upload
27 files, and commit or rollback the transaction.
28 """
29 from __future__ import with_statement
32 import calendar
33 import contextlib
34 import copy
35 import datetime
36 import errno
37 import getpass
38 import hashlib
39 import logging
40 import mimetypes
41 import optparse
42 import os
43 import random
44 import re
45 import shutil
46 import subprocess
47 import sys
48 import tempfile
49 import time
50 import urllib
51 import urllib2
55 import google
56 import yaml
58 from google.appengine.cron import groctimespecification
59 from google.appengine.api import appinfo
60 from google.appengine.api import appinfo_includes
61 from google.appengine.api import backendinfo
62 from google.appengine.api import client_deployinfo
63 from google.appengine.api import croninfo
64 from google.appengine.api import dispatchinfo
65 from google.appengine.api import dosinfo
66 from google.appengine.api import queueinfo
67 from google.appengine.api import yaml_errors
68 from google.appengine.api import yaml_object
69 from google.appengine.datastore import datastore_index
70 from google.appengine.tools import appengine_rpc
72 try:
75 from google.appengine.tools import appengine_rpc_httplib2
76 except ImportError:
77 appengine_rpc_httplib2 = None
78 if sys.version_info[:2] >= (2, 7):
82 from google.appengine.tools import appcfg_java
83 else:
84 appcfg_java = None
86 from google.appengine.tools import augment_mimetypes
87 from google.appengine.tools import bulkloader
88 from google.appengine.tools import sdk_update_checker
92 LIST_DELIMITER = '\n'
93 TUPLE_DELIMITER = '|'
94 BACKENDS_ACTION = 'backends'
95 BACKENDS_MESSAGE = ('Warning: This application uses Backends, a deprecated '
96 'feature that has been replaced by Modules, which '
97 'offers additional functionality. Please convert your '
98 'backends to modules as described at: ')
99 _CONVERTING_URL = (
100 'https://developers.google.com/appengine/docs/%s/modules/converting')
103 MAX_LOG_LEVEL = 4
106 MAX_BATCH_SIZE = 3200000
107 MAX_BATCH_COUNT = 100
108 MAX_BATCH_FILE_SIZE = 200000
109 BATCH_OVERHEAD = 500
116 verbosity = 1
119 PREFIXED_BY_ADMIN_CONSOLE_RE = '^(?:admin-console)(.*)'
122 SDK_PRODUCT = 'appcfg_py'
125 DAY = 24*3600
126 SUNDAY = 6
128 SUPPORTED_RUNTIMES = (
129 'contrib-dart', 'dart', 'go', 'php', 'python', 'python27', 'java', 'java7',
130 'vm', 'custom')
135 MEGA = 1024 * 1024
136 MILLION = 1000 * 1000
137 DEFAULT_RESOURCE_LIMITS = {
138 'max_file_size': 32 * MILLION,
139 'max_blob_size': 32 * MILLION,
140 'max_files_to_clone': 100,
141 'max_total_file_size': 150 * MEGA,
142 'max_file_count': 10000,
145 # Client ID and secrets are managed in the Google API console.
151 APPCFG_CLIENT_ID = '550516889912.apps.googleusercontent.com'
152 APPCFG_CLIENT_NOTSOSECRET = 'ykPq-0UYfKNprLRjVx1hBBar'
153 APPCFG_SCOPES = ('https://www.googleapis.com/auth/appengine.admin',
154 'https://www.googleapis.com/auth/cloud-platform')
157 STATIC_FILE_PREFIX = '__static__'
161 METADATA_BASE = 'http://metadata.google.internal'
162 SERVICE_ACCOUNT_BASE = (
163 'computeMetadata/v1beta1/instance/service-accounts/default')
166 APP_YAML_FILENAME = 'app.yaml'
171 GO_APP_BUILDER = os.path.join('goroot', 'bin', 'go-app-builder')
172 if sys.platform.startswith('win'):
173 GO_APP_BUILDER += '.exe'
176 augment_mimetypes.init()
179 class Error(Exception):
180 pass
183 class OAuthNotAvailable(Error):
184 """The appengine_rpc_httplib2 module could not be imported."""
185 pass
188 class CannotStartServingError(Error):
189 """We could not start serving the version being uploaded."""
190 pass
193 def PrintUpdate(msg, error_fh=sys.stderr):
194 """Print a message to stderr or the given file-like object.
196 If 'verbosity' is greater than 0, print the message.
198 Args:
199 msg: The string to print.
200 error_fh: Where to send the message.
202 if verbosity > 0:
203 timestamp = datetime.datetime.now()
204 print >>error_fh, '%s %s' % (timestamp.strftime('%I:%M %p'), msg)
207 def StatusUpdate(msg, error_fh=sys.stderr):
208 """Print a status message to stderr or the given file-like object."""
209 PrintUpdate(msg, error_fh)
212 def BackendsStatusUpdate(runtime, error_fh=sys.stderr):
213 """Print the Backends status message based on current runtime.
215 Args:
216 runtime: String name of current runtime.
217 error_fh: Where to send the message.
219 language = runtime
220 if language == 'python27':
221 language = 'python'
222 elif language == 'java7':
223 language = 'java'
224 if language == 'python' or language == 'java':
225 StatusUpdate(BACKENDS_MESSAGE + (_CONVERTING_URL % language), error_fh)
228 def ErrorUpdate(msg, error_fh=sys.stderr):
229 """Print an error message to stderr."""
230 PrintUpdate(msg, error_fh)
233 def _PrintErrorAndExit(stream, msg, exit_code=2):
234 """Prints the given error message and exists the program.
236 Args:
237 stream: The stream (e.g. StringIO or file) to write the message to.
238 msg: The error message to display as a string.
239 exit_code: The integer code to pass to sys.exit().
241 stream.write(msg)
242 sys.exit(exit_code)
245 def JavaSupported():
246 """True if Java is supported by this SDK."""
249 tools_java_dir = os.path.join(os.path.dirname(appcfg_java.__file__), 'java')
250 return os.path.isdir(tools_java_dir)
253 @contextlib.contextmanager
254 def TempChangeField(obj, field_name, new_value):
255 """Context manager to change a field value on an object temporarily.
257 Args:
258 obj: The object to change the field on.
259 field_name: The field name to change.
260 new_value: The new value.
262 Yields:
263 The old value.
265 old_value = getattr(obj, field_name)
266 setattr(obj, field_name, new_value)
267 yield old_value
268 setattr(obj, field_name, old_value)
271 class FileClassification(object):
272 """A class to hold a file's classification.
274 This class both abstracts away the details of how we determine
275 whether a file is a regular, static or error file as well as acting
276 as a container for various metadata about the file.
279 def __init__(self, config, filename, error_fh=sys.stderr):
280 """Initializes a FileClassification instance.
282 Args:
283 config: The app.yaml object to check the filename against.
284 filename: The name of the file.
285 error_fh: Where to send status and error messages.
287 self.__error_fh = error_fh
288 self.__static_mime_type = self.__GetMimeTypeIfStaticFile(config, filename)
289 self.__static_app_readable = self.__GetAppReadableIfStaticFile(config,
290 filename)
291 self.__error_mime_type, self.__error_code = self.__LookupErrorBlob(config,
292 filename)
294 def __GetMimeTypeIfStaticFile(self, config, filename):
295 """Looks up the mime type for 'filename'.
297 Uses the handlers in 'config' to determine if the file should
298 be treated as a static file.
300 Args:
301 config: The app.yaml object to check the filename against.
302 filename: The name of the file.
304 Returns:
305 The mime type string. For example, 'text/plain' or 'image/gif'.
306 None if this is not a static file.
308 if self.__FileNameImpliesStaticFile(filename):
309 return self.__MimeType(filename)
310 for handler in config.handlers:
311 handler_type = handler.GetHandlerType()
312 if handler_type in ('static_dir', 'static_files'):
313 if handler_type == 'static_dir':
314 regex = os.path.join(re.escape(handler.GetHandler()), '.*')
315 else:
316 regex = handler.upload
317 if re.match(regex, filename):
318 return handler.mime_type or self.__MimeType(filename)
319 return None
321 @staticmethod
322 def __FileNameImpliesStaticFile(filename):
323 """True if the name of a file implies that it is a static resource.
325 For Java applications specified with web.xml and appengine-web.xml, we
326 create a staging directory that includes a __static__ hierarchy containing
327 links to all files that are implied static by the contents of those XML
328 files. So if a file has been copied into that directory then we can assume
329 it is static.
331 Args:
332 filename: The full path to the file.
334 Returns:
335 True if the file should be considered a static resource based on its name.
337 static = '__static__' + os.sep
338 return static in filename
340 @staticmethod
341 def __GetAppReadableIfStaticFile(config, filename):
342 """Looks up whether a static file is readable by the application.
344 Uses the handlers in 'config' to determine if the file should
345 be treated as a static file and if so, if the file should be readable by the
346 application.
348 Args:
349 config: The AppInfoExternal object to check the filename against.
350 filename: The name of the file.
352 Returns:
353 True if the file is static and marked as app readable, False otherwise.
355 for handler in config.handlers:
356 handler_type = handler.GetHandlerType()
357 if handler_type in ('static_dir', 'static_files'):
358 if handler_type == 'static_dir':
359 regex = os.path.join(re.escape(handler.GetHandler()), '.*')
360 else:
361 regex = handler.upload
362 if re.match(regex, filename):
363 return handler.application_readable
364 return False
366 def __LookupErrorBlob(self, config, filename):
367 """Looks up the mime type and error_code for 'filename'.
369 Uses the error handlers in 'config' to determine if the file should
370 be treated as an error blob.
372 Args:
373 config: The app.yaml object to check the filename against.
374 filename: The name of the file.
376 Returns:
378 A tuple of (mime_type, error_code), or (None, None) if this is not an
379 error blob. For example, ('text/plain', default) or ('image/gif',
380 timeout) or (None, None).
382 if not config.error_handlers:
383 return (None, None)
384 for error_handler in config.error_handlers:
385 if error_handler.file == filename:
386 error_code = error_handler.error_code
387 error_code = error_code or 'default'
388 if error_handler.mime_type:
389 return (error_handler.mime_type, error_code)
390 else:
391 return (self.__MimeType(filename), error_code)
392 return (None, None)
394 def __MimeType(self, filename, default='application/octet-stream'):
395 guess = mimetypes.guess_type(filename)[0]
396 if guess is None:
397 print >>self.__error_fh, ('Could not guess mimetype for %s. Using %s.'
398 % (filename, default))
399 return default
400 return guess
402 def IsApplicationFile(self):
403 return bool((not self.IsStaticFile() or self.__static_app_readable) and
404 not self.IsErrorFile())
406 def IsStaticFile(self):
407 return bool(self.__static_mime_type)
409 def StaticMimeType(self):
410 return self.__static_mime_type
412 def IsErrorFile(self):
413 return bool(self.__error_mime_type)
415 def ErrorMimeType(self):
416 return self.__error_mime_type
418 def ErrorCode(self):
419 return self.__error_code
422 def BuildClonePostBody(file_tuples):
423 """Build the post body for the /api/clone{files,blobs,errorblobs} urls.
425 Args:
426 file_tuples: A list of tuples. Each tuple should contain the entries
427 appropriate for the endpoint in question.
429 Returns:
430 A string containing the properly delimited tuples.
432 file_list = []
433 for tup in file_tuples:
434 path = tup[1]
435 tup = tup[2:]
436 file_list.append(TUPLE_DELIMITER.join([path] + list(tup)))
437 return LIST_DELIMITER.join(file_list)
440 def _GetRemoteResourceLimits(logging_context):
441 """Get the resource limit as reported by the admin console.
443 Get the resource limits by querying the admin_console/appserver. The
444 actual limits returned depends on the server we are talking to and
445 could be missing values we expect or include extra values.
447 Args:
448 logging_context: The _ClientDeployLoggingContext for this upload.
450 Returns:
451 A dictionary.
453 try:
454 yaml_data = logging_context.Send('/api/appversion/getresourcelimits')
456 except urllib2.HTTPError, err:
460 if err.code != 404:
461 raise
462 return {}
464 return yaml.safe_load(yaml_data)
467 def GetResourceLimits(logging_context, error_fh=sys.stderr):
468 """Gets the resource limits.
470 Gets the resource limits that should be applied to apps. Any values
471 that the server does not know about will have their default value
472 reported (although it is also possible for the server to report
473 values we don't know about).
475 Args:
476 logging_context: The _ClientDeployLoggingContext for this upload.
477 error_fh: Where to send status and error messages.
479 Returns:
480 A dictionary.
482 resource_limits = DEFAULT_RESOURCE_LIMITS.copy()
483 StatusUpdate('Getting current resource limits.', error_fh)
484 resource_limits.update(_GetRemoteResourceLimits(logging_context))
485 logging.debug('Using resource limits: %s', resource_limits)
486 return resource_limits
489 def RetryWithBackoff(callable_func, retry_notify_func,
490 initial_delay=1, backoff_factor=2,
491 max_delay=60, max_tries=20):
492 """Calls a function multiple times, backing off more and more each time.
494 Args:
495 callable_func: A function that performs some operation that should be
496 retried a number of times upon failure. Signature: () -> (done, value)
497 If 'done' is True, we'll immediately return (True, value)
498 If 'done' is False, we'll delay a bit and try again, unless we've
499 hit the 'max_tries' limit, in which case we'll return (False, value).
500 retry_notify_func: This function will be called immediately before the
501 next retry delay. Signature: (value, delay) -> None
502 'value' is the value returned by the last call to 'callable_func'
503 'delay' is the retry delay, in seconds
504 initial_delay: Initial delay after first try, in seconds.
505 backoff_factor: Delay will be multiplied by this factor after each try.
506 max_delay: Maximum delay, in seconds.
507 max_tries: Maximum number of tries (the first one counts).
509 Returns:
510 What the last call to 'callable_func' returned, which is of the form
511 (done, value). If 'done' is True, you know 'callable_func' returned True
512 before we ran out of retries. If 'done' is False, you know 'callable_func'
513 kept returning False and we ran out of retries.
515 Raises:
516 Whatever the function raises--an exception will immediately stop retries.
519 delay = initial_delay
520 num_tries = 0
522 while True:
523 done, opaque_value = callable_func()
524 num_tries += 1
526 if done:
527 return True, opaque_value
529 if num_tries >= max_tries:
530 return False, opaque_value
532 retry_notify_func(opaque_value, delay)
533 time.sleep(delay)
534 delay = min(delay * backoff_factor, max_delay)
537 def RetryNoBackoff(callable_func,
538 retry_notify_func,
539 delay=5,
540 max_tries=200):
541 """Calls a function multiple times, with the same delay each time.
543 Args:
544 callable_func: A function that performs some operation that should be
545 retried a number of times upon failure. Signature: () -> (done, value)
546 If 'done' is True, we'll immediately return (True, value)
547 If 'done' is False, we'll delay a bit and try again, unless we've
548 hit the 'max_tries' limit, in which case we'll return (False, value).
549 retry_notify_func: This function will be called immediately before the
550 next retry delay. Signature: (value, delay) -> None
551 'value' is the value returned by the last call to 'callable_func'
552 'delay' is the retry delay, in seconds
553 delay: Delay between tries, in seconds.
554 max_tries: Maximum number of tries (the first one counts).
556 Returns:
557 What the last call to 'callable_func' returned, which is of the form
558 (done, value). If 'done' is True, you know 'callable_func' returned True
559 before we ran out of retries. If 'done' is False, you know 'callable_func'
560 kept returning False and we ran out of retries.
562 Raises:
563 Whatever the function raises--an exception will immediately stop retries.
566 return RetryWithBackoff(callable_func,
567 retry_notify_func,
568 delay,
570 delay,
571 max_tries)
574 def MigratePython27Notice():
575 """Tells the user that Python 2.5 runtime is deprecated.
577 Encourages the user to migrate from Python 2.5 to Python 2.7.
579 Prints a message to sys.stdout. The caller should have tested that the user is
580 using Python 2.5, so as not to spuriously display this message.
582 print (
583 'WARNING: This application is using the Python 2.5 runtime, which is '
584 'deprecated! It should be updated to the Python 2.7 runtime as soon as '
585 'possible, which offers performance improvements and many new features. '
586 'Learn how simple it is to migrate your application to Python 2.7 at '
587 'https://developers.google.com/appengine/docs/python/python25/migrate27.')
590 class IndexDefinitionUpload(object):
591 """Provides facilities to upload index definitions to the hosting service."""
593 def __init__(self, rpcserver, definitions, error_fh=sys.stderr):
594 """Creates a new DatastoreIndexUpload.
596 Args:
597 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
598 or TestRpcServer.
599 definitions: An IndexDefinitions object.
600 error_fh: Where to send status and error messages.
602 self.rpcserver = rpcserver
603 self.definitions = definitions
604 self.error_fh = error_fh
606 def DoUpload(self):
607 """Uploads the index definitions."""
608 StatusUpdate('Uploading index definitions.', self.error_fh)
610 with TempChangeField(self.definitions, 'application', None) as app_id:
611 self.rpcserver.Send('/api/datastore/index/add',
612 app_id=app_id,
613 payload=self.definitions.ToYAML())
616 class CronEntryUpload(object):
617 """Provides facilities to upload cron entries to the hosting service."""
619 def __init__(self, rpcserver, cron, error_fh=sys.stderr):
620 """Creates a new CronEntryUpload.
622 Args:
623 rpcserver: The RPC server to use. Should be an instance of a subclass of
624 AbstractRpcServer
625 cron: The CronInfoExternal object loaded from the cron.yaml file.
626 error_fh: Where to send status and error messages.
628 self.rpcserver = rpcserver
629 self.cron = cron
630 self.error_fh = error_fh
632 def DoUpload(self):
633 """Uploads the cron entries."""
634 StatusUpdate('Uploading cron entries.', self.error_fh)
636 with TempChangeField(self.cron, 'application', None) as app_id:
637 self.rpcserver.Send('/api/cron/update',
638 app_id=app_id,
639 payload=self.cron.ToYAML())
642 class QueueEntryUpload(object):
643 """Provides facilities to upload task queue entries to the hosting service."""
645 def __init__(self, rpcserver, queue, error_fh=sys.stderr):
646 """Creates a new QueueEntryUpload.
648 Args:
649 rpcserver: The RPC server to use. Should be an instance of a subclass of
650 AbstractRpcServer
651 queue: The QueueInfoExternal object loaded from the queue.yaml file.
652 error_fh: Where to send status and error messages.
654 self.rpcserver = rpcserver
655 self.queue = queue
656 self.error_fh = error_fh
658 def DoUpload(self):
659 """Uploads the task queue entries."""
660 StatusUpdate('Uploading task queue entries.', self.error_fh)
662 with TempChangeField(self.queue, 'application', None) as app_id:
663 self.rpcserver.Send('/api/queue/update',
664 app_id=app_id,
665 payload=self.queue.ToYAML())
668 class DispatchEntryUpload(object):
669 """Provides facilities to upload dispatch entries to the hosting service."""
671 def __init__(self, rpcserver, dispatch, error_fh=sys.stderr):
672 """Creates a new DispatchEntryUpload.
674 Args:
675 rpcserver: The RPC server to use. Should be an instance of a subclass of
676 AbstractRpcServer
677 dispatch: The DispatchInfoExternal object loaded from the dispatch.yaml
678 file.
679 error_fh: Where to send status and error messages.
681 self.rpcserver = rpcserver
682 self.dispatch = dispatch
683 self.error_fh = error_fh
685 def DoUpload(self):
686 """Uploads the dispatch entries."""
687 StatusUpdate('Uploading dispatch entries.', self.error_fh)
688 self.rpcserver.Send('/api/dispatch/update',
689 app_id=self.dispatch.application,
690 payload=self.dispatch.ToYAML())
693 class DosEntryUpload(object):
694 """Provides facilities to upload dos entries to the hosting service."""
696 def __init__(self, rpcserver, dos, error_fh=sys.stderr):
697 """Creates a new DosEntryUpload.
699 Args:
700 rpcserver: The RPC server to use. Should be an instance of a subclass of
701 AbstractRpcServer.
702 dos: The DosInfoExternal object loaded from the dos.yaml file.
703 error_fh: Where to send status and error messages.
705 self.rpcserver = rpcserver
706 self.dos = dos
707 self.error_fh = error_fh
709 def DoUpload(self):
710 """Uploads the dos entries."""
711 StatusUpdate('Uploading DOS entries.', self.error_fh)
713 with TempChangeField(self.dos, 'application', None) as app_id:
714 self.rpcserver.Send('/api/dos/update',
715 app_id=app_id,
716 payload=self.dos.ToYAML())
719 class PagespeedEntryUpload(object):
720 """Provides facilities to upload pagespeed configs to the hosting service."""
722 def __init__(self, rpcserver, config, pagespeed, error_fh=sys.stderr):
723 """Creates a new PagespeedEntryUpload.
725 Args:
726 rpcserver: The RPC server to use. Should be an instance of a subclass of
727 AbstractRpcServer.
728 config: The AppInfoExternal object derived from the app.yaml file.
729 pagespeed: The PagespeedEntry object from config.
730 error_fh: Where to send status and error messages.
732 self.rpcserver = rpcserver
733 self.config = config
734 self.pagespeed = pagespeed
735 self.error_fh = error_fh
737 def DoUpload(self):
738 """Uploads the pagespeed entries."""
740 pagespeed_yaml = ''
741 if self.pagespeed:
742 StatusUpdate('Uploading PageSpeed configuration.', self.error_fh)
743 pagespeed_yaml = self.pagespeed.ToYAML()
744 try:
745 self.rpcserver.Send('/api/appversion/updatepagespeed',
746 app_id=self.config.application,
747 version=self.config.version,
748 payload=pagespeed_yaml)
749 except urllib2.HTTPError, err:
759 if err.code != 404 or self.pagespeed is not None:
760 raise
763 class DefaultVersionSet(object):
764 """Provides facilities to set the default (serving) version."""
766 def __init__(self, rpcserver, app_id, module, version, error_fh=sys.stderr):
767 """Creates a new DefaultVersionSet.
769 Args:
770 rpcserver: The RPC server to use. Should be an instance of a subclass of
771 AbstractRpcServer.
772 app_id: The application to make the change to.
773 module: The module to set the default version of (if any).
774 version: The version to set as the default.
775 error_fh: Where to send status and error messages.
777 self.rpcserver = rpcserver
778 self.app_id = app_id
779 self.module = module
780 self.version = version
781 self.error_fh = error_fh
783 def SetVersion(self):
784 """Sets the default version."""
785 if self.module:
787 modules = self.module.split(',')
788 if len(modules) > 1:
789 StatusUpdate('Setting the default version of modules %s of application '
790 '%s to %s.' % (', '.join(modules),
791 self.app_id,
792 self.version),
793 self.error_fh)
798 params = [('app_id', self.app_id), ('version', self.version)]
799 params.extend(('module', module) for module in modules)
800 url = '/api/appversion/setdefault?' + urllib.urlencode(sorted(params))
801 self.rpcserver.Send(url)
802 return
804 else:
805 StatusUpdate('Setting default version of module %s of application %s '
806 'to %s.' % (self.module, self.app_id, self.version),
807 self.error_fh)
808 else:
809 StatusUpdate('Setting default version of application %s to %s.'
810 % (self.app_id, self.version), self.error_fh)
811 self.rpcserver.Send('/api/appversion/setdefault',
812 app_id=self.app_id,
813 module=self.module,
814 version=self.version)
817 class TrafficMigrator(object):
818 """Provides facilities to migrate traffic."""
820 def __init__(self, rpcserver, app_id, version, error_fh=sys.stderr):
821 """Creates a new TrafficMigrator.
823 Args:
824 rpcserver: The RPC server to use. Should be an instance of a subclass of
825 AbstractRpcServer.
826 app_id: The application to make the change to.
827 version: The version to set as the default.
828 error_fh: Where to send status and error messages.
830 self.rpcserver = rpcserver
831 self.app_id = app_id
832 self.version = version
833 self.error_fh = error_fh
835 def MigrateTraffic(self):
836 """Migrates traffic."""
837 StatusUpdate('Migrating traffic of application %s to %s.'
838 % (self.app_id, self.version), self.error_fh)
839 self.rpcserver.Send('/api/appversion/migratetraffic',
840 app_id=self.app_id,
841 version=self.version)
844 class IndexOperation(object):
845 """Provide facilities for writing Index operation commands."""
847 def __init__(self, rpcserver, error_fh=sys.stderr):
848 """Creates a new IndexOperation.
850 Args:
851 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
852 or TestRpcServer.
853 error_fh: Where to send status and error messages.
855 self.rpcserver = rpcserver
856 self.error_fh = error_fh
858 def DoDiff(self, definitions):
859 """Retrieve diff file from the server.
861 Args:
862 definitions: datastore_index.IndexDefinitions as loaded from users
863 index.yaml file.
865 Returns:
866 A pair of datastore_index.IndexDefinitions objects. The first record
867 is the set of indexes that are present in the index.yaml file but missing
868 from the server. The second record is the set of indexes that are
869 present on the server but missing from the index.yaml file (indicating
870 that these indexes should probably be vacuumed).
872 StatusUpdate('Fetching index definitions diff.', self.error_fh)
873 with TempChangeField(definitions, 'application', None) as app_id:
874 response = self.rpcserver.Send('/api/datastore/index/diff',
875 app_id=app_id,
876 payload=definitions.ToYAML())
878 return datastore_index.ParseMultipleIndexDefinitions(response)
880 def DoDelete(self, definitions, app_id):
881 """Delete indexes from the server.
883 Args:
884 definitions: Index definitions to delete from datastore.
885 app_id: The application id.
887 Returns:
888 A single datstore_index.IndexDefinitions containing indexes that were
889 not deleted, probably because they were already removed. This may
890 be normal behavior as there is a potential race condition between fetching
891 the index-diff and sending deletion confirmation through.
893 StatusUpdate('Deleting selected index definitions.', self.error_fh)
895 response = self.rpcserver.Send('/api/datastore/index/delete',
896 app_id=app_id,
897 payload=definitions.ToYAML())
898 return datastore_index.ParseIndexDefinitions(response)
901 class VacuumIndexesOperation(IndexOperation):
902 """Provide facilities to request the deletion of datastore indexes."""
904 def __init__(self, rpcserver, force, confirmation_fn=raw_input,
905 error_fh=sys.stderr):
906 """Creates a new VacuumIndexesOperation.
908 Args:
909 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
910 or TestRpcServer.
911 force: True to force deletion of indexes, else False.
912 confirmation_fn: Function used for getting input form user.
913 error_fh: Where to send status and error messages.
915 super(VacuumIndexesOperation, self).__init__(rpcserver, error_fh)
916 self.force = force
917 self.confirmation_fn = confirmation_fn
919 def GetConfirmation(self, index):
920 """Get confirmation from user to delete an index.
922 This method will enter an input loop until the user provides a
923 response it is expecting. Valid input is one of three responses:
925 y: Confirm deletion of index.
926 n: Do not delete index.
927 a: Delete all indexes without asking for further confirmation.
929 If the user enters nothing at all, the default action is to skip
930 that index and do not delete.
932 If the user selects 'a', as a side effect, the 'force' flag is set.
934 Args:
935 index: Index to confirm.
937 Returns:
938 True if user enters 'y' or 'a'. False if user enter 'n'.
940 while True:
942 print 'This index is no longer defined in your index.yaml file.'
943 print
944 print index.ToYAML()
945 print
948 confirmation = self.confirmation_fn(
949 'Are you sure you want to delete this index? (N/y/a): ')
950 confirmation = confirmation.strip().lower()
953 if confirmation == 'y':
954 return True
955 elif confirmation == 'n' or not confirmation:
956 return False
957 elif confirmation == 'a':
958 self.force = True
959 return True
960 else:
961 print 'Did not understand your response.'
963 def DoVacuum(self, definitions):
964 """Vacuum indexes in datastore.
966 This method will query the server to determine which indexes are not
967 being used according to the user's local index.yaml file. Once it has
968 made this determination, it confirms with the user which unused indexes
969 should be deleted. Once confirmation for each index is receives, it
970 deletes those indexes.
972 Because another user may in theory delete the same indexes at the same
973 time as the user, there is a potential race condition. In this rare cases,
974 some of the indexes previously confirmed for deletion will not be found.
975 The user is notified which indexes these were.
977 Args:
978 definitions: datastore_index.IndexDefinitions as loaded from users
979 index.yaml file.
982 unused_new_indexes, notused_indexes = self.DoDiff(definitions)
985 deletions = datastore_index.IndexDefinitions(indexes=[])
986 if notused_indexes.indexes is not None:
987 for index in notused_indexes.indexes:
988 if self.force or self.GetConfirmation(index):
989 deletions.indexes.append(index)
992 if deletions.indexes:
993 not_deleted = self.DoDelete(deletions, definitions.application)
996 if not_deleted.indexes:
997 not_deleted_count = len(not_deleted.indexes)
998 if not_deleted_count == 1:
999 warning_message = ('An index was not deleted. Most likely this is '
1000 'because it no longer exists.\n\n')
1001 else:
1002 warning_message = ('%d indexes were not deleted. Most likely this '
1003 'is because they no longer exist.\n\n'
1004 % not_deleted_count)
1005 for index in not_deleted.indexes:
1006 warning_message += index.ToYAML()
1007 logging.warning(warning_message)
1010 class LogsRequester(object):
1011 """Provide facilities to export request logs."""
1013 def __init__(self,
1014 rpcserver,
1015 app_id,
1016 module,
1017 version_id,
1018 output_file,
1019 num_days,
1020 append,
1021 severity,
1022 end,
1023 vhost,
1024 include_vhost,
1025 include_all=None,
1026 time_func=time.time,
1027 error_fh=sys.stderr):
1028 """Constructor.
1030 Args:
1031 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1032 or TestRpcServer.
1033 app_id: The application to fetch logs from.
1034 module: The module of the app to fetch logs from, optional.
1035 version_id: The version of the app to fetch logs for.
1036 output_file: Output file name.
1037 num_days: Number of days worth of logs to export; 0 for all available.
1038 append: True if appending to an existing file.
1039 severity: App log severity to request (0-4); None for no app logs.
1040 end: date object representing last day of logs to return.
1041 vhost: The virtual host of log messages to get. None for all hosts.
1042 include_vhost: If true, the virtual host is included in log messages.
1043 include_all: If true, we add to the log message everything we know
1044 about the request.
1045 time_func: A time.time() compatible function, which can be overridden for
1046 testing.
1047 error_fh: Where to send status and error messages.
1050 self.rpcserver = rpcserver
1051 self.app_id = app_id
1052 self.output_file = output_file
1053 self.append = append
1054 self.num_days = num_days
1055 self.severity = severity
1056 self.vhost = vhost
1057 self.include_vhost = include_vhost
1058 self.include_all = include_all
1059 self.error_fh = error_fh
1061 self.module = module
1062 self.version_id = version_id
1063 self.sentinel = None
1064 self.write_mode = 'w'
1065 if self.append:
1066 self.sentinel = FindSentinel(self.output_file)
1067 self.write_mode = 'a'
1070 self.skip_until = False
1071 now = PacificDate(time_func())
1072 if end < now:
1073 self.skip_until = end
1074 else:
1076 end = now
1078 self.valid_dates = None
1079 if self.num_days:
1080 start = end - datetime.timedelta(self.num_days - 1)
1081 self.valid_dates = (start, end)
1083 def DownloadLogs(self):
1084 """Download the requested logs.
1086 This will write the logs to the file designated by
1087 self.output_file, or to stdout if the filename is '-'.
1088 Multiple roundtrips to the server may be made.
1090 if self.module:
1091 StatusUpdate('Downloading request logs for app %s module %s version %s.' %
1092 (self.app_id, self.module, self.version_id), self.error_fh)
1093 else:
1094 StatusUpdate('Downloading request logs for app %s version %s.' %
1095 (self.app_id, self.version_id), self.error_fh)
1101 tf = tempfile.TemporaryFile()
1102 last_offset = None
1103 try:
1104 while True:
1105 try:
1106 new_offset = self.RequestLogLines(tf, last_offset)
1107 if not new_offset or new_offset == last_offset:
1108 break
1109 last_offset = new_offset
1110 except KeyboardInterrupt:
1111 StatusUpdate('Keyboard interrupt; saving data downloaded so far.',
1112 self.error_fh)
1113 break
1114 StatusUpdate('Copying request logs to %r.' % self.output_file,
1115 self.error_fh)
1116 if self.output_file == '-':
1117 of = sys.stdout
1118 else:
1119 try:
1120 of = open(self.output_file, self.write_mode)
1121 except IOError, err:
1122 StatusUpdate('Can\'t write %r: %s.' % (self.output_file, err))
1123 sys.exit(1)
1124 try:
1125 line_count = CopyReversedLines(tf, of)
1126 finally:
1127 of.flush()
1128 if of is not sys.stdout:
1129 of.close()
1130 finally:
1131 tf.close()
1132 StatusUpdate('Copied %d records.' % line_count, self.error_fh)
1134 def RequestLogLines(self, tf, offset):
1135 """Make a single roundtrip to the server.
1137 Args:
1138 tf: Writable binary stream to which the log lines returned by
1139 the server are written, stripped of headers, and excluding
1140 lines skipped due to self.sentinel or self.valid_dates filtering.
1141 offset: Offset string for a continued request; None for the first.
1143 Returns:
1144 The offset string to be used for the next request, if another
1145 request should be issued; or None, if not.
1147 logging.info('Request with offset %r.', offset)
1148 kwds = {'app_id': self.app_id,
1149 'version': self.version_id,
1150 'limit': 1000,
1151 'no_header': 1,
1153 if self.module:
1154 kwds['module'] = self.module
1155 if offset:
1156 kwds['offset'] = offset
1157 if self.severity is not None:
1158 kwds['severity'] = str(self.severity)
1159 if self.vhost is not None:
1160 kwds['vhost'] = str(self.vhost)
1161 if self.include_vhost is not None:
1162 kwds['include_vhost'] = str(self.include_vhost)
1163 if self.include_all is not None:
1164 kwds['include_all'] = str(self.include_all)
1165 response = self.rpcserver.Send('/api/request_logs', payload=None, **kwds)
1166 response = response.replace('\r', '\0')
1167 lines = response.splitlines()
1168 logging.info('Received %d bytes, %d records.', len(response), len(lines))
1169 offset = None
1171 valid_dates = self.valid_dates
1172 sentinel = self.sentinel
1173 skip_until = self.skip_until
1174 len_sentinel = None
1175 if sentinel:
1176 len_sentinel = len(sentinel)
1177 for line in lines:
1178 if line.startswith('#'):
1179 match = re.match(r'^#\s*next_offset=(\S+)\s*$', line)
1182 if match and match.group(1) != 'None':
1183 offset = match.group(1)
1184 continue
1186 if (sentinel and
1187 line.startswith(sentinel) and
1188 line[len_sentinel : len_sentinel+1] in ('', '\0')):
1189 return None
1191 linedate = DateOfLogLine(line)
1193 if not linedate:
1194 continue
1196 if skip_until:
1197 if linedate > skip_until:
1198 continue
1199 else:
1201 self.skip_until = skip_until = False
1203 if valid_dates and not valid_dates[0] <= linedate <= valid_dates[1]:
1204 return None
1205 tf.write(line + '\n')
1206 if not lines:
1207 return None
1208 return offset
1211 def DateOfLogLine(line):
1212 """Returns a date object representing the log line's timestamp.
1214 Args:
1215 line: a log line string.
1216 Returns:
1217 A date object representing the timestamp or None if parsing fails.
1219 m = re.compile(r'[^[]+\[(\d+/[A-Za-z]+/\d+):[^\d]*').match(line)
1220 if not m:
1221 return None
1222 try:
1223 return datetime.date(*time.strptime(m.group(1), '%d/%b/%Y')[:3])
1224 except ValueError:
1225 return None
1228 def PacificDate(now):
1229 """For a UTC timestamp, return the date in the US/Pacific timezone.
1231 Args:
1232 now: A posix timestamp giving current UTC time.
1234 Returns:
1235 A date object representing what day it is in the US/Pacific timezone.
1238 return datetime.date(*time.gmtime(PacificTime(now))[:3])
1241 def PacificTime(now):
1242 """Helper to return the number of seconds between UTC and Pacific time.
1244 This is needed to compute today's date in Pacific time (more
1245 specifically: Mountain View local time), which is how request logs
1246 are reported. (Google servers always report times in Mountain View
1247 local time, regardless of where they are physically located.)
1249 This takes (post-2006) US DST into account. Pacific time is either
1250 8 hours or 7 hours west of UTC, depending on whether DST is in
1251 effect. Since 2007, US DST starts on the Second Sunday in March
1252 March, and ends on the first Sunday in November. (Reference:
1253 http://aa.usno.navy.mil/faq/docs/daylight_time.php.)
1255 Note that the server doesn't report its local time (the HTTP Date
1256 header uses UTC), and the client's local time is irrelevant.
1258 Args:
1259 now: A posix timestamp giving current UTC time.
1261 Returns:
1262 A pseudo-posix timestamp giving current Pacific time. Passing
1263 this through time.gmtime() will produce a tuple in Pacific local
1264 time.
1266 now -= 8*3600
1267 if IsPacificDST(now):
1268 now += 3600
1269 return now
1272 def IsPacificDST(now):
1273 """Helper for PacificTime to decide whether now is Pacific DST (PDT).
1275 Args:
1276 now: A pseudo-posix timestamp giving current time in PST.
1278 Returns:
1279 True if now falls within the range of DST, False otherwise.
1281 pst = time.gmtime(now)
1282 year = pst[0]
1283 assert year >= 2007
1285 begin = calendar.timegm((year, 3, 8, 2, 0, 0, 0, 0, 0))
1286 while time.gmtime(begin).tm_wday != SUNDAY:
1287 begin += DAY
1289 end = calendar.timegm((year, 11, 1, 2, 0, 0, 0, 0, 0))
1290 while time.gmtime(end).tm_wday != SUNDAY:
1291 end += DAY
1292 return begin <= now < end
1295 def CopyReversedLines(instream, outstream, blocksize=2**16):
1296 r"""Copy lines from input stream to output stream in reverse order.
1298 As a special feature, null bytes in the input are turned into
1299 newlines followed by tabs in the output, but these 'sub-lines'
1300 separated by null bytes are not reversed. E.g. If the input is
1301 'A\0B\nC\0D\n', the output is 'C\n\tD\nA\n\tB\n'.
1303 Args:
1304 instream: A seekable stream open for reading in binary mode.
1305 outstream: A stream open for writing; doesn't have to be seekable or binary.
1306 blocksize: Optional block size for buffering, for unit testing.
1308 Returns:
1309 The number of lines copied.
1311 line_count = 0
1312 instream.seek(0, 2)
1313 last_block = instream.tell() // blocksize
1314 spillover = ''
1315 for iblock in xrange(last_block + 1, -1, -1):
1316 instream.seek(iblock * blocksize)
1317 data = instream.read(blocksize)
1318 lines = data.splitlines(True)
1319 lines[-1:] = ''.join(lines[-1:] + [spillover]).splitlines(True)
1320 if lines and not lines[-1].endswith('\n'):
1322 lines[-1] += '\n'
1323 lines.reverse()
1324 if lines and iblock > 0:
1325 spillover = lines.pop()
1326 if lines:
1327 line_count += len(lines)
1328 data = ''.join(lines).replace('\0', '\n\t')
1329 outstream.write(data)
1330 return line_count
1333 def FindSentinel(filename, blocksize=2**16, error_fh=sys.stderr):
1334 """Return the sentinel line from the output file.
1336 Args:
1337 filename: The filename of the output file. (We'll read this file.)
1338 blocksize: Optional block size for buffering, for unit testing.
1339 error_fh: Where to send status and error messages.
1341 Returns:
1342 The contents of the last line in the file that doesn't start with
1343 a tab, with its trailing newline stripped; or None if the file
1344 couldn't be opened or no such line could be found by inspecting
1345 the last 'blocksize' bytes of the file.
1347 if filename == '-':
1348 StatusUpdate('Can\'t combine --append with output to stdout.',
1349 error_fh)
1350 sys.exit(2)
1351 try:
1352 fp = open(filename, 'rb')
1353 except IOError, err:
1354 StatusUpdate('Append mode disabled: can\'t read %r: %s.' % (filename, err),
1355 error_fh)
1356 return None
1357 try:
1358 fp.seek(0, 2)
1359 fp.seek(max(0, fp.tell() - blocksize))
1360 lines = fp.readlines()
1361 del lines[:1]
1362 sentinel = None
1363 for line in lines:
1364 if not line.startswith('\t'):
1365 sentinel = line
1366 if not sentinel:
1368 StatusUpdate('Append mode disabled: can\'t find sentinel in %r.' %
1369 filename, error_fh)
1370 return None
1371 return sentinel.rstrip('\n')
1372 finally:
1373 fp.close()
1376 class UploadBatcher(object):
1377 """Helper to batch file uploads."""
1379 def __init__(self, what, logging_context):
1380 """Constructor.
1382 Args:
1383 what: Either 'file' or 'blob' or 'errorblob' indicating what kind of
1384 objects this batcher uploads. Used in messages and URLs.
1385 logging_context: The _ClientDeployLoggingContext for this upload.
1387 assert what in ('file', 'blob', 'errorblob'), repr(what)
1388 self.what = what
1389 self.logging_context = logging_context
1390 self.single_url = '/api/appversion/add' + what
1391 self.batch_url = self.single_url + 's'
1392 self.batching = True
1393 self.batch = []
1394 self.batch_size = 0
1396 def SendBatch(self):
1397 """Send the current batch on its way.
1399 If successful, resets self.batch and self.batch_size.
1401 Raises:
1402 HTTPError with code=404 if the server doesn't support batching.
1404 boundary = 'boundary'
1405 parts = []
1406 for path, payload, mime_type in self.batch:
1407 while boundary in payload:
1408 boundary += '%04x' % random.randint(0, 0xffff)
1409 assert len(boundary) < 80, 'Unexpected error, please try again.'
1410 part = '\n'.join(['',
1411 'X-Appcfg-File: %s' % urllib.quote(path),
1412 'X-Appcfg-Hash: %s' % _Hash(payload),
1413 'Content-Type: %s' % mime_type,
1414 'Content-Length: %d' % len(payload),
1415 'Content-Transfer-Encoding: 8bit',
1417 payload,
1419 parts.append(part)
1420 parts.insert(0,
1421 'MIME-Version: 1.0\n'
1422 'Content-Type: multipart/mixed; boundary="%s"\n'
1423 '\n'
1424 'This is a message with multiple parts in MIME format.' %
1425 boundary)
1426 parts.append('--\n')
1427 delimiter = '\n--%s' % boundary
1428 payload = delimiter.join(parts)
1429 logging.info('Uploading batch of %d %ss to %s with boundary="%s".',
1430 len(self.batch), self.what, self.batch_url, boundary)
1431 self.logging_context.Send(self.batch_url,
1432 payload=payload,
1433 content_type='message/rfc822')
1434 self.batch = []
1435 self.batch_size = 0
1437 def SendSingleFile(self, path, payload, mime_type):
1438 """Send a single file on its way."""
1439 logging.info('Uploading %s %s (%s bytes, type=%s) to %s.',
1440 self.what, path, len(payload), mime_type, self.single_url)
1441 self.logging_context.Send(self.single_url,
1442 payload=payload,
1443 content_type=mime_type,
1444 path=path)
1446 def Flush(self):
1447 """Flush the current batch.
1449 This first attempts to send the batch as a single request; if that
1450 fails because the server doesn't support batching, the files are
1451 sent one by one, and self.batching is reset to False.
1453 At the end, self.batch and self.batch_size are reset.
1455 if not self.batch:
1456 return
1457 try:
1458 self.SendBatch()
1459 except urllib2.HTTPError, err:
1460 if err.code != 404:
1461 raise
1464 logging.info('Old server detected; turning off %s batching.', self.what)
1465 self.batching = False
1468 for path, payload, mime_type in self.batch:
1469 self.SendSingleFile(path, payload, mime_type)
1472 self.batch = []
1473 self.batch_size = 0
1475 def AddToBatch(self, path, payload, mime_type):
1476 """Batch a file, possibly flushing first, or perhaps upload it directly.
1478 Args:
1479 path: The name of the file.
1480 payload: The contents of the file.
1481 mime_type: The MIME Content-type of the file, or None.
1483 If mime_type is None, application/octet-stream is substituted.
1485 if not mime_type:
1486 mime_type = 'application/octet-stream'
1487 size = len(payload)
1488 if size <= MAX_BATCH_FILE_SIZE:
1489 if (len(self.batch) >= MAX_BATCH_COUNT or
1490 self.batch_size + size > MAX_BATCH_SIZE):
1491 self.Flush()
1492 if self.batching:
1493 logging.info('Adding %s %s (%s bytes, type=%s) to batch.',
1494 self.what, path, size, mime_type)
1495 self.batch.append((path, payload, mime_type))
1496 self.batch_size += size + BATCH_OVERHEAD
1497 return
1498 self.SendSingleFile(path, payload, mime_type)
1501 def _FormatHash(h):
1502 """Return a string representation of a hash.
1504 The hash is a sha1 hash. It is computed both for files that need to be
1505 pushed to App Engine and for data payloads of requests made to App Engine.
1507 Args:
1508 h: The hash
1510 Returns:
1511 The string representation of the hash.
1513 return '%s_%s_%s_%s_%s' % (h[0:8], h[8:16], h[16:24], h[24:32], h[32:40])
1516 def _Hash(content):
1517 """Compute the sha1 hash of the content.
1519 Args:
1520 content: The data to hash as a string.
1522 Returns:
1523 The string representation of the hash.
1525 h = hashlib.sha1(content).hexdigest()
1526 return _FormatHash(h)
1529 def _HashFromFileHandle(file_handle):
1530 """Compute the hash of the content of the file pointed to by file_handle.
1532 Args:
1533 file_handle: File-like object which provides seek, read and tell.
1535 Returns:
1536 The string representation of the hash.
1545 pos = file_handle.tell()
1546 content_hash = _Hash(file_handle.read())
1547 file_handle.seek(pos, 0)
1548 return content_hash
1551 def EnsureDir(path):
1552 """Makes sure that a directory exists at the given path.
1554 If a directory already exists at that path, nothing is done.
1555 Otherwise, try to create a directory at that path with os.makedirs.
1556 If that fails, propagate the resulting OSError exception.
1558 Args:
1559 path: The path that you want to refer to a directory.
1561 try:
1562 os.makedirs(path)
1563 except OSError, exc:
1566 if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
1567 raise
1570 def DoDownloadApp(rpcserver, out_dir, app_id, module, app_version,
1571 error_fh=sys.stderr):
1572 """Downloads the files associated with a particular app version.
1574 Args:
1575 rpcserver: The RPC server to use to download.
1576 out_dir: The directory the files should be downloaded to.
1577 app_id: The app ID of the app whose files we want to download.
1578 module: The module we want to download from. Can be:
1579 - None: We'll download from the default module.
1580 - <module>: We'll download from the specified module.
1581 app_version: The version number we want to download. Can be:
1582 - None: We'll download the latest default version.
1583 - <major>: We'll download the latest minor version.
1584 - <major>/<minor>: We'll download that exact version.
1585 error_fh: Where to send status and error messages.
1588 StatusUpdate('Fetching file list...', error_fh)
1590 url_args = {'app_id': app_id}
1591 if module:
1592 url_args['module'] = module
1593 if app_version is not None:
1594 url_args['version_match'] = app_version
1596 result = rpcserver.Send('/api/files/list', **url_args)
1598 StatusUpdate('Fetching files...', error_fh)
1600 lines = result.splitlines()
1602 if len(lines) < 1:
1603 logging.error('Invalid response from server: empty')
1604 return
1606 full_version = lines[0]
1607 file_lines = lines[1:]
1609 current_file_number = 0
1610 num_files = len(file_lines)
1612 num_errors = 0
1614 for line in file_lines:
1615 parts = line.split('|', 2)
1616 if len(parts) != 3:
1617 logging.error('Invalid response from server: expecting '
1618 '"<id>|<size>|<path>", found: "%s"\n', line)
1619 return
1621 current_file_number += 1
1623 file_id, size_str, path = parts
1624 try:
1625 size = int(size_str)
1626 except ValueError:
1627 logging.error('Invalid file list entry from server: invalid size: '
1628 '"%s"', size_str)
1629 return
1631 StatusUpdate('[%d/%d] %s' % (current_file_number, num_files, path),
1632 error_fh)
1634 def TryGet():
1635 """A request to /api/files/get which works with the RetryWithBackoff."""
1636 try:
1637 contents = rpcserver.Send('/api/files/get', app_id=app_id,
1638 version=full_version, id=file_id)
1639 return True, contents
1640 except urllib2.HTTPError, exc:
1643 if exc.code == 503:
1644 return False, exc
1645 else:
1646 raise
1648 def PrintRetryMessage(_, delay):
1649 StatusUpdate('Server busy. Will try again in %d seconds.' % delay,
1650 error_fh)
1652 success, contents = RetryWithBackoff(TryGet, PrintRetryMessage)
1653 if not success:
1654 logging.error('Unable to download file "%s".', path)
1655 num_errors += 1
1656 continue
1658 if len(contents) != size:
1659 logging.error('File "%s": server listed as %d bytes but served '
1660 '%d bytes.', path, size, len(contents))
1661 num_errors += 1
1663 full_path = os.path.join(out_dir, path)
1665 if os.path.exists(full_path):
1666 logging.error('Unable to create file "%s": path conflicts with '
1667 'an existing file or directory', path)
1668 num_errors += 1
1669 continue
1671 full_dir = os.path.dirname(full_path)
1672 try:
1673 EnsureDir(full_dir)
1674 except OSError, exc:
1675 logging.error('Couldn\'t create directory "%s": %s', full_dir, exc)
1676 num_errors += 1
1677 continue
1679 try:
1680 out_file = open(full_path, 'wb')
1681 except IOError, exc:
1682 logging.error('Couldn\'t open file "%s": %s', full_path, exc)
1683 num_errors += 1
1684 continue
1686 try:
1687 try:
1688 out_file.write(contents)
1689 except IOError, exc:
1690 logging.error('Couldn\'t write to file "%s": %s', full_path, exc)
1691 num_errors += 1
1692 continue
1693 finally:
1694 out_file.close()
1696 if num_errors > 0:
1697 logging.error('Number of errors: %d. See output for details.', num_errors)
1700 class _ClientDeployLoggingContext(object):
1701 """Context for sending and recording server rpc requests.
1703 Attributes:
1704 rpcserver: The AbstractRpcServer to use for the upload.
1705 requests: A list of client_deployinfo.Request objects to include
1706 with the client deploy log.
1707 time_func: Function to get the current time in milliseconds.
1708 request_params: A dictionary with params to append to requests
1711 def __init__(self,
1712 rpcserver,
1713 request_params,
1714 usage_reporting,
1715 time_func=time.time):
1716 """Creates a new AppVersionUpload.
1718 Args:
1719 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1720 or TestRpcServer.
1721 request_params: A dictionary with params to append to requests
1722 usage_reporting: Whether to actually upload data.
1723 time_func: Function to return the current time in millisecods
1724 (default time.time).
1726 self.rpcserver = rpcserver
1727 self.request_params = request_params
1728 self.usage_reporting = usage_reporting
1729 self.time_func = time_func
1730 self.requests = []
1732 def Send(self, url, payload='', **kwargs):
1733 """Sends a request to the server, with common params."""
1734 start_time_usec = self.GetCurrentTimeUsec()
1735 request_size_bytes = len(payload)
1736 try:
1737 logging.info('Send: %s, params=%s', url, self.request_params)
1739 kwargs.update(self.request_params)
1740 result = self.rpcserver.Send(url, payload=payload, **kwargs)
1741 self._RegisterReqestForLogging(url, 200, start_time_usec,
1742 request_size_bytes)
1743 return result
1744 except urllib2.HTTPError, e:
1745 self._RegisterReqestForLogging(url, e.code, start_time_usec,
1746 request_size_bytes)
1747 raise e
1749 def GetCurrentTimeUsec(self):
1750 """Returns the current time in microseconds."""
1751 return int(round(self.time_func() * 1000 * 1000))
1753 def GetSdkVersion(self):
1754 """Returns the current SDK Version."""
1755 sdk_version = sdk_update_checker.GetVersionObject()
1756 return sdk_version.get('release', '?') if sdk_version else '?'
1758 def _RegisterReqestForLogging(self, path, response_code, start_time_usec,
1759 request_size_bytes):
1760 """Registers a request for client deploy logging purposes."""
1761 end_time_usec = self.GetCurrentTimeUsec()
1762 self.requests.append(client_deployinfo.Request(
1763 path=path,
1764 response_code=response_code,
1765 start_time_usec=start_time_usec,
1766 end_time_usec=end_time_usec,
1767 request_size_bytes=request_size_bytes))
1769 def LogClientDeploy(self, runtime, start_time_usec, success):
1770 """Logs a client deployment attempt.
1772 Args:
1773 runtime: The runtime for the app being deployed.
1774 start_time_usec: The start time of the deployment in micro seconds.
1775 success: True if the deployment succeeded otherwise False.
1777 if not self.usage_reporting:
1778 logging.info('Skipping usage reporting.')
1779 return
1780 end_time_usec = self.GetCurrentTimeUsec()
1781 try:
1782 info = client_deployinfo.ClientDeployInfoExternal(
1783 runtime=runtime,
1784 start_time_usec=start_time_usec,
1785 end_time_usec=end_time_usec,
1786 requests=self.requests,
1787 success=success,
1788 sdk_version=self.GetSdkVersion())
1789 self.Send('/api/logclientdeploy', info.ToYAML())
1790 except BaseException, e:
1791 logging.debug('Exception logging deploy info continuing - %s', e)
1794 class EndpointsState(object):
1795 SERVING = 'serving'
1796 PENDING = 'pending'
1797 FAILED = 'failed'
1798 _STATES = frozenset((SERVING, PENDING, FAILED))
1800 @classmethod
1801 def Parse(cls, value):
1802 state = value.lower()
1803 if state not in cls._STATES:
1804 lst = sorted(cls._STATES)
1805 pretty_states = ', '.join(lst[:-1]) + ', or ' + lst[-1]
1806 raise ValueError('Unexpected Endpoints state "%s"; should be %s.' %
1807 (value, pretty_states))
1808 return state
1811 class AppVersionUpload(object):
1812 """Provides facilities to upload a new appversion to the hosting service.
1814 Attributes:
1815 rpcserver: The AbstractRpcServer to use for the upload.
1816 config: The AppInfoExternal object derived from the app.yaml file.
1817 app_id: The application string from 'config'.
1818 version: The version string from 'config'.
1819 backend: The backend to update, if any.
1820 files: A dictionary of files to upload to the rpcserver, mapping path to
1821 hash of the file contents.
1822 in_transaction: True iff a transaction with the server has started.
1823 An AppVersionUpload can do only one transaction at a time.
1824 deployed: True iff the Deploy method has been called.
1825 started: True iff the StartServing method has been called.
1826 logging_context: The _ClientDeployLoggingContext for this upload.
1829 def __init__(self, rpcserver, config, module_yaml_path='app.yaml',
1830 backend=None,
1831 error_fh=None,
1832 get_version=sdk_update_checker.GetVersionObject,
1833 usage_reporting=False):
1834 """Creates a new AppVersionUpload.
1836 Args:
1837 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1838 or TestRpcServer.
1839 config: An AppInfoExternal object that specifies the configuration for
1840 this application.
1841 module_yaml_path: The (string) path to the yaml file corresponding to
1842 <config>, relative to the bundle directory.
1843 backend: If specified, indicates the update applies to the given backend.
1844 The backend name must match an entry in the backends: stanza.
1845 error_fh: Unexpected HTTPErrors are printed to this file handle.
1846 get_version: Method for determining the current SDK version. The override
1847 is used for testing.
1848 usage_reporting: Whether or not to report usage.
1850 self.rpcserver = rpcserver
1851 self.config = config
1852 self.app_id = self.config.application
1853 self.module = self.config.module
1854 self.backend = backend
1855 self.error_fh = error_fh or sys.stderr
1857 self.version = self.config.version
1859 self.params = {}
1860 if self.app_id:
1861 self.params['app_id'] = self.app_id
1862 if self.module:
1863 self.params['module'] = self.module
1864 if self.backend:
1865 self.params['backend'] = self.backend
1866 elif self.version:
1867 self.params['version'] = self.version
1872 self.files = {}
1875 self.all_files = set()
1877 self.in_transaction = False
1878 self.deployed = False
1879 self.started = False
1880 self.batching = True
1881 self.logging_context = _ClientDeployLoggingContext(rpcserver,
1882 self.params,
1883 usage_reporting)
1884 self.file_batcher = UploadBatcher('file', self.logging_context)
1885 self.blob_batcher = UploadBatcher('blob', self.logging_context)
1886 self.errorblob_batcher = UploadBatcher('errorblob', self.logging_context)
1888 if not self.config.vm_settings:
1889 self.config.vm_settings = appinfo.VmSettings()
1890 self.config.vm_settings['module_yaml_path'] = module_yaml_path
1892 if not self.config.vm_settings.get('image'):
1893 sdk_version = get_version()
1894 if sdk_version and sdk_version.get('release'):
1895 self.config.vm_settings['image'] = sdk_version['release']
1897 if not self.config.auto_id_policy:
1898 self.config.auto_id_policy = appinfo.DATASTORE_ID_POLICY_DEFAULT
1900 def AddFile(self, path, file_handle):
1901 """Adds the provided file to the list to be pushed to the server.
1903 Args:
1904 path: The path the file should be uploaded as.
1905 file_handle: A stream containing data to upload.
1907 assert not self.in_transaction, 'Already in a transaction.'
1908 assert file_handle is not None
1910 reason = appinfo.ValidFilename(path)
1911 if reason:
1912 logging.error(reason)
1913 return
1915 content_hash = _HashFromFileHandle(file_handle)
1917 self.files[path] = content_hash
1918 self.all_files.add(path)
1920 def Describe(self):
1921 """Returns a string describing the object being updated."""
1922 result = 'app: %s' % self.app_id
1923 if self.module is not None and self.module != appinfo.DEFAULT_MODULE:
1924 result += ', module: %s' % self.module
1925 if self.backend:
1926 result += ', backend: %s' % self.backend
1927 elif self.version:
1928 result += ', version: %s' % self.version
1929 return result
1931 @staticmethod
1932 def _ValidateBeginYaml(resp):
1933 """Validates the given /api/appversion/create response string."""
1934 response_dict = yaml.safe_load(resp)
1935 if not response_dict or 'warnings' not in response_dict:
1936 return False
1937 return response_dict
1939 def Begin(self):
1940 """Begins the transaction, returning a list of files that need uploading.
1942 All calls to AddFile must be made before calling Begin().
1944 Returns:
1945 A list of pathnames for files that should be uploaded using UploadFile()
1946 before Commit() can be called.
1948 assert not self.in_transaction, 'Already in a transaction.'
1953 config_copy = copy.deepcopy(self.config)
1954 for url in config_copy.handlers:
1955 handler_type = url.GetHandlerType()
1956 if url.application_readable:
1959 if handler_type == 'static_dir':
1960 url.static_dir = '%s/%s' % (STATIC_FILE_PREFIX, url.static_dir)
1961 elif handler_type == 'static_files':
1962 url.static_files = '%s/%s' % (STATIC_FILE_PREFIX, url.static_files)
1963 url.upload = '%s/%s' % (STATIC_FILE_PREFIX, url.upload)
1965 response = self.logging_context.Send(
1966 '/api/appversion/create',
1967 payload=config_copy.ToYAML())
1969 result = self._ValidateBeginYaml(response)
1970 if result:
1971 warnings = result.get('warnings')
1972 for warning in warnings:
1973 StatusUpdate('WARNING: %s' % warning, self.error_fh)
1975 self.in_transaction = True
1977 files_to_clone = []
1978 blobs_to_clone = []
1979 errorblobs = {}
1980 for path, content_hash in self.files.iteritems():
1981 file_classification = FileClassification(
1982 self.config, path, error_fh=self.error_fh)
1984 if file_classification.IsStaticFile():
1985 upload_path = path
1986 if file_classification.IsApplicationFile():
1987 upload_path = '%s/%s' % (STATIC_FILE_PREFIX, path)
1988 blobs_to_clone.append((path, upload_path, content_hash,
1989 file_classification.StaticMimeType()))
1993 if file_classification.IsErrorFile():
1997 errorblobs[path] = content_hash
1999 if file_classification.IsApplicationFile():
2000 files_to_clone.append((path, path, content_hash))
2002 files_to_upload = {}
2004 def CloneFiles(url, files, file_type):
2005 """Sends files to the given url.
2007 Args:
2008 url: the server URL to use.
2009 files: a list of files
2010 file_type: the type of the files
2012 if not files:
2013 return
2015 StatusUpdate('Cloning %d %s file%s.' %
2016 (len(files), file_type, len(files) != 1 and 's' or ''),
2017 self.error_fh)
2019 max_files = self.resource_limits['max_files_to_clone']
2020 for i in xrange(0, len(files), max_files):
2021 if i > 0 and i % max_files == 0:
2022 StatusUpdate('Cloned %d files.' % i, self.error_fh)
2024 chunk = files[i:min(len(files), i + max_files)]
2025 result = self.logging_context.Send(url,
2026 payload=BuildClonePostBody(chunk))
2027 if result:
2028 to_upload = {}
2029 for f in result.split(LIST_DELIMITER):
2030 for entry in files:
2031 real_path, upload_path = entry[:2]
2032 if f == upload_path:
2033 to_upload[real_path] = self.files[real_path]
2034 break
2035 files_to_upload.update(to_upload)
2037 CloneFiles('/api/appversion/cloneblobs', blobs_to_clone, 'static')
2038 CloneFiles('/api/appversion/clonefiles', files_to_clone, 'application')
2040 logging.debug('Files to upload: %s', files_to_upload)
2042 for (path, content_hash) in errorblobs.iteritems():
2043 files_to_upload[path] = content_hash
2044 self.files = files_to_upload
2045 return sorted(files_to_upload.iterkeys())
2047 def UploadFile(self, path, file_handle):
2048 """Uploads a file to the hosting service.
2050 Must only be called after Begin().
2051 The path provided must be one of those that were returned by Begin().
2053 Args:
2054 path: The path the file is being uploaded as.
2055 file_handle: A file-like object containing the data to upload.
2057 Raises:
2058 KeyError: The provided file is not amongst those to be uploaded.
2060 assert self.in_transaction, 'Begin() must be called before UploadFile().'
2061 if path not in self.files:
2062 raise KeyError('File \'%s\' is not in the list of files to be uploaded.'
2063 % path)
2065 del self.files[path]
2067 file_classification = FileClassification(
2068 self.config, path, error_fh=self.error_fh)
2069 payload = file_handle.read()
2070 if file_classification.IsStaticFile():
2071 upload_path = path
2072 if file_classification.IsApplicationFile():
2073 upload_path = '%s/%s' % (STATIC_FILE_PREFIX, path)
2074 self.blob_batcher.AddToBatch(upload_path, payload,
2075 file_classification.StaticMimeType())
2079 if file_classification.IsErrorFile():
2082 self.errorblob_batcher.AddToBatch(file_classification.ErrorCode(),
2083 payload,
2084 file_classification.ErrorMimeType())
2086 if file_classification.IsApplicationFile():
2088 self.file_batcher.AddToBatch(path, payload, None)
2090 def Precompile(self):
2091 """Handle precompilation."""
2093 StatusUpdate('Compilation starting.', self.error_fh)
2095 files = []
2096 if self.config.GetEffectiveRuntime() == 'go':
2099 for f in self.all_files:
2100 if f.endswith('.go') and not self.config.nobuild_files.match(f):
2101 files.append(f)
2103 while True:
2104 if files:
2105 StatusUpdate('Compilation: %d files left.' % len(files), self.error_fh)
2106 files = self.PrecompileBatch(files)
2107 if not files:
2108 break
2109 StatusUpdate('Compilation completed.', self.error_fh)
2111 def PrecompileBatch(self, files):
2112 """Precompile a batch of files.
2114 Args:
2115 files: Either an empty list (for the initial request) or a list
2116 of files to be precompiled.
2118 Returns:
2119 Either an empty list (if no more files need to be precompiled)
2120 or a list of files to be precompiled subsequently.
2122 payload = LIST_DELIMITER.join(files)
2123 response = self.logging_context.Send('/api/appversion/precompile',
2124 payload=payload)
2125 if not response:
2126 return []
2127 return response.split(LIST_DELIMITER)
2129 def Commit(self):
2130 """Commits the transaction, making the new app version available.
2132 All the files returned by Begin() must have been uploaded with UploadFile()
2133 before Commit() can be called.
2135 This tries the new 'deploy' method; if that fails it uses the old 'commit'.
2137 Returns:
2138 An appinfo.AppInfoSummary if one was returned from the Deploy, None
2139 otherwise.
2141 Raises:
2142 RuntimeError: Some required files were not uploaded.
2143 CannotStartServingError: Another operation is in progress on this version.
2145 assert self.in_transaction, 'Begin() must be called before Commit().'
2146 if self.files:
2147 raise RuntimeError('Not all required files have been uploaded.')
2149 def PrintRetryMessage(_, delay):
2150 StatusUpdate('Will check again in %s seconds.' % delay, self.error_fh)
2152 app_summary = self.Deploy()
2155 success, unused_contents = RetryWithBackoff(
2156 lambda: (self.IsReady(), None), PrintRetryMessage, 1, 2, 60, 20)
2157 if not success:
2159 logging.warning('Version still not ready to serve, aborting.')
2160 raise RuntimeError('Version not ready.')
2162 result = self.StartServing()
2163 if not result:
2166 self.in_transaction = False
2167 else:
2168 if result == '0':
2169 raise CannotStartServingError(
2170 'Another operation on this version is in progress.')
2171 success, response = RetryNoBackoff(self.IsServing, PrintRetryMessage)
2172 if not success:
2174 logging.warning('Version still not serving, aborting.')
2175 raise RuntimeError('Version not ready.')
2179 check_config_updated = response.get('check_endpoints_config')
2180 if check_config_updated:
2181 unused_done, last_state = RetryWithBackoff(
2182 self.IsEndpointsConfigUpdated,
2183 PrintRetryMessage, 1, 2, 60, 20)
2184 if last_state != EndpointsState.SERVING:
2185 error_message = (
2186 'Failed to update Endpoints configuration (last result %s). '
2187 'Check the app\'s AppEngine logs for errors: %s' %
2188 (last_state, self.GetLogUrl()))
2189 StatusUpdate(error_message, self.error_fh)
2190 logging.warning(error_message)
2191 raise RuntimeError(error_message)
2192 self.in_transaction = False
2194 return app_summary
2196 def Deploy(self):
2197 """Deploys the new app version but does not make it default.
2199 All the files returned by Begin() must have been uploaded with UploadFile()
2200 before Deploy() can be called.
2202 Returns:
2203 An appinfo.AppInfoSummary if one was returned from the Deploy, None
2204 otherwise.
2206 Raises:
2207 RuntimeError: Some required files were not uploaded.
2209 assert self.in_transaction, 'Begin() must be called before Deploy().'
2210 if self.files:
2211 raise RuntimeError('Not all required files have been uploaded.')
2213 StatusUpdate('Starting deployment.', self.error_fh)
2214 result = self.logging_context.Send('/api/appversion/deploy')
2215 self.deployed = True
2217 if result:
2218 return yaml_object.BuildSingleObject(appinfo.AppInfoSummary, result)
2219 else:
2220 return None
2222 def IsReady(self):
2223 """Check if the new app version is ready to serve traffic.
2225 Raises:
2226 RuntimeError: Deploy has not yet been called.
2228 Returns:
2229 True if the server returned the app is ready to serve.
2231 assert self.deployed, 'Deploy() must be called before IsReady().'
2233 StatusUpdate('Checking if deployment succeeded.', self.error_fh)
2234 result = self.logging_context.Send('/api/appversion/isready')
2235 return result == '1'
2237 def StartServing(self):
2238 """Start serving with the newly created version.
2240 Raises:
2241 RuntimeError: Deploy has not yet been called.
2243 Returns:
2244 The response body, as a string.
2246 assert self.deployed, 'Deploy() must be called before StartServing().'
2248 StatusUpdate('Deployment successful.', self.error_fh)
2249 self.params['willcheckserving'] = '1'
2250 result = self.logging_context.Send('/api/appversion/startserving')
2251 del self.params['willcheckserving']
2252 self.started = True
2253 return result
2255 @staticmethod
2256 def _ValidateIsServingYaml(resp):
2257 """Validates the given /isserving YAML string.
2259 Args:
2260 resp: the response from an RPC to a URL such as /api/appversion/isserving.
2262 Returns:
2263 The resulting dictionary if the response is valid, or None otherwise.
2265 response_dict = yaml.safe_load(resp)
2266 if 'serving' not in response_dict:
2267 return None
2268 return response_dict
2270 def IsServing(self):
2271 """Check if the new app version is serving.
2273 Raises:
2274 RuntimeError: Deploy has not yet been called.
2275 CannotStartServingError: A bad response was received from the isserving
2276 API call.
2278 Returns:
2279 (serving, response) Where serving is True if the deployed app version is
2280 serving, False otherwise. response is a dict containing the parsed
2281 response from the server, or an empty dict if the server's response was
2282 an old style 0/1 response.
2284 assert self.started, 'StartServing() must be called before IsServing().'
2286 StatusUpdate('Checking if updated app version is serving.', self.error_fh)
2288 self.params['new_serving_resp'] = '1'
2289 result = self.logging_context.Send('/api/appversion/isserving')
2290 del self.params['new_serving_resp']
2291 if result in ['0', '1']:
2292 return result == '1', {}
2293 result = AppVersionUpload._ValidateIsServingYaml(result)
2294 if not result:
2295 raise CannotStartServingError(
2296 'Internal error: Could not parse IsServing response.')
2297 message = result.get('message')
2298 fatal = result.get('fatal')
2299 if message:
2300 StatusUpdate(message, self.error_fh)
2301 if fatal:
2302 raise CannotStartServingError(message or 'Unknown error.')
2303 return result['serving'], result
2305 @staticmethod
2306 def _ValidateIsEndpointsConfigUpdatedYaml(resp):
2307 """Validates the YAML string response from an isconfigupdated request.
2309 Args:
2310 resp: A string containing the response from the server.
2312 Returns:
2313 The dictionary with the parsed response if the response is valid.
2314 Otherwise returns False.
2316 response_dict = yaml.safe_load(resp)
2318 if 'updated' not in response_dict and 'updatedDetail' not in response_dict:
2319 return None
2320 return response_dict
2322 def GetLogUrl(self):
2323 """Get the URL for the app's logs."""
2324 module = '%s:' % self.module if self.module else ''
2325 return ('https://appengine.google.com/logs?' +
2326 urllib.urlencode((('app_id', self.app_id),
2327 ('version_id', module + self.version))))
2329 def IsEndpointsConfigUpdated(self):
2330 """Check if the Endpoints configuration for this app has been updated.
2332 This should only be called if the app has a Google Cloud Endpoints
2333 handler, or if it's removing one. The server performs the check to see
2334 if Endpoints support is added/updated/removed, and the response to the
2335 isserving call indicates whether IsEndpointsConfigUpdated should be called.
2337 Raises:
2338 AssertionError: Deploy has not yet been called.
2339 CannotStartServingError: There was an unexpected error with the server
2340 response.
2342 Returns:
2343 (done, updated_state), where done is False if this function should
2344 be called again to retry, True if not. updated_state is an
2345 EndpointsState value indicating whether the Endpoints configuration has
2346 been updated on the server.
2349 assert self.started, ('StartServing() must be called before '
2350 'IsEndpointsConfigUpdated().')
2352 StatusUpdate('Checking if Endpoints configuration has been updated.',
2353 self.error_fh)
2355 result = self.logging_context.Send('/api/isconfigupdated')
2356 result = AppVersionUpload._ValidateIsEndpointsConfigUpdatedYaml(result)
2357 if result is None:
2358 raise CannotStartServingError(
2359 'Internal error: Could not parse IsEndpointsConfigUpdated response.')
2360 if 'updatedDetail' in result:
2361 updated_state = EndpointsState.Parse(result['updatedDetail'])
2362 else:
2368 updated_state = (EndpointsState.SERVING if result['updated']
2369 else EndpointsState.PENDING)
2370 return updated_state != EndpointsState.PENDING, updated_state
2372 def Rollback(self, force_rollback=False):
2373 """Rolls back the transaction if one is in progress."""
2374 if not self.in_transaction:
2375 return
2376 msg = 'Rolling back the update.'
2377 if self.config.vm and not force_rollback:
2378 msg += (' This can sometimes take a while since a VM version is being '
2379 'rolled back.')
2380 StatusUpdate(msg, self.error_fh)
2381 self.logging_context.Send('/api/appversion/rollback',
2382 force_rollback='1' if force_rollback else '0')
2383 self.in_transaction = False
2384 self.files = {}
2386 def DoUpload(self, paths, openfunc):
2387 """Uploads a new appversion with the given config and files to the server.
2389 Args:
2390 paths: An iterator that yields the relative paths of the files to upload.
2391 openfunc: A function that takes a path and returns a file-like object.
2393 Returns:
2394 An appinfo.AppInfoSummary if one was returned from the server, None
2395 otherwise.
2397 start_time_usec = self.logging_context.GetCurrentTimeUsec()
2398 logging.info('Reading app configuration.')
2400 StatusUpdate('\nStarting update of %s' % self.Describe(), self.error_fh)
2403 path = ''
2404 try:
2405 self.resource_limits = GetResourceLimits(self.logging_context,
2406 self.error_fh)
2407 self._AddFilesThatAreSmallEnough(paths, openfunc)
2408 except KeyboardInterrupt:
2409 logging.info('User interrupted. Aborting.')
2410 raise
2411 except EnvironmentError, e:
2412 if self._IsExceptionClientDeployLoggable(e):
2413 self.logging_context.LogClientDeploy(self.config.runtime,
2414 start_time_usec, False)
2415 logging.error('An error occurred processing file \'%s\': %s. Aborting.',
2416 path, e)
2417 raise
2419 try:
2420 missing_files = self.Begin()
2421 self._UploadMissingFiles(missing_files, openfunc)
2424 if (self.config.derived_file_type and
2425 appinfo.PYTHON_PRECOMPILED in self.config.derived_file_type):
2426 try:
2427 self.Precompile()
2428 except urllib2.HTTPError, e:
2429 ErrorUpdate('Error %d: --- begin server output ---\n'
2430 '%s\n--- end server output ---' %
2431 (e.code, e.read().rstrip('\n')))
2432 if e.code == 422 or self.config.GetEffectiveRuntime() == 'go':
2439 raise
2440 print >>self.error_fh, (
2441 'Precompilation failed. Your app can still serve but may '
2442 'have reduced startup performance. You can retry the update '
2443 'later to retry the precompilation step.')
2446 app_summary = self.Commit()
2447 StatusUpdate('Completed update of %s' % self.Describe(), self.error_fh)
2448 self.logging_context.LogClientDeploy(self.config.runtime, start_time_usec,
2449 True)
2450 except BaseException, e:
2451 try:
2452 self._LogDoUploadException(e)
2453 self.Rollback()
2454 finally:
2455 if self._IsExceptionClientDeployLoggable(e):
2456 self.logging_context.LogClientDeploy(self.config.runtime,
2457 start_time_usec, False)
2459 raise
2461 logging.info('Done!')
2462 return app_summary
2464 def _IsExceptionClientDeployLoggable(self, exception):
2465 """Determines if an exception qualifes for client deploy log reistration.
2467 Args:
2468 exception: The exception to check.
2470 Returns:
2471 True iff exception qualifies for client deploy logging - basically a
2472 system error rather than a user or error or cancellation.
2475 if isinstance(exception, KeyboardInterrupt):
2476 return False
2478 if (isinstance(exception, urllib2.HTTPError)
2479 and 400 <= exception.code <= 499):
2480 return False
2482 return True
2484 def _AddFilesThatAreSmallEnough(self, paths, openfunc):
2485 """Calls self.AddFile on files that are small enough.
2487 By small enough, we mean that their size is within
2488 self.resource_limits['max_file_size'] for application files, and
2489 'max_blob_size' otherwise. Files that are too large are logged as errors,
2490 and dropped (not sure why this isn't handled by raising an exception...).
2492 Args:
2493 paths: List of paths, relative to the app's base path.
2494 openfunc: A function that takes a paths element, and returns a file-like
2495 object.
2497 StatusUpdate('Scanning files on local disk.', self.error_fh)
2498 num_files = 0
2499 for path in paths:
2500 file_handle = openfunc(path)
2501 try:
2502 file_length = GetFileLength(file_handle)
2505 file_classification = FileClassification(
2506 self.config, path, self.error_fh)
2507 if file_classification.IsApplicationFile():
2508 max_size = self.resource_limits['max_file_size']
2509 else:
2510 max_size = self.resource_limits['max_blob_size']
2513 if file_length > max_size:
2514 extra_msg = (' Consider --enable_jar_splitting.'
2515 if JavaSupported() and path.endswith('jar')
2516 else '')
2517 logging.error('Ignoring file \'%s\': Too long '
2518 '(max %d bytes, file is %d bytes).%s',
2519 path, max_size, file_length, extra_msg)
2520 else:
2521 logging.info('Processing file \'%s\'', path)
2522 self.AddFile(path, file_handle)
2523 finally:
2524 file_handle.close()
2527 num_files += 1
2528 if num_files % 500 == 0:
2529 StatusUpdate('Scanned %d files.' % num_files, self.error_fh)
2531 def _UploadMissingFiles(self, missing_files, openfunc):
2532 """DoUpload helper to upload files that need to be uploaded.
2534 Args:
2535 missing_files: List of files that need to be uploaded. Begin returns such
2536 a list. Design note: we don't call Begin here, because we want DoUpload
2537 to call it directly so that Begin/Commit are more clearly paired.
2538 openfunc: Function that takes a path relative to the app's base path, and
2539 returns a file-like object.
2541 if not missing_files:
2542 return
2544 StatusUpdate('Uploading %d files and blobs.' % len(missing_files),
2545 self.error_fh)
2546 num_files = 0
2547 for missing_file in missing_files:
2548 file_handle = openfunc(missing_file)
2549 try:
2550 self.UploadFile(missing_file, file_handle)
2551 finally:
2552 file_handle.close()
2555 num_files += 1
2556 if num_files % 500 == 0:
2557 StatusUpdate('Processed %d out of %s.' %
2558 (num_files, len(missing_files)), self.error_fh)
2561 self.file_batcher.Flush()
2562 self.blob_batcher.Flush()
2563 self.errorblob_batcher.Flush()
2564 StatusUpdate('Uploaded %d files and blobs' % num_files, self.error_fh)
2566 @staticmethod
2567 def _LogDoUploadException(exception):
2568 """Helper that logs exceptions that occurred during DoUpload.
2570 Args:
2571 exception: An exception that was thrown during DoUpload.
2573 def InstanceOf(tipe):
2574 return isinstance(exception, tipe)
2576 if InstanceOf(KeyboardInterrupt):
2577 logging.info('User interrupted. Aborting.')
2578 elif InstanceOf(urllib2.HTTPError):
2579 logging.info('HTTP Error (%s)', exception)
2580 elif InstanceOf(CannotStartServingError):
2581 logging.error(exception.message)
2582 else:
2583 logging.exception('An unexpected error occurred. Aborting.')
2586 class DoLockAction(object):
2587 """Locks/unlocks a particular vm app version and shows state."""
2589 def __init__(
2590 self, url, rpcserver, app_id, version, module, instance, file_handle):
2591 self.url = url
2592 self.rpcserver = rpcserver
2593 self.app_id = app_id
2594 self.version = version
2595 self.module = module
2596 self.instance = instance
2597 self.file_handle = file_handle
2599 def GetState(self):
2600 yaml_data = self.rpcserver.Send('/api/vms/debugstate',
2601 app_id=self.app_id,
2602 version_match=self.version,
2603 module=self.module)
2604 state = yaml.safe_load(yaml_data)
2605 done = state['state'] != 'PENDING'
2606 if done:
2607 print >> self.file_handle, state['message']
2608 return (done, state['message'])
2610 def PrintRetryMessage(self, msg, delay):
2611 StatusUpdate('%s. Will try again in %d seconds.' % (msg, delay),
2612 self.file_handle)
2614 def Do(self):
2615 kwargs = {'app_id': self.app_id,
2616 'version_match': self.version,
2617 'module': self.module}
2618 if self.instance:
2619 kwargs['instance'] = self.instance
2621 response = self.rpcserver.Send(self.url, **kwargs)
2622 print >> self.file_handle, response
2623 RetryWithBackoff(self.GetState, self.PrintRetryMessage, 1, 2, 5, 20)
2626 def FileIterator(base, skip_files, runtime, separator=os.path.sep):
2627 """Walks a directory tree, returning all the files. Follows symlinks.
2629 Args:
2630 base: The base path to search for files under.
2631 skip_files: A regular expression object for files/directories to skip.
2632 runtime: The name of the runtime e.g. "python". If "python27" then .pyc
2633 files with matching .py files will be skipped.
2634 separator: Path separator used by the running system's platform.
2636 Yields:
2637 Paths of files found, relative to base.
2639 dirs = ['']
2640 while dirs:
2641 current_dir = dirs.pop()
2642 entries = set(os.listdir(os.path.join(base, current_dir)))
2643 for entry in sorted(entries):
2644 name = os.path.join(current_dir, entry)
2645 fullname = os.path.join(base, name)
2650 if separator == '\\':
2651 name = name.replace('\\', '/')
2653 if runtime == 'python27' and not skip_files.match(name):
2654 root, extension = os.path.splitext(entry)
2655 if extension == '.pyc' and (root + '.py') in entries:
2656 logging.warning('Ignoring file \'%s\': Cannot upload both '
2657 '<filename>.py and <filename>.pyc', name)
2658 continue
2660 if os.path.isfile(fullname):
2661 if skip_files.match(name):
2662 logging.info('Ignoring file \'%s\': File matches ignore regex.', name)
2663 else:
2664 yield name
2665 elif os.path.isdir(fullname):
2666 if skip_files.match(name):
2667 logging.info(
2668 'Ignoring directory \'%s\': Directory matches ignore regex.',
2669 name)
2670 else:
2671 dirs.append(name)
2674 def GetFileLength(fh):
2675 """Returns the length of the file represented by fh.
2677 This function is capable of finding the length of any seekable stream,
2678 unlike os.fstat, which only works on file streams.
2680 Args:
2681 fh: The stream to get the length of.
2683 Returns:
2684 The length of the stream.
2686 pos = fh.tell()
2688 fh.seek(0, 2)
2689 length = fh.tell()
2690 fh.seek(pos, 0)
2691 return length
2694 def GetUserAgent(get_version=sdk_update_checker.GetVersionObject,
2695 get_platform=appengine_rpc.GetPlatformToken,
2696 sdk_product=SDK_PRODUCT):
2697 """Determines the value of the 'User-agent' header to use for HTTP requests.
2699 If the 'APPCFG_SDK_NAME' environment variable is present, that will be
2700 used as the first product token in the user-agent.
2702 Args:
2703 get_version: Used for testing.
2704 get_platform: Used for testing.
2705 sdk_product: Used as part of sdk/version product token.
2707 Returns:
2708 String containing the 'user-agent' header value, which includes the SDK
2709 version, the platform information, and the version of Python;
2710 e.g., 'appcfg_py/1.0.1 Darwin/9.2.0 Python/2.5.2'.
2712 product_tokens = []
2715 sdk_name = os.environ.get('APPCFG_SDK_NAME')
2716 if sdk_name:
2717 product_tokens.append(sdk_name)
2718 else:
2719 version = get_version()
2720 if version is None:
2721 release = 'unknown'
2722 else:
2723 release = version['release']
2725 product_tokens.append('%s/%s' % (sdk_product, release))
2728 product_tokens.append(get_platform())
2731 python_version = '.'.join(str(i) for i in sys.version_info)
2732 product_tokens.append('Python/%s' % python_version)
2734 return ' '.join(product_tokens)
2737 def GetSourceName(get_version=sdk_update_checker.GetVersionObject):
2738 """Gets the name of this source version."""
2739 version = get_version()
2740 if version is None:
2741 release = 'unknown'
2742 else:
2743 release = version['release']
2744 return 'Google-appcfg-%s' % (release,)
2747 def _ReadUrlContents(url):
2748 """Reads the contents of a URL into a string.
2750 Args:
2751 url: a string that is the URL to read.
2753 Returns:
2754 A string that is the contents read from the URL.
2756 Raises:
2757 urllib2.URLError: If the URL cannot be read.
2759 req = urllib2.Request(url)
2760 return urllib2.urlopen(req).read()
2763 class AppCfgApp(object):
2764 """Singleton class to wrap AppCfg tool functionality.
2766 This class is responsible for parsing the command line and executing
2767 the desired action on behalf of the user. Processing files and
2768 communicating with the server is handled by other classes.
2770 Attributes:
2771 actions: A dictionary mapping action names to Action objects.
2772 action: The Action specified on the command line.
2773 parser: An instance of optparse.OptionParser.
2774 options: The command line options parsed by 'parser'.
2775 argv: The original command line as a list.
2776 args: The positional command line args left over after parsing the options.
2777 raw_input_fn: Function used for getting raw user input, like email.
2778 password_input_fn: Function used for getting user password.
2779 error_fh: Unexpected HTTPErrors are printed to this file handle.
2781 Attributes for testing:
2782 parser_class: The class to use for parsing the command line. Because
2783 OptionsParser will exit the program when there is a parse failure, it
2784 is nice to subclass OptionsParser and catch the error before exiting.
2785 read_url_contents: A function to read the contents of a URL.
2788 def __init__(self, argv, parser_class=optparse.OptionParser,
2789 rpc_server_class=None,
2790 raw_input_fn=raw_input,
2791 password_input_fn=getpass.getpass,
2792 out_fh=sys.stdout,
2793 error_fh=sys.stderr,
2794 update_check_class=sdk_update_checker.SDKUpdateChecker,
2795 throttle_class=None,
2796 opener=open,
2797 file_iterator=FileIterator,
2798 time_func=time.time,
2799 wrap_server_error_message=True,
2800 oauth_client_id=APPCFG_CLIENT_ID,
2801 oauth_client_secret=APPCFG_CLIENT_NOTSOSECRET,
2802 oauth_scopes=APPCFG_SCOPES):
2803 """Initializer. Parses the cmdline and selects the Action to use.
2805 Initializes all of the attributes described in the class docstring.
2806 Prints help or error messages if there is an error parsing the cmdline.
2808 Args:
2809 argv: The list of arguments passed to this program.
2810 parser_class: Options parser to use for this application.
2811 rpc_server_class: RPC server class to use for this application.
2812 raw_input_fn: Function used for getting user email.
2813 password_input_fn: Function used for getting user password.
2814 out_fh: All normal output is printed to this file handle.
2815 error_fh: Unexpected HTTPErrors are printed to this file handle.
2816 update_check_class: sdk_update_checker.SDKUpdateChecker class (can be
2817 replaced for testing).
2818 throttle_class: A class to use instead of ThrottledHttpRpcServer
2819 (only used in the bulkloader).
2820 opener: Function used for opening files.
2821 file_iterator: Callable that takes (basepath, skip_files, file_separator)
2822 and returns a generator that yields all filenames in the file tree
2823 rooted at that path, skipping files that match the skip_files compiled
2824 regular expression.
2825 time_func: A time.time() compatible function, which can be overridden for
2826 testing.
2827 wrap_server_error_message: If true, the error messages from
2828 urllib2.HTTPError exceptions in Run() are wrapped with
2829 '--- begin server output ---' and '--- end server output ---',
2830 otherwise the error message is printed as is.
2831 oauth_client_id: The client ID of the project providing Auth. Defaults to
2832 the SDK default project client ID, the constant APPCFG_CLIENT_ID.
2833 oauth_client_secret: The client secret of the project providing Auth.
2834 Defaults to the SDK default project client secret, the constant
2835 APPCFG_CLIENT_NOTSOSECRET.
2836 oauth_scopes: The scope or set of scopes to be accessed by the OAuth2
2837 token retrieved. Defaults to APPCFG_SCOPES. Can be a string or
2838 iterable of strings, representing the scope(s) to request.
2840 self.parser_class = parser_class
2841 self.argv = argv
2842 self.rpc_server_class = rpc_server_class
2843 self.raw_input_fn = raw_input_fn
2844 self.password_input_fn = password_input_fn
2845 self.out_fh = out_fh
2846 self.error_fh = error_fh
2847 self.update_check_class = update_check_class
2848 self.throttle_class = throttle_class
2849 self.time_func = time_func
2850 self.wrap_server_error_message = wrap_server_error_message
2851 self.oauth_client_id = oauth_client_id
2852 self.oauth_client_secret = oauth_client_secret
2853 self.oauth_scopes = oauth_scopes
2855 self.read_url_contents = _ReadUrlContents
2861 self.parser = self._GetOptionParser()
2862 for action in self.actions.itervalues():
2863 action.options(self, self.parser)
2866 self.options, self.args = self.parser.parse_args(argv[1:])
2868 if len(self.args) < 1:
2869 self._PrintHelpAndExit()
2871 if not self.options.allow_any_runtime:
2872 if self.options.runtime:
2873 if self.options.runtime not in SUPPORTED_RUNTIMES:
2874 _PrintErrorAndExit(self.error_fh,
2875 '"%s" is not a supported runtime\n' %
2876 self.options.runtime)
2877 else:
2878 appinfo.AppInfoExternal.ATTRIBUTES[appinfo.RUNTIME] = (
2879 '|'.join(SUPPORTED_RUNTIMES))
2881 action = self.args.pop(0)
2883 def RaiseParseError(actionname, action):
2886 self.parser, self.options = self._MakeSpecificParser(action)
2887 error_desc = action.error_desc
2888 if not error_desc:
2889 error_desc = "Expected a <directory> argument after '%s'." % (
2890 actionname.split(' ')[0])
2891 self.parser.error(error_desc)
2896 if action == BACKENDS_ACTION:
2897 if len(self.args) < 1:
2898 RaiseParseError(action, self.actions[BACKENDS_ACTION])
2900 backend_action_first = BACKENDS_ACTION + ' ' + self.args[0]
2901 if backend_action_first in self.actions:
2902 self.args.pop(0)
2903 action = backend_action_first
2905 elif len(self.args) > 1:
2906 backend_directory_first = BACKENDS_ACTION + ' ' + self.args[1]
2907 if backend_directory_first in self.actions:
2908 self.args.pop(1)
2909 action = backend_directory_first
2912 if len(self.args) < 1 or action == BACKENDS_ACTION:
2913 RaiseParseError(action, self.actions[action])
2915 if action not in self.actions:
2916 self.parser.error("Unknown action: '%s'\n%s" %
2917 (action, self.parser.get_description()))
2920 self.action = self.actions[action]
2925 if not self.action.uses_basepath or self.options.help:
2926 self.basepath = None
2927 else:
2928 if not self.args:
2929 RaiseParseError(action, self.action)
2930 self.basepath = self.args.pop(0)
2936 self.parser, self.options = self._MakeSpecificParser(self.action)
2940 if self.options.help:
2941 self._PrintHelpAndExit()
2943 if self.options.verbose == 2:
2944 logging.getLogger().setLevel(logging.INFO)
2945 elif self.options.verbose == 3:
2946 logging.getLogger().setLevel(logging.DEBUG)
2951 global verbosity
2952 verbosity = self.options.verbose
2956 if any((self.options.oauth2_refresh_token, self.options.oauth2_access_token,
2957 self.options.authenticate_service_account)):
2958 self.options.oauth2 = True
2961 if self.options.oauth2_client_id:
2962 self.oauth_client_id = self.options.oauth2_client_id
2963 if self.options.oauth2_client_secret:
2964 self.oauth_client_secret = self.options.oauth2_client_secret
2969 self.opener = opener
2970 self.file_iterator = file_iterator
2972 def Run(self):
2973 """Executes the requested action.
2975 Catches any HTTPErrors raised by the action and prints them to stderr.
2977 Returns:
2978 1 on error, 0 if successful.
2980 try:
2981 self.action(self)
2982 except urllib2.HTTPError, e:
2983 body = e.read()
2984 if self.wrap_server_error_message:
2985 error_format = ('Error %d: --- begin server output ---\n'
2986 '%s\n--- end server output ---')
2987 else:
2988 error_format = 'Error %d: %s'
2990 print >>self.error_fh, (error_format % (e.code, body.rstrip('\n')))
2991 return 1
2992 except yaml_errors.EventListenerError, e:
2993 print >>self.error_fh, ('Error parsing yaml file:\n%s' % e)
2994 return 1
2995 except CannotStartServingError:
2996 print >>self.error_fh, 'Could not start serving the given version.'
2997 return 1
2998 return 0
3000 def _GetActionDescriptions(self):
3001 """Returns a formatted string containing the short_descs for all actions."""
3002 action_names = self.actions.keys()
3003 action_names.sort()
3004 desc = ''
3005 for action_name in action_names:
3006 if not self.actions[action_name].hidden:
3007 desc += ' %s: %s\n' % (action_name,
3008 self.actions[action_name].short_desc)
3009 return desc
3011 def _GetOptionParser(self):
3012 """Creates an OptionParser with generic usage and description strings.
3014 Returns:
3015 An OptionParser instance.
3018 class Formatter(optparse.IndentedHelpFormatter):
3019 """Custom help formatter that does not reformat the description."""
3021 def format_description(self, description):
3022 """Very simple formatter."""
3023 return description + '\n'
3025 class AppCfgOption(optparse.Option):
3026 """Custom Option for AppCfg.
3028 Adds an 'update' action for storing key-value pairs as a dict.
3031 _ACTION = 'update'
3032 ACTIONS = optparse.Option.ACTIONS + (_ACTION,)
3033 STORE_ACTIONS = optparse.Option.STORE_ACTIONS + (_ACTION,)
3034 TYPED_ACTIONS = optparse.Option.TYPED_ACTIONS + (_ACTION,)
3035 ALWAYS_TYPED_ACTIONS = optparse.Option.ALWAYS_TYPED_ACTIONS + (_ACTION,)
3037 def take_action(self, action, dest, opt, value, values, parser):
3038 if action != self._ACTION:
3039 return optparse.Option.take_action(
3040 self, action, dest, opt, value, values, parser)
3041 try:
3042 key, value = value.split(':', 1)
3043 except ValueError:
3044 raise optparse.OptionValueError(
3045 'option %s: invalid value: %s (must match NAME:VALUE)' % (
3046 opt, value))
3047 values.ensure_value(dest, {})[key] = value
3049 desc = self._GetActionDescriptions()
3050 desc = ('Action must be one of:\n%s'
3051 'Use \'help <action>\' for a detailed description.') % desc
3055 parser = self.parser_class(usage='%prog [options] <action>',
3056 description=desc,
3057 formatter=Formatter(),
3058 conflict_handler='resolve',
3059 option_class=AppCfgOption)
3064 parser.add_option('-h', '--help', action='store_true',
3065 dest='help', help='Show the help message and exit.')
3066 parser.add_option('-q', '--quiet', action='store_const', const=0,
3067 dest='verbose', help='Print errors only.')
3068 parser.add_option('-v', '--verbose', action='store_const', const=2,
3069 dest='verbose', default=1,
3070 help='Print info level logs.')
3071 parser.add_option('--noisy', action='store_const', const=3,
3072 dest='verbose', help='Print all logs.')
3073 parser.add_option('-s', '--server', action='store', dest='server',
3074 default='appengine.google.com',
3075 metavar='SERVER', help='The App Engine server.')
3076 parser.add_option('--secure', action='store_true', dest='secure',
3077 default=True, help=optparse.SUPPRESS_HELP)
3078 parser.add_option('--ignore_bad_cert', action='store_true',
3079 dest='ignore_certs', default=False,
3080 help=optparse.SUPPRESS_HELP)
3081 parser.add_option('--insecure', action='store_false', dest='secure',
3082 help=optparse.SUPPRESS_HELP)
3083 parser.add_option('-e', '--email', action='store', dest='email',
3084 metavar='EMAIL', default=None,
3085 help='The username to use. Will prompt if omitted.')
3086 parser.add_option('-H', '--host', action='store', dest='host',
3087 metavar='HOST', default=None,
3088 help='Overrides the Host header sent with all RPCs.')
3089 parser.add_option('--no_cookies', action='store_false',
3090 dest='save_cookies', default=True,
3091 help='Do not save authentication cookies to local disk.')
3092 parser.add_option('--skip_sdk_update_check', action='store_true',
3093 dest='skip_sdk_update_check', default=False,
3094 help='Do not check for SDK updates.')
3095 parser.add_option('--passin', action='store_true',
3096 dest='passin', default=False,
3097 help='Read the login password from stdin.')
3098 parser.add_option('-A', '--application', action='store', dest='app_id',
3099 help=('Set the application, overriding the application '
3100 'value from app.yaml file.'))
3101 parser.add_option('-M', '--module', action='store', dest='module',
3102 help=('Set the module, overriding the module value '
3103 'from app.yaml.'))
3104 parser.add_option('-V', '--version', action='store', dest='version',
3105 help=('Set the (major) version, overriding the version '
3106 'value from app.yaml file.'))
3107 parser.add_option('-r', '--runtime', action='store', dest='runtime',
3108 help='Override runtime from app.yaml file.')
3109 parser.add_option('-E', '--env_variable', action='update',
3110 dest='env_variables', metavar='NAME:VALUE',
3111 help=('Set an environment variable, potentially '
3112 'overriding an env_variable value from app.yaml '
3113 'file (flag may be repeated to set multiple '
3114 'variables).'))
3115 parser.add_option('-R', '--allow_any_runtime', action='store_true',
3116 dest='allow_any_runtime', default=False,
3117 help='Do not validate the runtime in app.yaml')
3118 parser.add_option('--oauth2', action='store_true', dest='oauth2',
3119 default=False,
3120 help='Use OAuth2 instead of password auth.')
3121 parser.add_option('--oauth2_refresh_token', action='store',
3122 dest='oauth2_refresh_token', default=None,
3123 help='An existing OAuth2 refresh token to use. Will '
3124 'not attempt interactive OAuth approval.')
3125 parser.add_option('--oauth2_access_token', action='store',
3126 dest='oauth2_access_token', default=None,
3127 help='An existing OAuth2 access token to use. Will '
3128 'not attempt interactive OAuth approval.')
3129 parser.add_option('--oauth2_client_id', action='store',
3130 dest='oauth2_client_id', default=None,
3131 help=optparse.SUPPRESS_HELP)
3132 parser.add_option('--oauth2_client_secret', action='store',
3133 dest='oauth2_client_secret', default=None,
3134 help=optparse.SUPPRESS_HELP)
3135 parser.add_option('--oauth2_credential_file', action='store',
3136 dest='oauth2_credential_file', default=None,
3137 help=optparse.SUPPRESS_HELP)
3138 parser.add_option('--authenticate_service_account', action='store_true',
3139 dest='authenticate_service_account', default=False,
3140 help='Authenticate using the default service account '
3141 'for the Google Compute Engine VM in which appcfg is '
3142 'being called')
3143 parser.add_option('--noauth_local_webserver', action='store_false',
3144 dest='auth_local_webserver', default=True,
3145 help='Do not run a local web server to handle redirects '
3146 'during OAuth authorization.')
3147 parser.add_option('--called_by_gcloud',
3148 action='store_true', default=False,
3149 help=optparse.SUPPRESS_HELP)
3150 return parser
3152 def _MakeSpecificParser(self, action):
3153 """Creates a new parser with documentation specific to 'action'.
3155 Args:
3156 action: An Action instance to be used when initializing the new parser.
3158 Returns:
3159 A tuple containing:
3160 parser: An instance of OptionsParser customized to 'action'.
3161 options: The command line options after re-parsing.
3163 parser = self._GetOptionParser()
3164 parser.set_usage(action.usage)
3165 parser.set_description('%s\n%s' % (action.short_desc, action.long_desc))
3166 action.options(self, parser)
3167 options, unused_args = parser.parse_args(self.argv[1:])
3168 return parser, options
3170 def _PrintHelpAndExit(self, exit_code=2):
3171 """Prints the parser's help message and exits the program.
3173 Args:
3174 exit_code: The integer code to pass to sys.exit().
3176 self.parser.print_help()
3177 sys.exit(exit_code)
3179 def _GetRpcServer(self):
3180 """Returns an instance of an AbstractRpcServer.
3182 Returns:
3183 A new AbstractRpcServer, on which RPC calls can be made.
3185 Raises:
3186 OAuthNotAvailable: OAuth is requested but the dependecies aren't imported.
3187 RuntimeError: The user has request non-interactive authentication but the
3188 environment is not correct for that to work.
3191 def GetUserCredentials():
3192 """Prompts the user for a username and password."""
3193 email = self.options.email
3194 if email is None:
3195 email = self.raw_input_fn('Email: ')
3197 password_prompt = 'Password for %s: ' % email
3200 if self.options.passin:
3201 password = self.raw_input_fn(password_prompt)
3202 else:
3203 password = self.password_input_fn(password_prompt)
3205 return (email, password)
3207 StatusUpdate('Host: %s' % self.options.server, self.error_fh)
3209 source = GetSourceName()
3213 dev_appserver = self.options.host == 'localhost'
3214 if self.options.oauth2 and not dev_appserver:
3215 if not appengine_rpc_httplib2:
3217 raise OAuthNotAvailable()
3218 if not self.rpc_server_class:
3219 self.rpc_server_class = appengine_rpc_httplib2.HttpRpcServerOAuth2
3222 get_user_credentials = (
3223 appengine_rpc_httplib2.HttpRpcServerOAuth2.OAuth2Parameters(
3224 access_token=self.options.oauth2_access_token,
3225 client_id=self.oauth_client_id,
3226 client_secret=self.oauth_client_secret,
3227 scope=self.oauth_scopes,
3228 refresh_token=self.options.oauth2_refresh_token,
3229 credential_file=self.options.oauth2_credential_file,
3230 token_uri=self._GetTokenUri()))
3232 if hasattr(appengine_rpc_httplib2.tools, 'FLAGS'):
3233 appengine_rpc_httplib2.tools.FLAGS.auth_local_webserver = (
3234 self.options.auth_local_webserver)
3235 else:
3236 if not self.rpc_server_class:
3237 self.rpc_server_class = appengine_rpc.HttpRpcServerWithOAuth2Suggestion
3238 if hasattr(self, 'runtime'):
3239 self.rpc_server_class.RUNTIME = self.runtime
3240 get_user_credentials = GetUserCredentials
3243 if dev_appserver:
3244 email = self.options.email
3245 if email is None:
3246 email = 'test@example.com'
3247 logging.info('Using debug user %s. Override with --email', email)
3248 rpcserver = self.rpc_server_class(
3249 self.options.server,
3250 lambda: (email, 'password'),
3251 GetUserAgent(),
3252 source,
3253 host_override=self.options.host,
3254 save_cookies=self.options.save_cookies,
3256 secure=False)
3258 rpcserver.authenticated = True
3259 return rpcserver
3262 if self.options.passin:
3263 auth_tries = 1
3264 else:
3265 auth_tries = 3
3267 return self.rpc_server_class(self.options.server, get_user_credentials,
3268 GetUserAgent(), source,
3269 host_override=self.options.host,
3270 save_cookies=self.options.save_cookies,
3271 auth_tries=auth_tries,
3272 account_type='HOSTED_OR_GOOGLE',
3273 secure=self.options.secure,
3274 ignore_certs=self.options.ignore_certs)
3276 def _GetTokenUri(self):
3277 """Returns the OAuth2 token_uri, or None to use the default URI.
3279 Returns:
3280 A string that is the token_uri, or None.
3282 Raises:
3283 RuntimeError: The user has requested authentication for a service account
3284 but the environment is not correct for that to work.
3286 if self.options.authenticate_service_account:
3290 url = '%s/%s/scopes' % (METADATA_BASE, SERVICE_ACCOUNT_BASE)
3291 try:
3292 vm_scopes_string = self.read_url_contents(url)
3293 except urllib2.URLError, e:
3294 raise RuntimeError('Could not obtain scope list from metadata service: '
3295 '%s: %s. This may be because we are not running in '
3296 'a Google Compute Engine VM.' % (url, e))
3297 vm_scopes = vm_scopes_string.split()
3298 missing = list(set(self.oauth_scopes).difference(vm_scopes))
3299 if missing:
3300 raise RuntimeError('Required scopes %s missing from %s. '
3301 'This VM instance probably needs to be recreated '
3302 'with the missing scopes.' % (missing, vm_scopes))
3303 return '%s/%s/token' % (METADATA_BASE, SERVICE_ACCOUNT_BASE)
3304 else:
3305 return None
3307 def _FindYaml(self, basepath, file_name):
3308 """Find yaml files in application directory.
3310 Args:
3311 basepath: Base application directory.
3312 file_name: Relative file path from basepath, without extension, to search
3313 for.
3315 Returns:
3316 Path to located yaml file if one exists, else None.
3318 if not os.path.isdir(basepath):
3319 self.parser.error('Not a directory: %s' % basepath)
3323 alt_basepath = os.path.join(basepath, 'WEB-INF', 'appengine-generated')
3325 for yaml_basepath in (basepath, alt_basepath):
3326 for yaml_file in (file_name + '.yaml', file_name + '.yml'):
3327 yaml_path = os.path.join(yaml_basepath, yaml_file)
3328 if os.path.isfile(yaml_path):
3329 return yaml_path
3331 return None
3333 def _ParseAppInfoFromYaml(self, basepath, basename='app'):
3334 """Parses the app.yaml file.
3336 Args:
3337 basepath: The directory of the application.
3338 basename: The relative file path, from basepath, to search for.
3340 Returns:
3341 An AppInfoExternal object.
3343 try:
3344 appyaml = self._ParseYamlFile(basepath, basename, appinfo_includes.Parse)
3345 except yaml_errors.EventListenerError, e:
3346 self.parser.error('Error parsing %s.yaml: %s.' % (
3347 os.path.join(basepath, basename), e))
3348 if not appyaml:
3349 if JavaSupported():
3350 if appcfg_java.IsWarFileWithoutYaml(basepath):
3351 java_app_update = appcfg_java.JavaAppUpdate(basepath, self.options)
3352 appyaml_string = java_app_update.GenerateAppYamlString([])
3353 appyaml = appinfo.LoadSingleAppInfo(appyaml_string)
3354 if not appyaml:
3355 self.parser.error('Directory contains neither an %s.yaml '
3356 'configuration file nor a WEB-INF subdirectory '
3357 'with web.xml and appengine-web.xml.' % basename)
3358 else:
3359 self.parser.error('Directory does not contain an %s.yaml configuration '
3360 'file' % basename)
3362 orig_application = appyaml.application
3363 orig_module = appyaml.module
3364 orig_version = appyaml.version
3365 if self.options.app_id:
3366 appyaml.application = self.options.app_id
3367 if self.options.module:
3368 appyaml.module = self.options.module
3369 if self.options.version:
3370 appyaml.version = self.options.version
3371 if self.options.runtime:
3372 appinfo.VmSafeSetRuntime(appyaml, self.options.runtime)
3373 if self.options.env_variables:
3374 if appyaml.env_variables is None:
3375 appyaml.env_variables = appinfo.EnvironmentVariables()
3376 appyaml.env_variables.update(self.options.env_variables)
3378 if not appyaml.application:
3379 self.parser.error('Expected -A app_id when application property in file '
3380 '%s.yaml is not set.' % basename)
3382 msg = 'Application: %s' % appyaml.application
3383 if appyaml.application != orig_application:
3384 msg += ' (was: %s)' % orig_application
3385 if self.action.function is 'Update':
3387 if (appyaml.module is not None and
3388 appyaml.module != appinfo.DEFAULT_MODULE):
3389 msg += '; module: %s' % appyaml.module
3390 if appyaml.module != orig_module:
3391 msg += ' (was: %s)' % orig_module
3392 msg += '; version: %s' % appyaml.version
3393 if appyaml.version != orig_version:
3394 msg += ' (was: %s)' % orig_version
3395 StatusUpdate(msg, self.error_fh)
3396 return appyaml
3398 def _ParseYamlFile(self, basepath, basename, parser):
3399 """Parses a yaml file.
3401 Args:
3402 basepath: The base directory of the application.
3403 basename: The relative file path, from basepath, (with the '.yaml'
3404 stripped off).
3405 parser: the function or method used to parse the file.
3407 Returns:
3408 A single parsed yaml file or None if the file does not exist.
3410 file_name = self._FindYaml(basepath, basename)
3411 if file_name is not None:
3412 fh = self.opener(file_name, 'r')
3413 try:
3414 defns = parser(fh, open_fn=self.opener)
3415 finally:
3416 fh.close()
3417 return defns
3418 return None
3420 def _ParseBackendsYaml(self, basepath):
3421 """Parses the backends.yaml file.
3423 Args:
3424 basepath: the directory of the application.
3426 Returns:
3427 A BackendsInfoExternal object or None if the file does not exist.
3429 return self._ParseYamlFile(basepath, 'backends',
3430 backendinfo.LoadBackendInfo)
3432 def _ParseIndexYaml(self, basepath, appyaml=None):
3433 """Parses the index.yaml file.
3435 Args:
3436 basepath: the directory of the application.
3437 appyaml: The app.yaml, if present.
3438 Returns:
3439 A single parsed yaml file or None if the file does not exist.
3441 index_yaml = self._ParseYamlFile(basepath,
3442 'index',
3443 datastore_index.ParseIndexDefinitions)
3444 if not index_yaml:
3445 return None
3446 self._SetApplication(index_yaml, 'index', appyaml)
3448 return index_yaml
3450 def _SetApplication(self, dest_yaml, basename, appyaml=None):
3451 """Parses and sets the application property onto the dest_yaml parameter.
3453 The order of precendence is:
3454 1. Command line (-A application)
3455 2. Specified dest_yaml file
3456 3. App.yaml file
3458 This exits with a parse error if application is not present in any of these
3459 locations.
3461 Args:
3462 dest_yaml: The yaml object to set 'application' on.
3463 basename: The name of the dest_yaml file for use in errors.
3464 appyaml: The already parsed appyaml, if present. If none, this method will
3465 attempt to parse app.yaml.
3467 if self.options.app_id:
3468 dest_yaml.application = self.options.app_id
3469 if not dest_yaml.application:
3470 if not appyaml:
3471 appyaml = self._ParseYamlFile(self.basepath,
3472 'app',
3473 appinfo_includes.Parse)
3474 if appyaml:
3475 dest_yaml.application = appyaml.application
3476 else:
3477 self.parser.error('Expected -A app_id when %s.yaml.application is not '
3478 'set and app.yaml is not present.' % basename)
3480 def _ParseCronYaml(self, basepath, appyaml=None):
3481 """Parses the cron.yaml file.
3483 Args:
3484 basepath: the directory of the application.
3485 appyaml: The app.yaml, if present.
3487 Returns:
3488 A CronInfoExternal object or None if the file does not exist.
3490 cron_yaml = self._ParseYamlFile(basepath, 'cron', croninfo.LoadSingleCron)
3491 if not cron_yaml:
3492 return None
3493 self._SetApplication(cron_yaml, 'cron', appyaml)
3495 return cron_yaml
3497 def _ParseQueueYaml(self, basepath, appyaml=None):
3498 """Parses the queue.yaml file.
3500 Args:
3501 basepath: the directory of the application.
3502 appyaml: The app.yaml, if present.
3504 Returns:
3505 A QueueInfoExternal object or None if the file does not exist.
3507 queue_yaml = self._ParseYamlFile(basepath,
3508 'queue',
3509 queueinfo.LoadSingleQueue)
3510 if not queue_yaml:
3511 return None
3513 self._SetApplication(queue_yaml, 'queue', appyaml)
3514 return queue_yaml
3516 def _ParseDispatchYaml(self, basepath, appyaml=None):
3517 """Parses the dispatch.yaml file.
3519 Args:
3520 basepath: the directory of the application.
3521 appyaml: The app.yaml, if present.
3523 Returns:
3524 A DispatchInfoExternal object or None if the file does not exist.
3526 dispatch_yaml = self._ParseYamlFile(basepath,
3527 'dispatch',
3528 dispatchinfo.LoadSingleDispatch)
3530 if not dispatch_yaml:
3531 return None
3533 self._SetApplication(dispatch_yaml, 'dispatch', appyaml)
3534 return dispatch_yaml
3536 def _ParseDosYaml(self, basepath, appyaml=None):
3537 """Parses the dos.yaml file.
3539 Args:
3540 basepath: the directory of the application.
3541 appyaml: The app.yaml, if present.
3543 Returns:
3544 A DosInfoExternal object or None if the file does not exist.
3546 dos_yaml = self._ParseYamlFile(basepath, 'dos', dosinfo.LoadSingleDos)
3547 if not dos_yaml:
3548 return None
3550 self._SetApplication(dos_yaml, 'dos', appyaml)
3551 return dos_yaml
3553 def Help(self, action=None):
3554 """Prints help for a specific action.
3556 Args:
3557 action: If provided, print help for the action provided.
3559 Expects self.args[0], or 'action', to contain the name of the action in
3560 question. Exits the program after printing the help message.
3562 if not action:
3563 if len(self.args) > 1:
3564 self.args = [' '.join(self.args)]
3566 if len(self.args) != 1 or self.args[0] not in self.actions:
3567 self.parser.error('Expected a single action argument. '
3568 ' Must be one of:\n' +
3569 self._GetActionDescriptions())
3570 action = self.args[0]
3571 action = self.actions[action]
3572 self.parser, unused_options = self._MakeSpecificParser(action)
3573 self._PrintHelpAndExit(exit_code=0)
3575 def DownloadApp(self):
3576 """Downloads the given app+version."""
3577 if len(self.args) != 1:
3578 self.parser.error('\"download_app\" expects one non-option argument, '
3579 'found ' + str(len(self.args)) + '.')
3581 out_dir = self.args[0]
3583 app_id = self.options.app_id
3584 if app_id is None:
3585 self.parser.error('You must specify an app ID via -A or --application.')
3587 module = self.options.module
3588 app_version = self.options.version
3592 if os.path.exists(out_dir):
3593 if not os.path.isdir(out_dir):
3594 self.parser.error('Cannot download to path "%s": '
3595 'there\'s a file in the way.' % out_dir)
3596 elif os.listdir(out_dir):
3597 self.parser.error('Cannot download to path "%s": directory already '
3598 'exists and it isn\'t empty.' % out_dir)
3600 rpcserver = self._GetRpcServer()
3602 DoDownloadApp(rpcserver, out_dir, app_id, module, app_version)
3604 def UpdateVersion(self, rpcserver, basepath, appyaml, module_yaml_path,
3605 backend=None):
3606 """Updates and deploys a new appversion.
3608 Args:
3609 rpcserver: An AbstractRpcServer instance on which RPC calls can be made.
3610 basepath: The root directory of the version to update.
3611 appyaml: The AppInfoExternal object parsed from an app.yaml-like file.
3612 module_yaml_path: The (string) path to the yaml file, relative to the
3613 bundle directory.
3614 backend: The name of the backend to update, if any.
3616 Returns:
3617 An appinfo.AppInfoSummary if one was returned from the Deploy, None
3618 otherwise.
3620 Raises:
3621 RuntimeError: If go-app-builder fails to generate a mapping from relative
3622 paths to absolute paths, its stderr is raised.
3639 runtime = appyaml.GetEffectiveRuntime()
3640 if appyaml.vm and (self.options.called_by_gcloud or runtime != 'go'):
3641 self.options.precompilation = False
3642 elif runtime == 'dart':
3643 self.options.precompilation = False
3644 elif runtime == 'go' and not self.options.precompilation:
3645 logging.warning('Precompilation is required for Go apps; '
3646 'ignoring --no_precompilation')
3647 self.options.precompilation = True
3648 elif (runtime.startswith('java') and
3649 appinfo.JAVA_PRECOMPILED not in (appyaml.derived_file_type or [])):
3650 self.options.precompilation = False
3652 if self.options.precompilation:
3653 if not appyaml.derived_file_type:
3654 appyaml.derived_file_type = []
3655 if appinfo.PYTHON_PRECOMPILED not in appyaml.derived_file_type:
3656 appyaml.derived_file_type.append(appinfo.PYTHON_PRECOMPILED)
3658 paths = self.file_iterator(basepath, appyaml.skip_files, appyaml.runtime)
3659 openfunc = lambda path: self.opener(os.path.join(basepath, path), 'rb')
3661 if appyaml.GetEffectiveRuntime() == 'go':
3663 sdk_base = os.path.normpath(os.path.join(
3664 google.appengine.__file__, '..', '..', '..'))
3666 gopath = os.environ.get('GOPATH')
3667 if not gopath:
3668 gopath = os.path.join(sdk_base, 'gopath')
3674 goroot = os.path.join(sdk_base, 'goroot')
3675 if not os.path.exists(goroot):
3677 goroot = None
3678 gab = os.path.join(sdk_base, GO_APP_BUILDER)
3679 if os.path.exists(gab):
3680 app_paths = list(paths)
3681 go_files = [f for f in app_paths
3682 if f.endswith('.go') and not appyaml.nobuild_files.match(f)]
3683 if not go_files:
3684 raise RuntimeError('no Go source files to upload '
3685 '(-nobuild_files applied)')
3686 gab_argv = [
3687 gab,
3688 '-app_base', self.basepath,
3689 '-arch', '6',
3690 '-gopath', gopath,
3691 '-print_extras',
3693 if goroot:
3694 gab_argv.extend(['-goroot', goroot])
3695 if appyaml.runtime == 'vm':
3696 gab_argv.append('-vm')
3697 gab_argv.extend(go_files)
3699 env = {
3700 'GOOS': 'linux',
3701 'GOARCH': 'amd64',
3703 logging.info('Invoking go-app-builder: %s', ' '.join(gab_argv))
3704 try:
3705 p = subprocess.Popen(gab_argv, stdout=subprocess.PIPE,
3706 stderr=subprocess.PIPE, env=env)
3707 (stdout, stderr) = p.communicate()
3708 except Exception, e:
3709 raise RuntimeError('failed running go-app-builder', e)
3710 if p.returncode != 0:
3711 raise RuntimeError(stderr)
3716 overlay = dict([l.split('|') for l in stdout.split('\n') if l])
3717 logging.info('GOPATH overlay: %s', overlay)
3719 def Open(path):
3720 if path in overlay:
3721 return self.opener(overlay[path], 'rb')
3722 return self.opener(os.path.join(basepath, path), 'rb')
3723 paths = app_paths + overlay.keys()
3724 openfunc = Open
3726 appversion = AppVersionUpload(rpcserver,
3727 appyaml,
3728 module_yaml_path=module_yaml_path,
3729 backend=backend,
3730 error_fh=self.error_fh,
3731 usage_reporting=self.options.usage_reporting)
3732 return appversion.DoUpload(paths, openfunc)
3734 def UpdateUsingSpecificFiles(self):
3735 """Updates and deploys new app versions based on given config files."""
3736 rpcserver = self._GetRpcServer()
3737 all_files = [self.basepath] + self.args
3738 has_python25_version = False
3740 for yaml_path in all_files:
3741 file_name = os.path.basename(yaml_path)
3742 self.basepath = os.path.dirname(yaml_path)
3743 if not self.basepath:
3744 self.basepath = '.'
3745 module_yaml = self._ParseAppInfoFromYaml(self.basepath,
3746 os.path.splitext(file_name)[0])
3747 if module_yaml.runtime == 'python':
3748 has_python25_version = True
3752 if not module_yaml.module and file_name != 'app.yaml':
3753 ErrorUpdate("Error: 'module' parameter not specified in %s" %
3754 yaml_path)
3755 continue
3756 self.UpdateVersion(rpcserver, self.basepath, module_yaml, file_name)
3757 if has_python25_version:
3758 MigratePython27Notice()
3760 def Update(self):
3761 """Updates and deploys a new appversion and global app configs."""
3762 if not os.path.isdir(self.basepath):
3764 self.UpdateUsingSpecificFiles()
3765 return
3767 if JavaSupported() and appcfg_java.IsWarFileWithoutYaml(self.basepath):
3768 java_app_update = appcfg_java.JavaAppUpdate(self.basepath, self.options)
3769 self.options.compile_jsps = not java_app_update.app_engine_web_xml.vm
3776 sdk_root = os.path.dirname(appcfg_java.__file__)
3777 self.stage_dir = java_app_update.CreateStagingDirectory(sdk_root)
3778 try:
3779 appyaml = self._ParseAppInfoFromYaml(
3780 self.stage_dir,
3781 basename=os.path.splitext(APP_YAML_FILENAME)[0])
3782 self._UpdateWithParsedAppYaml(appyaml, self.stage_dir)
3783 finally:
3784 if self.options.retain_upload_dir:
3785 StatusUpdate(
3786 'Temporary staging directory left in %s' % self.stage_dir,
3787 self.error_fh)
3788 else:
3789 shutil.rmtree(self.stage_dir)
3790 else:
3791 appyaml = self._ParseAppInfoFromYaml(
3792 self.basepath,
3793 basename=os.path.splitext(APP_YAML_FILENAME)[0])
3794 self._UpdateWithParsedAppYaml(appyaml, self.basepath)
3796 def _UpdateWithParsedAppYaml(self, appyaml, basepath):
3797 """Completes update command.
3799 Helper to Update.
3801 Args:
3802 appyaml: AppInfoExternal for the app.
3803 basepath: Path where application's files can be found.
3805 self.runtime = appyaml.runtime
3806 rpcserver = self._GetRpcServer()
3811 if self.options.skip_sdk_update_check:
3812 logging.info('Skipping update check')
3813 else:
3814 updatecheck = self.update_check_class(rpcserver, appyaml)
3815 updatecheck.CheckForUpdates()
3817 def _AbortAppMismatch(yaml_name):
3818 StatusUpdate('Error: Aborting upload because application in %s does not '
3819 'match application in app.yaml' % yaml_name, self.error_fh)
3822 dos_yaml = self._ParseDosYaml(basepath, appyaml)
3823 if dos_yaml and dos_yaml.application != appyaml.application:
3824 _AbortAppMismatch('dos.yaml')
3825 return
3827 queue_yaml = self._ParseQueueYaml(basepath, appyaml)
3828 if queue_yaml and queue_yaml.application != appyaml.application:
3829 _AbortAppMismatch('queue.yaml')
3830 return
3832 cron_yaml = self._ParseCronYaml(basepath, appyaml)
3833 if cron_yaml and cron_yaml.application != appyaml.application:
3834 _AbortAppMismatch('cron.yaml')
3835 return
3837 index_defs = self._ParseIndexYaml(basepath, appyaml)
3838 if index_defs and index_defs.application != appyaml.application:
3839 _AbortAppMismatch('index.yaml')
3840 return
3842 dispatch_yaml = self._ParseDispatchYaml(basepath, appyaml)
3843 if dispatch_yaml and dispatch_yaml.application != appyaml.application:
3844 _AbortAppMismatch('dispatch.yaml')
3845 return
3847 self.UpdateVersion(rpcserver, basepath, appyaml, APP_YAML_FILENAME)
3849 if appyaml.runtime == 'python':
3850 MigratePython27Notice()
3853 if self.options.backends:
3854 self.BackendsUpdate()
3861 if index_defs:
3862 index_upload = IndexDefinitionUpload(rpcserver, index_defs, self.error_fh)
3863 try:
3864 index_upload.DoUpload()
3865 except urllib2.HTTPError, e:
3866 ErrorUpdate('Error %d: --- begin server output ---\n'
3867 '%s\n--- end server output ---' %
3868 (e.code, e.read().rstrip('\n')))
3869 print >> self.error_fh, (
3870 'Your app was updated, but there was an error updating your '
3871 'indexes. Please retry later with appcfg.py update_indexes.')
3874 if cron_yaml:
3875 cron_upload = CronEntryUpload(rpcserver, cron_yaml, self.error_fh)
3876 cron_upload.DoUpload()
3879 if queue_yaml:
3880 queue_upload = QueueEntryUpload(rpcserver, queue_yaml, self.error_fh)
3881 queue_upload.DoUpload()
3884 if dos_yaml:
3885 dos_upload = DosEntryUpload(rpcserver, dos_yaml, self.error_fh)
3886 dos_upload.DoUpload()
3889 if dispatch_yaml:
3890 dispatch_upload = DispatchEntryUpload(rpcserver,
3891 dispatch_yaml,
3892 self.error_fh)
3893 dispatch_upload.DoUpload()
3896 if appyaml:
3897 pagespeed_upload = PagespeedEntryUpload(
3898 rpcserver, appyaml, appyaml.pagespeed, self.error_fh)
3899 try:
3900 pagespeed_upload.DoUpload()
3901 except urllib2.HTTPError, e:
3902 ErrorUpdate('Error %d: --- begin server output ---\n'
3903 '%s\n--- end server output ---' %
3904 (e.code, e.read().rstrip('\n')))
3905 print >> self.error_fh, (
3906 'Your app was updated, but there was an error updating PageSpeed. '
3907 'Please try the update again later.')
3909 def _UpdateOptions(self, parser):
3910 """Adds update-specific options to 'parser'.
3912 Args:
3913 parser: An instance of OptionsParser.
3915 parser.add_option('--no_precompilation', action='store_false',
3916 dest='precompilation', default=True,
3917 help='Disable automatic precompilation '
3918 '(ignored for Go apps).')
3919 parser.add_option('--backends', action='store_true',
3920 dest='backends', default=False,
3921 help='Update backends when performing appcfg update.')
3922 parser.add_option('--no_usage_reporting', action='store_false',
3923 dest='usage_reporting', default=True,
3924 help='Disable usage reporting.')
3925 if JavaSupported():
3926 appcfg_java.AddUpdateOptions(parser)
3928 def VacuumIndexes(self):
3929 """Deletes unused indexes."""
3930 if self.args:
3931 self.parser.error('Expected a single <directory> argument.')
3934 index_defs = self._ParseIndexYaml(self.basepath)
3935 if index_defs is None:
3936 index_defs = datastore_index.IndexDefinitions()
3938 rpcserver = self._GetRpcServer()
3939 vacuum = VacuumIndexesOperation(rpcserver,
3940 self.options.force_delete)
3941 vacuum.DoVacuum(index_defs)
3943 def _VacuumIndexesOptions(self, parser):
3944 """Adds vacuum_indexes-specific options to 'parser'.
3946 Args:
3947 parser: An instance of OptionsParser.
3949 parser.add_option('-f', '--force', action='store_true', dest='force_delete',
3950 default=False,
3951 help='Force deletion without being prompted.')
3953 def UpdateCron(self):
3954 """Updates any new or changed cron definitions."""
3955 if self.args:
3956 self.parser.error('Expected a single <directory> argument.')
3958 rpcserver = self._GetRpcServer()
3961 cron_yaml = self._ParseCronYaml(self.basepath)
3962 if cron_yaml:
3963 cron_upload = CronEntryUpload(rpcserver, cron_yaml, self.error_fh)
3964 cron_upload.DoUpload()
3965 else:
3966 print >>self.error_fh, (
3967 'Could not find cron configuration. No action taken.')
3969 def UpdateIndexes(self):
3970 """Updates indexes."""
3971 if self.args:
3972 self.parser.error('Expected a single <directory> argument.')
3974 rpcserver = self._GetRpcServer()
3977 index_defs = self._ParseIndexYaml(self.basepath)
3978 if index_defs:
3979 index_upload = IndexDefinitionUpload(rpcserver, index_defs, self.error_fh)
3980 index_upload.DoUpload()
3981 else:
3982 print >>self.error_fh, (
3983 'Could not find index configuration. No action taken.')
3985 def UpdateQueues(self):
3986 """Updates any new or changed task queue definitions."""
3987 if self.args:
3988 self.parser.error('Expected a single <directory> argument.')
3989 rpcserver = self._GetRpcServer()
3992 queue_yaml = self._ParseQueueYaml(self.basepath)
3993 if queue_yaml:
3994 queue_upload = QueueEntryUpload(rpcserver, queue_yaml, self.error_fh)
3995 queue_upload.DoUpload()
3996 else:
3997 print >>self.error_fh, (
3998 'Could not find queue configuration. No action taken.')
4000 def UpdateDispatch(self):
4001 """Updates new or changed dispatch definitions."""
4002 if self.args:
4003 self.parser.error('Expected a single <directory> argument.')
4005 rpcserver = self._GetRpcServer()
4008 dispatch_yaml = self._ParseDispatchYaml(self.basepath)
4009 if dispatch_yaml:
4010 dispatch_upload = DispatchEntryUpload(rpcserver,
4011 dispatch_yaml,
4012 self.error_fh)
4013 dispatch_upload.DoUpload()
4014 else:
4015 print >>self.error_fh, ('Could not find dispatch configuration. No action'
4016 ' taken.')
4018 def UpdateDos(self):
4019 """Updates any new or changed dos definitions."""
4020 if self.args:
4021 self.parser.error('Expected a single <directory> argument.')
4022 rpcserver = self._GetRpcServer()
4025 dos_yaml = self._ParseDosYaml(self.basepath)
4026 if dos_yaml:
4027 dos_upload = DosEntryUpload(rpcserver, dos_yaml, self.error_fh)
4028 dos_upload.DoUpload()
4029 else:
4030 print >>self.error_fh, (
4031 'Could not find dos configuration. No action taken.')
4033 def BackendsAction(self):
4034 """Placeholder; we never expect this action to be invoked."""
4035 pass
4037 def BackendsPhpCheck(self, appyaml):
4038 """Don't support backends with the PHP runtime.
4040 This should be used to prevent use of backends update/start/configure
4041 with the PHP runtime. We continue to allow backends
4042 stop/delete/list/rollback just in case there are existing PHP backends.
4044 Args:
4045 appyaml: A parsed app.yaml file.
4047 if appyaml.runtime == 'php':
4048 _PrintErrorAndExit(
4049 self.error_fh,
4050 'Error: Backends are not supported with the PHP runtime. '
4051 'Please use Modules instead.\n')
4053 def BackendsYamlCheck(self, basepath, appyaml, backend=None):
4054 """Check the backends.yaml file is sane and which backends to update."""
4057 if appyaml.backends:
4058 self.parser.error('Backends are not allowed in app.yaml.')
4060 backends_yaml = self._ParseBackendsYaml(basepath)
4061 appyaml.backends = backends_yaml.backends
4063 if not appyaml.backends:
4064 self.parser.error('No backends found in backends.yaml.')
4066 backends = []
4067 for backend_entry in appyaml.backends:
4068 entry = backendinfo.LoadBackendEntry(backend_entry.ToYAML())
4069 if entry.name in backends:
4070 self.parser.error('Duplicate entry for backend: %s.' % entry.name)
4071 else:
4072 backends.append(entry.name)
4074 backends_to_update = []
4076 if backend:
4078 if backend in backends:
4079 backends_to_update = [backend]
4080 else:
4081 self.parser.error("Backend '%s' not found in backends.yaml." %
4082 backend)
4083 else:
4085 backends_to_update = backends
4087 return backends_to_update
4089 def BackendsUpdate(self):
4090 """Updates a backend."""
4091 self.backend = None
4092 if len(self.args) == 1:
4093 self.backend = self.args[0]
4094 elif len(self.args) > 1:
4095 self.parser.error('Expected an optional <backend> argument.')
4096 if JavaSupported() and appcfg_java.IsWarFileWithoutYaml(self.basepath):
4097 java_app_update = appcfg_java.JavaAppUpdate(self.basepath, self.options)
4098 self.options.compile_jsps = True
4099 sdk_root = os.path.dirname(appcfg_java.__file__)
4100 basepath = java_app_update.CreateStagingDirectory(sdk_root)
4101 else:
4102 basepath = self.basepath
4104 yaml_file_basename = 'app'
4105 appyaml = self._ParseAppInfoFromYaml(basepath,
4106 basename=yaml_file_basename)
4107 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4108 self.BackendsPhpCheck(appyaml)
4109 rpcserver = self._GetRpcServer()
4111 backends_to_update = self.BackendsYamlCheck(basepath, appyaml, self.backend)
4112 for backend in backends_to_update:
4113 self.UpdateVersion(rpcserver, basepath, appyaml, yaml_file_basename,
4114 backend=backend)
4116 def BackendsList(self):
4117 """Lists all backends for an app."""
4118 if self.args:
4119 self.parser.error('Expected no arguments.')
4124 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4125 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4126 rpcserver = self._GetRpcServer()
4127 response = rpcserver.Send('/api/backends/list', app_id=appyaml.application)
4128 print >> self.out_fh, response
4130 def BackendsRollback(self):
4131 """Does a rollback of an existing transaction on this backend."""
4132 if len(self.args) != 1:
4133 self.parser.error('Expected a single <backend> argument.')
4135 self._Rollback(self.args[0])
4137 def BackendsStart(self):
4138 """Starts a backend."""
4139 if len(self.args) != 1:
4140 self.parser.error('Expected a single <backend> argument.')
4142 backend = self.args[0]
4143 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4144 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4145 self.BackendsPhpCheck(appyaml)
4146 rpcserver = self._GetRpcServer()
4147 response = rpcserver.Send('/api/backends/start',
4148 app_id=appyaml.application,
4149 backend=backend)
4150 print >> self.out_fh, response
4152 def BackendsStop(self):
4153 """Stops a backend."""
4154 if len(self.args) != 1:
4155 self.parser.error('Expected a single <backend> argument.')
4157 backend = self.args[0]
4158 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4159 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4160 rpcserver = self._GetRpcServer()
4161 response = rpcserver.Send('/api/backends/stop',
4162 app_id=appyaml.application,
4163 backend=backend)
4164 print >> self.out_fh, response
4166 def BackendsDelete(self):
4167 """Deletes a backend."""
4168 if len(self.args) != 1:
4169 self.parser.error('Expected a single <backend> argument.')
4171 backend = self.args[0]
4172 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4173 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4174 rpcserver = self._GetRpcServer()
4175 response = rpcserver.Send('/api/backends/delete',
4176 app_id=appyaml.application,
4177 backend=backend)
4178 print >> self.out_fh, response
4180 def BackendsConfigure(self):
4181 """Changes the configuration of an existing backend."""
4182 if len(self.args) != 1:
4183 self.parser.error('Expected a single <backend> argument.')
4185 backend = self.args[0]
4186 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4187 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4188 self.BackendsPhpCheck(appyaml)
4189 backends_yaml = self._ParseBackendsYaml(self.basepath)
4190 rpcserver = self._GetRpcServer()
4191 response = rpcserver.Send('/api/backends/configure',
4192 app_id=appyaml.application,
4193 backend=backend,
4194 payload=backends_yaml.ToYAML())
4195 print >> self.out_fh, response
4197 def ListVersions(self):
4198 """Lists all versions for an app."""
4199 if len(self.args) == 0:
4200 if not self.options.app_id:
4201 self.parser.error('Expected <directory> argument or -A <app id>.')
4202 app_id = self.options.app_id
4203 elif len(self.args) == 1:
4204 if self.options.app_id:
4205 self.parser.error('<directory> argument is not needed with -A.')
4206 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4207 app_id = appyaml.application
4208 else:
4209 self.parser.error('Expected 1 argument, not %d.' % len(self.args))
4211 rpcserver = self._GetRpcServer()
4212 response = rpcserver.Send('/api/versions/list', app_id=app_id)
4214 parsed_response = yaml.safe_load(response)
4215 if not parsed_response:
4216 print >> self.out_fh, ('No versions uploaded for app: %s.' % app_id)
4217 else:
4218 print >> self.out_fh, response
4220 def DeleteVersion(self):
4221 """Deletes the specified version for an app."""
4222 if not (self.options.app_id and self.options.version):
4223 self.parser.error('Expected an <app_id> argument, a <version> argument '
4224 'and an optional <module> argument.')
4225 if self.options.module:
4226 module = self.options.module
4227 else:
4228 module = ''
4230 rpcserver = self._GetRpcServer()
4231 response = rpcserver.Send('/api/versions/delete',
4232 app_id=self.options.app_id,
4233 version_match=self.options.version,
4234 module=module)
4236 print >> self.out_fh, response
4238 def _LockingAction(self, url):
4239 """Changes the locking state for a given version."""
4240 if len(self.args) == 1:
4241 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4242 app_id = appyaml.application
4243 module = appyaml.module or ''
4244 version = appyaml.version
4245 elif not self.args:
4246 if not (self.options.app_id and self.options.version):
4247 self.parser.error(
4248 ('Expected a <directory> argument or both --application and '
4249 '--version flags.'))
4250 module = ''
4251 else:
4252 self._PrintHelpAndExit()
4256 if self.options.app_id:
4257 app_id = self.options.app_id
4258 if self.options.module:
4259 module = self.options.module
4260 if self.options.version:
4261 version = self.options.version
4263 rpcserver = self._GetRpcServer()
4264 DoLockAction(
4265 url,
4266 rpcserver,
4267 app_id, version, module,
4268 self.options.instance,
4269 self.out_fh).Do()
4271 def DebugAction(self):
4272 """Sets the specified version and instance for an app to be debuggable."""
4273 self._LockingAction('/api/vms/debug')
4275 def LockAction(self):
4276 """Locks the specified version and instance for an app."""
4277 self._LockingAction('/api/vms/lock')
4279 def _LockActionOptions(self, parser):
4280 """Adds lock/unlock-specific options to 'parser'.
4282 Args:
4283 parser: An instance of OptionsParser.
4285 parser.add_option('-I', '--instance', type='string', dest='instance',
4286 help='Instance to lock/unlock.')
4288 def PrepareVmRuntimeAction(self):
4289 """Prepare the application for vm runtimes and return state."""
4290 if not self.options.app_id:
4291 self.parser.error('Expected an --application argument')
4292 rpcserver = self._GetRpcServer()
4293 response = rpcserver.Send('/api/vms/prepare',
4294 app_id=self.options.app_id)
4295 print >> self.out_fh, response
4297 def _ParseAndValidateModuleYamls(self, yaml_paths):
4298 """Validates given yaml paths and returns the parsed yaml objects.
4300 Args:
4301 yaml_paths: List of paths to AppInfo yaml files.
4303 Returns:
4304 List of parsed AppInfo yamls.
4306 results = []
4307 app_id = None
4308 last_yaml_path = None
4309 for yaml_path in yaml_paths:
4310 if not os.path.isfile(yaml_path):
4311 _PrintErrorAndExit(
4312 self.error_fh,
4313 ("Error: The given path '%s' is not to a YAML configuration "
4314 "file.\n") % yaml_path)
4315 file_name = os.path.basename(yaml_path)
4316 base_path = os.path.dirname(yaml_path)
4317 if not base_path:
4318 base_path = '.'
4319 module_yaml = self._ParseAppInfoFromYaml(base_path,
4320 os.path.splitext(file_name)[0])
4322 if not module_yaml.module and file_name != 'app.yaml':
4323 _PrintErrorAndExit(
4324 self.error_fh,
4325 "Error: 'module' parameter not specified in %s" % yaml_path)
4329 if app_id is not None and module_yaml.application != app_id:
4330 _PrintErrorAndExit(
4331 self.error_fh,
4332 "Error: 'application' value '%s' in %s does not match the value "
4333 "'%s', found in %s" % (module_yaml.application,
4334 yaml_path,
4335 app_id,
4336 last_yaml_path))
4337 app_id = module_yaml.application
4338 last_yaml_path = yaml_path
4339 results.append(module_yaml)
4341 return results
4343 def _ModuleAction(self, action_path):
4344 """Process flags and yaml files and make a call to the given path.
4346 The 'start_module_version' and 'stop_module_version' actions are extremely
4347 similar in how they process input to appcfg.py and only really differ in
4348 what path they hit on the RPCServer.
4350 Args:
4351 action_path: Path on the RPCServer to send the call to.
4354 modules_to_process = []
4355 if not self.args:
4357 if not (self.options.app_id and
4358 self.options.module and
4359 self.options.version):
4360 _PrintErrorAndExit(self.error_fh,
4361 'Expected at least one <file> argument or the '
4362 '--application, --module and --version flags to'
4363 ' be set.')
4364 else:
4365 modules_to_process.append((self.options.app_id,
4366 self.options.module,
4367 self.options.version))
4368 else:
4371 if self.options.module:
4373 _PrintErrorAndExit(self.error_fh,
4374 'You may not specify a <file> argument with the '
4375 '--module flag.')
4377 module_yamls = self._ParseAndValidateModuleYamls(self.args)
4378 for serv_yaml in module_yamls:
4381 app_id = serv_yaml.application
4382 modules_to_process.append((self.options.app_id or serv_yaml.application,
4383 serv_yaml.module or appinfo.DEFAULT_MODULE,
4384 self.options.version or serv_yaml.version))
4386 rpcserver = self._GetRpcServer()
4389 for app_id, module, version in modules_to_process:
4390 response = rpcserver.Send(action_path,
4391 app_id=app_id,
4392 module=module,
4393 version=version)
4394 print >> self.out_fh, response
4396 def StartModuleVersion(self):
4397 """Starts one or more versions."""
4398 self._ModuleAction('/api/modules/start')
4400 def StopModuleVersion(self):
4401 """Stops one or more versions."""
4402 self._ModuleAction('/api/modules/stop')
4404 def Rollback(self):
4405 """Does a rollback of an existing transaction for this app version."""
4406 self._Rollback()
4408 def _RollbackOptions(self, parser):
4409 """Adds rollback-specific options to parser.
4411 Args:
4412 parser: An instance of OptionsParser.
4414 parser.add_option('--force_rollback', action='store_true',
4415 dest='force_rollback', default=False,
4416 help='Force rollback.')
4418 def _Rollback(self, backend=None):
4419 """Does a rollback of an existing transaction.
4421 Args:
4422 backend: name of a backend to rollback, or None
4424 If a backend is specified the rollback will affect only that backend, if no
4425 backend is specified the rollback will affect the current app version.
4427 if os.path.isdir(self.basepath):
4428 module_yaml = self._ParseAppInfoFromYaml(self.basepath)
4429 else:
4431 file_name = os.path.basename(self.basepath)
4432 self.basepath = os.path.dirname(self.basepath)
4433 if not self.basepath:
4434 self.basepath = '.'
4435 module_yaml = self._ParseAppInfoFromYaml(self.basepath,
4436 os.path.splitext(file_name)[0])
4438 appversion = AppVersionUpload(self._GetRpcServer(), module_yaml,
4439 module_yaml_path='app.yaml',
4440 backend=backend)
4442 appversion.in_transaction = True
4447 force_rollback = False
4448 if hasattr(self.options, 'force_rollback'):
4449 force_rollback = self.options.force_rollback
4451 appversion.Rollback(force_rollback)
4453 def SetDefaultVersion(self):
4454 """Sets the default version."""
4455 module = ''
4456 if len(self.args) == 1:
4460 stored_modules = self.options.module
4461 self.options.module = None
4462 try:
4463 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4464 finally:
4465 self.options.module = stored_modules
4467 app_id = appyaml.application
4468 module = appyaml.module or ''
4469 version = appyaml.version
4470 elif not self.args:
4471 if not (self.options.app_id and self.options.version):
4472 self.parser.error(
4473 ('Expected a <directory> argument or both --application and '
4474 '--version flags.'))
4475 else:
4476 self._PrintHelpAndExit()
4479 if self.options.app_id:
4480 app_id = self.options.app_id
4481 if self.options.module:
4482 module = self.options.module
4483 if self.options.version:
4484 version = self.options.version
4486 version_setter = DefaultVersionSet(self._GetRpcServer(),
4487 app_id,
4488 module,
4489 version,
4490 self.error_fh)
4491 version_setter.SetVersion()
4494 def MigrateTraffic(self):
4495 """Migrates traffic."""
4496 if len(self.args) == 1:
4497 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4498 app_id = appyaml.application
4499 version = appyaml.version
4500 elif not self.args:
4501 if not (self.options.app_id and self.options.version):
4502 self.parser.error(
4503 ('Expected a <directory> argument or both --application and '
4504 '--version flags.'))
4505 else:
4506 self._PrintHelpAndExit()
4509 if self.options.app_id:
4510 app_id = self.options.app_id
4511 if self.options.version:
4512 version = self.options.version
4514 traffic_migrator = TrafficMigrator(
4515 self._GetRpcServer(), app_id, version, self.error_fh)
4516 traffic_migrator.MigrateTraffic()
4518 def RequestLogs(self):
4519 """Write request logs to a file."""
4521 args_length = len(self.args)
4522 module = ''
4523 if args_length == 2:
4524 appyaml = self._ParseAppInfoFromYaml(self.args.pop(0))
4525 app_id = appyaml.application
4526 module = appyaml.module or ''
4527 version = appyaml.version
4528 elif args_length == 1:
4529 if not (self.options.app_id and self.options.version):
4530 self.parser.error(
4531 ('Expected the --application and --version flags if <directory> '
4532 'argument is not specified.'))
4533 else:
4534 self._PrintHelpAndExit()
4537 if self.options.app_id:
4538 app_id = self.options.app_id
4539 if self.options.module:
4540 module = self.options.module
4541 if self.options.version:
4542 version = self.options.version
4544 if (self.options.severity is not None and
4545 not 0 <= self.options.severity <= MAX_LOG_LEVEL):
4546 self.parser.error(
4547 'Severity range is 0 (DEBUG) through %s (CRITICAL).' % MAX_LOG_LEVEL)
4549 if self.options.num_days is None:
4550 self.options.num_days = int(not self.options.append)
4552 try:
4553 end_date = self._ParseEndDate(self.options.end_date)
4554 except (TypeError, ValueError):
4555 self.parser.error('End date must be in the format YYYY-MM-DD.')
4557 rpcserver = self._GetRpcServer()
4559 logs_requester = LogsRequester(rpcserver,
4560 app_id,
4561 module,
4562 version,
4563 self.args[0],
4564 self.options.num_days,
4565 self.options.append,
4566 self.options.severity,
4567 end_date,
4568 self.options.vhost,
4569 self.options.include_vhost,
4570 self.options.include_all,
4571 time_func=self.time_func)
4572 logs_requester.DownloadLogs()
4574 @staticmethod
4575 def _ParseEndDate(date, time_func=time.time):
4576 """Translates an ISO 8601 date to a date object.
4578 Args:
4579 date: A date string as YYYY-MM-DD.
4580 time_func: A time.time() compatible function, which can be overridden for
4581 testing.
4583 Returns:
4584 A date object representing the last day of logs to get.
4585 If no date is given, returns today in the US/Pacific timezone.
4587 if not date:
4588 return PacificDate(time_func())
4589 return datetime.date(*[int(i) for i in date.split('-')])
4591 def _RequestLogsOptions(self, parser):
4592 """Adds request_logs-specific options to 'parser'.
4594 Args:
4595 parser: An instance of OptionsParser.
4597 parser.add_option('-n', '--num_days', type='int', dest='num_days',
4598 action='store', default=None,
4599 help='Number of days worth of log data to get. '
4600 'The cut-off point is midnight US/Pacific. '
4601 'Use 0 to get all available logs. '
4602 'Default is 1, unless --append is also given; '
4603 'then the default is 0.')
4604 parser.add_option('-a', '--append', dest='append',
4605 action='store_true', default=False,
4606 help='Append to existing file.')
4607 parser.add_option('--severity', type='int', dest='severity',
4608 action='store', default=None,
4609 help='Severity of app-level log messages to get. '
4610 'The range is 0 (DEBUG) through 4 (CRITICAL). '
4611 'If omitted, only request logs are returned.')
4612 parser.add_option('--vhost', type='string', dest='vhost',
4613 action='store', default=None,
4614 help='The virtual host of log messages to get. '
4615 'If omitted, all log messages are returned.')
4616 parser.add_option('--include_vhost', dest='include_vhost',
4617 action='store_true', default=False,
4618 help='Include virtual host in log messages.')
4619 parser.add_option('--include_all', dest='include_all',
4620 action='store_true', default=None,
4621 help='Include everything in log messages.')
4622 parser.add_option('--end_date', dest='end_date',
4623 action='store', default='',
4624 help='End date (as YYYY-MM-DD) of period for log data. '
4625 'Defaults to today.')
4627 def CronInfo(self, now=None, output=sys.stdout):
4628 """Displays information about cron definitions.
4630 Args:
4631 now: used for testing.
4632 output: Used for testing.
4634 if self.args:
4635 self.parser.error('Expected a single <directory> argument.')
4636 if now is None:
4637 now = datetime.datetime.utcnow()
4639 cron_yaml = self._ParseCronYaml(self.basepath)
4640 if cron_yaml and cron_yaml.cron:
4641 for entry in cron_yaml.cron:
4642 description = entry.description
4643 if not description:
4644 description = '<no description>'
4645 if not entry.timezone:
4646 entry.timezone = 'UTC'
4648 print >>output, '\n%s:\nURL: %s\nSchedule: %s (%s)' % (description,
4649 entry.url,
4650 entry.schedule,
4651 entry.timezone)
4652 if entry.timezone != 'UTC':
4653 print >>output, ('Note: Schedules with timezones won\'t be calculated'
4654 ' correctly here')
4655 schedule = groctimespecification.GrocTimeSpecification(entry.schedule)
4657 matches = schedule.GetMatches(now, self.options.num_runs)
4658 for match in matches:
4659 print >>output, '%s, %s from now' % (
4660 match.strftime('%Y-%m-%d %H:%M:%SZ'), match - now)
4662 def _CronInfoOptions(self, parser):
4663 """Adds cron_info-specific options to 'parser'.
4665 Args:
4666 parser: An instance of OptionsParser.
4668 parser.add_option('-n', '--num_runs', type='int', dest='num_runs',
4669 action='store', default=5,
4670 help='Number of runs of each cron job to display'
4671 'Default is 5')
4673 def _CheckRequiredLoadOptions(self):
4674 """Checks that upload/download options are present."""
4675 for option in ['filename']:
4676 if getattr(self.options, option) is None:
4677 self.parser.error('Option \'%s\' is required.' % option)
4678 if not self.options.url:
4679 self.parser.error('You must have google.appengine.ext.remote_api.handler '
4680 'assigned to an endpoint in app.yaml, or provide '
4681 'the url of the handler via the \'url\' option.')
4683 def InferRemoteApiUrl(self, appyaml):
4684 """Uses app.yaml to determine the remote_api endpoint.
4686 Args:
4687 appyaml: A parsed app.yaml file.
4689 Returns:
4690 The url of the remote_api endpoint as a string, or None
4693 handlers = appyaml.handlers
4694 handler_suffixes = ['remote_api/handler.py',
4695 'remote_api.handler.application']
4696 app_id = appyaml.application
4697 for handler in handlers:
4698 if hasattr(handler, 'script') and handler.script:
4699 if any(handler.script.endswith(suffix) for suffix in handler_suffixes):
4700 server = self.options.server
4701 url = handler.url
4702 if url.endswith('(/.*)?'):
4705 url = url[:-6]
4706 if server == 'appengine.google.com':
4707 return 'http://%s.appspot.com%s' % (app_id, url)
4708 else:
4709 match = re.match(PREFIXED_BY_ADMIN_CONSOLE_RE, server)
4710 if match:
4711 return 'http://%s%s%s' % (app_id, match.group(1), url)
4712 else:
4713 return 'http://%s%s' % (server, url)
4714 return None
4716 def RunBulkloader(self, arg_dict):
4717 """Invokes the bulkloader with the given keyword arguments.
4719 Args:
4720 arg_dict: Dictionary of arguments to pass to bulkloader.Run().
4723 try:
4725 import sqlite3
4726 except ImportError:
4727 logging.error('upload_data action requires SQLite3 and the python '
4728 'sqlite3 module (included in python since 2.5).')
4729 sys.exit(1)
4731 sys.exit(bulkloader.Run(arg_dict))
4733 def _SetupLoad(self):
4734 """Performs common verification and set up for upload and download."""
4736 if len(self.args) != 1 and not self.options.url:
4737 self.parser.error('Expected either --url or a single <directory> '
4738 'argument.')
4740 if len(self.args) == 1:
4741 self.basepath = self.args[0]
4742 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4744 self.options.app_id = appyaml.application
4746 if not self.options.url:
4747 url = self.InferRemoteApiUrl(appyaml)
4748 if url is not None:
4749 self.options.url = url
4751 self._CheckRequiredLoadOptions()
4753 if self.options.batch_size < 1:
4754 self.parser.error('batch_size must be 1 or larger.')
4758 if verbosity == 1:
4759 logging.getLogger().setLevel(logging.INFO)
4760 self.options.debug = False
4761 else:
4762 logging.getLogger().setLevel(logging.DEBUG)
4763 self.options.debug = True
4765 def _MakeLoaderArgs(self):
4766 """Returns a dict made from many attributes of self.options, plus others.
4768 See body for list of self.options attributes included. In addition, result
4769 includes
4770 'application' = self.options.app_id
4771 'throttle_class' = self.throttle_class
4773 Returns:
4774 A dict.
4776 args = dict([(arg_name, getattr(self.options, arg_name, None)) for
4777 arg_name in (
4778 'url',
4779 'filename',
4780 'batch_size',
4781 'kind',
4782 'num_threads',
4783 'bandwidth_limit',
4784 'rps_limit',
4785 'http_limit',
4786 'db_filename',
4787 'config_file',
4788 'auth_domain',
4789 'has_header',
4790 'loader_opts',
4791 'log_file',
4792 'passin',
4793 'email',
4794 'debug',
4795 'exporter_opts',
4796 'mapper_opts',
4797 'result_db_filename',
4798 'mapper_opts',
4799 'dry_run',
4800 'dump',
4801 'restore',
4802 'namespace',
4803 'create_config',
4805 args['application'] = self.options.app_id
4806 args['throttle_class'] = self.throttle_class
4807 return args
4809 def PerformDownload(self, run_fn=None):
4810 """Performs a datastore download via the bulkloader.
4812 Args:
4813 run_fn: Function to invoke the bulkloader, used for testing.
4815 if run_fn is None:
4816 run_fn = self.RunBulkloader
4817 self._SetupLoad()
4819 StatusUpdate('Downloading data records.', self.error_fh)
4821 args = self._MakeLoaderArgs()
4822 args['download'] = bool(args['config_file'])
4823 args['has_header'] = False
4824 args['map'] = False
4825 args['dump'] = not args['config_file']
4826 args['restore'] = False
4827 args['create_config'] = False
4829 run_fn(args)
4831 def PerformUpload(self, run_fn=None):
4832 """Performs a datastore upload via the bulkloader.
4834 Args:
4835 run_fn: Function to invoke the bulkloader, used for testing.
4837 if run_fn is None:
4838 run_fn = self.RunBulkloader
4839 self._SetupLoad()
4841 StatusUpdate('Uploading data records.', self.error_fh)
4843 args = self._MakeLoaderArgs()
4844 args['download'] = False
4845 args['map'] = False
4846 args['dump'] = False
4847 args['restore'] = not args['config_file']
4848 args['create_config'] = False
4850 run_fn(args)
4852 def CreateBulkloadConfig(self, run_fn=None):
4853 """Create a bulkloader config via the bulkloader wizard.
4855 Args:
4856 run_fn: Function to invoke the bulkloader, used for testing.
4858 if run_fn is None:
4859 run_fn = self.RunBulkloader
4860 self._SetupLoad()
4862 StatusUpdate('Creating bulkloader configuration.', self.error_fh)
4864 args = self._MakeLoaderArgs()
4865 args['download'] = False
4866 args['has_header'] = False
4867 args['map'] = False
4868 args['dump'] = False
4869 args['restore'] = False
4870 args['create_config'] = True
4872 run_fn(args)
4874 def _PerformLoadOptions(self, parser):
4875 """Adds options common to 'upload_data' and 'download_data'.
4877 Args:
4878 parser: An instance of OptionsParser.
4880 parser.add_option('--url', type='string', dest='url',
4881 action='store',
4882 help='The location of the remote_api endpoint.')
4883 parser.add_option('--batch_size', type='int', dest='batch_size',
4884 action='store', default=10,
4885 help='Number of records to post in each request.')
4886 parser.add_option('--bandwidth_limit', type='int', dest='bandwidth_limit',
4887 action='store', default=250000,
4888 help='The maximum bytes/second bandwidth for transfers.')
4889 parser.add_option('--rps_limit', type='int', dest='rps_limit',
4890 action='store', default=20,
4891 help='The maximum records/second for transfers.')
4892 parser.add_option('--http_limit', type='int', dest='http_limit',
4893 action='store', default=8,
4894 help='The maximum requests/second for transfers.')
4895 parser.add_option('--db_filename', type='string', dest='db_filename',
4896 action='store',
4897 help='Name of the progress database file.')
4898 parser.add_option('--auth_domain', type='string', dest='auth_domain',
4899 action='store', default='gmail.com',
4900 help='The name of the authorization domain to use.')
4901 parser.add_option('--log_file', type='string', dest='log_file',
4902 help='File to write bulkloader logs. If not supplied '
4903 'then a new log file will be created, named: '
4904 'bulkloader-log-TIMESTAMP.')
4905 parser.add_option('--dry_run', action='store_true',
4906 dest='dry_run', default=False,
4907 help='Do not execute any remote_api calls')
4908 parser.add_option('--namespace', type='string', dest='namespace',
4909 action='store', default='',
4910 help='Namespace to use when accessing datastore.')
4911 parser.add_option('--num_threads', type='int', dest='num_threads',
4912 action='store', default=10,
4913 help='Number of threads to transfer records with.')
4915 def _PerformUploadOptions(self, parser):
4916 """Adds 'upload_data' specific options to the 'parser' passed in.
4918 Args:
4919 parser: An instance of OptionsParser.
4921 self._PerformLoadOptions(parser)
4922 parser.add_option('--filename', type='string', dest='filename',
4923 action='store',
4924 help='The name of the file containing the input data.'
4925 ' (Required)')
4926 parser.add_option('--kind', type='string', dest='kind',
4927 action='store',
4928 help='The kind of the entities to store.')
4929 parser.add_option('--has_header', dest='has_header',
4930 action='store_true', default=False,
4931 help='Whether the first line of the input file should be'
4932 ' skipped')
4933 parser.add_option('--loader_opts', type='string', dest='loader_opts',
4934 help='A string to pass to the Loader.initialize method.')
4935 parser.add_option('--config_file', type='string', dest='config_file',
4936 action='store',
4937 help='Name of the configuration file.')
4939 def _PerformDownloadOptions(self, parser):
4940 """Adds 'download_data' specific options to the 'parser' passed in.
4942 Args:
4943 parser: An instance of OptionsParser.
4945 self._PerformLoadOptions(parser)
4946 parser.add_option('--filename', type='string', dest='filename',
4947 action='store',
4948 help='The name of the file where output data is to be'
4949 ' written. (Required)')
4950 parser.add_option('--kind', type='string', dest='kind',
4951 action='store',
4952 help='The kind of the entities to retrieve.')
4953 parser.add_option('--exporter_opts', type='string', dest='exporter_opts',
4954 help='A string to pass to the Exporter.initialize method.'
4956 parser.add_option('--result_db_filename', type='string',
4957 dest='result_db_filename',
4958 action='store',
4959 help='Database to write entities to for download.')
4960 parser.add_option('--config_file', type='string', dest='config_file',
4961 action='store',
4962 help='Name of the configuration file.')
4964 def _CreateBulkloadConfigOptions(self, parser):
4965 """Adds 'download_data' specific options to the 'parser' passed in.
4967 Args:
4968 parser: An instance of OptionsParser.
4970 self._PerformLoadOptions(parser)
4971 parser.add_option('--filename', type='string', dest='filename',
4972 action='store',
4973 help='The name of the file where the generated template'
4974 ' is to be written. (Required)')
4975 parser.add_option('--result_db_filename', type='string',
4976 dest='result_db_filename',
4977 action='store',
4978 help='Database to write entities to during config '
4979 'generation.')
4981 def ResourceLimitsInfo(self, output=None):
4982 """Outputs the current resource limits.
4984 Args:
4985 output: The file handle to write the output to (used for testing).
4987 rpcserver = self._GetRpcServer()
4988 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4989 request_params = {'app_id': appyaml.application, 'version': appyaml.version}
4990 logging_context = _ClientDeployLoggingContext(rpcserver, request_params,
4991 usage_reporting=False)
4992 resource_limits = GetResourceLimits(logging_context, self.error_fh)
4995 for attr_name in sorted(resource_limits):
4996 print >>output, '%s: %s' % (attr_name, resource_limits[attr_name])
4998 class Action(object):
4999 """Contains information about a command line action.
5001 Attributes:
5002 function: The name of a function defined on AppCfg or its subclasses
5003 that will perform the appropriate action.
5004 usage: A command line usage string.
5005 short_desc: A one-line description of the action.
5006 long_desc: A detailed description of the action. Whitespace and
5007 formatting will be preserved.
5008 error_desc: An error message to display when the incorrect arguments are
5009 given.
5010 options: A function that will add extra options to a given OptionParser
5011 object.
5012 uses_basepath: Does the action use a basepath/app-directory (and hence
5013 app.yaml).
5014 hidden: Should this command be shown in the help listing.
5022 def __init__(self, function, usage, short_desc, long_desc='',
5023 error_desc=None, options=lambda obj, parser: None,
5024 uses_basepath=True, hidden=False):
5025 """Initializer for the class attributes."""
5026 self.function = function
5027 self.usage = usage
5028 self.short_desc = short_desc
5029 self.long_desc = long_desc
5030 self.error_desc = error_desc
5031 self.options = options
5032 self.uses_basepath = uses_basepath
5033 self.hidden = hidden
5035 def __call__(self, appcfg):
5036 """Invoke this Action on the specified AppCfg.
5038 This calls the function of the appropriate name on AppCfg, and
5039 respects polymophic overrides.
5041 Args:
5042 appcfg: The appcfg to use.
5043 Returns:
5044 The result of the function call.
5046 method = getattr(appcfg, self.function)
5047 return method()
5049 actions = {
5051 'help': Action(
5052 function='Help',
5053 usage='%prog help <action>',
5054 short_desc='Print help for a specific action.',
5055 uses_basepath=False),
5057 'update': Action(
5058 function='Update',
5059 usage='%prog [options] update <directory> | [file, ...]',
5060 options=_UpdateOptions,
5061 short_desc='Create or update an app version.',
5062 long_desc="""
5063 Specify a directory that contains all of the files required by
5064 the app, and appcfg.py will create/update the app version referenced
5065 in the app.yaml file at the top level of that directory. appcfg.py
5066 will follow symlinks and recursively upload all files to the server.
5067 Temporary or source control files (e.g. foo~, .svn/*) will be skipped.
5069 If you are using the Modules feature, then you may prefer to pass multiple files
5070 to update, rather than a directory, to specify which modules you would like
5071 updated."""),
5073 'download_app': Action(
5074 function='DownloadApp',
5075 usage='%prog [options] download_app -A app_id [ -V version ]'
5076 ' <out-dir>',
5077 short_desc='Download a previously-uploaded app.',
5078 long_desc="""
5079 Download a previously-uploaded app to the specified directory. The app
5080 ID is specified by the \"-A\" option. The optional version is specified
5081 by the \"-V\" option.""",
5082 uses_basepath=False),
5084 'update_cron': Action(
5085 function='UpdateCron',
5086 usage='%prog [options] update_cron <directory>',
5087 short_desc='Update application cron definitions.',
5088 long_desc="""
5089 The 'update_cron' command will update any new, removed or changed cron
5090 definitions from the optional cron.yaml file."""),
5092 'update_indexes': Action(
5093 function='UpdateIndexes',
5094 usage='%prog [options] update_indexes <directory>',
5095 short_desc='Update application indexes.',
5096 long_desc="""
5097 The 'update_indexes' command will add additional indexes which are not currently
5098 in production as well as restart any indexes that were not completed."""),
5100 'update_queues': Action(
5101 function='UpdateQueues',
5102 usage='%prog [options] update_queues <directory>',
5103 short_desc='Update application task queue definitions.',
5104 long_desc="""
5105 The 'update_queue' command will update any new, removed or changed task queue
5106 definitions from the optional queue.yaml file."""),
5108 'update_dispatch': Action(
5109 function='UpdateDispatch',
5110 usage='%prog [options] update_dispatch <directory>',
5111 short_desc='Update application dispatch definitions.',
5112 long_desc="""
5113 The 'update_dispatch' command will update any new, removed or changed dispatch
5114 definitions from the optional dispatch.yaml file."""),
5116 'update_dos': Action(
5117 function='UpdateDos',
5118 usage='%prog [options] update_dos <directory>',
5119 short_desc='Update application dos definitions.',
5120 long_desc="""
5121 The 'update_dos' command will update any new, removed or changed dos
5122 definitions from the optional dos.yaml file."""),
5124 'backends': Action(
5125 function='BackendsAction',
5126 usage='%prog [options] backends <directory> <action>',
5127 short_desc='Perform a backend action.',
5128 long_desc="""
5129 The 'backends' command will perform a backends action.""",
5130 error_desc="""\
5131 Expected a <directory> and <action> argument."""),
5133 'backends list': Action(
5134 function='BackendsList',
5135 usage='%prog [options] backends <directory> list',
5136 short_desc='List all backends configured for the app.',
5137 long_desc="""
5138 The 'backends list' command will list all backends configured for the app."""),
5140 'backends update': Action(
5141 function='BackendsUpdate',
5142 usage='%prog [options] backends <directory> update [<backend>]',
5143 options=_UpdateOptions,
5144 short_desc='Update one or more backends.',
5145 long_desc="""
5146 The 'backends update' command updates one or more backends. This command
5147 updates backend configuration settings and deploys new code to the server. Any
5148 existing instances will stop and be restarted. Updates all backends, or a
5149 single backend if the <backend> argument is provided."""),
5151 'backends rollback': Action(
5152 function='BackendsRollback',
5153 usage='%prog [options] backends <directory> rollback <backend>',
5154 short_desc='Roll back an update of a backend.',
5155 long_desc="""
5156 The 'backends update' command requires a server-side transaction.
5157 Use 'backends rollback' if you experience an error during 'backends update'
5158 and want to start the update over again."""),
5160 'backends start': Action(
5161 function='BackendsStart',
5162 usage='%prog [options] backends <directory> start <backend>',
5163 short_desc='Start a backend.',
5164 long_desc="""
5165 The 'backends start' command will put a backend into the START state."""),
5167 'backends stop': Action(
5168 function='BackendsStop',
5169 usage='%prog [options] backends <directory> stop <backend>',
5170 short_desc='Stop a backend.',
5171 long_desc="""
5172 The 'backends start' command will put a backend into the STOP state."""),
5174 'backends delete': Action(
5175 function='BackendsDelete',
5176 usage='%prog [options] backends <directory> delete <backend>',
5177 short_desc='Delete a backend.',
5178 long_desc="""
5179 The 'backends delete' command will delete a backend."""),
5181 'backends configure': Action(
5182 function='BackendsConfigure',
5183 usage='%prog [options] backends <directory> configure <backend>',
5184 short_desc='Reconfigure a backend without stopping it.',
5185 long_desc="""
5186 The 'backends configure' command performs an online update of a backend, without
5187 stopping instances that are currently running. No code or handlers are updated,
5188 only certain configuration settings specified in backends.yaml. Valid settings
5189 are: instances, options: public, and options: failfast."""),
5191 'vacuum_indexes': Action(
5192 function='VacuumIndexes',
5193 usage='%prog [options] vacuum_indexes <directory>',
5194 options=_VacuumIndexesOptions,
5195 short_desc='Delete unused indexes from application.',
5196 long_desc="""
5197 The 'vacuum_indexes' command will help clean up indexes which are no longer
5198 in use. It does this by comparing the local index configuration with
5199 indexes that are actually defined on the server. If any indexes on the
5200 server do not exist in the index configuration file, the user is given the
5201 option to delete them."""),
5203 'rollback': Action(
5204 function='Rollback',
5205 usage='%prog [options] rollback <directory> | <file>',
5206 options=_RollbackOptions,
5207 short_desc='Rollback an in-progress update.',
5208 long_desc="""
5209 The 'update' command requires a server-side transaction.
5210 Use 'rollback' if you experience an error during 'update'
5211 and want to begin a new update transaction."""),
5213 'request_logs': Action(
5214 function='RequestLogs',
5215 usage='%prog [options] request_logs [<directory>] <output_file>',
5216 options=_RequestLogsOptions,
5217 uses_basepath=False,
5218 short_desc='Write request logs in Apache common log format.',
5219 long_desc="""
5220 The 'request_logs' command exports the request logs from your application
5221 to a file. It will write Apache common log format records ordered
5222 chronologically. If output file is '-' stdout will be written.""",
5223 error_desc="""\
5224 Expected an optional <directory> and mandatory <output_file> argument."""),
5226 'cron_info': Action(
5227 function='CronInfo',
5228 usage='%prog [options] cron_info <directory>',
5229 options=_CronInfoOptions,
5230 short_desc='Display information about cron jobs.',
5231 long_desc="""
5232 The 'cron_info' command will display the next 'number' runs (default 5) for
5233 each cron job defined in the cron.yaml file."""),
5235 'start_module_version': Action(
5236 function='StartModuleVersion',
5237 uses_basepath=False,
5238 usage='%prog [options] start_module_version [file, ...]',
5239 short_desc='Start a module version.',
5240 long_desc="""
5241 The 'start_module_version' command will put a module version into the START
5242 state."""),
5244 'stop_module_version': Action(
5245 function='StopModuleVersion',
5246 uses_basepath=False,
5247 usage='%prog [options] stop_module_version [file, ...]',
5248 short_desc='Stop a module version.',
5249 long_desc="""
5250 The 'stop_module_version' command will put a module version into the STOP
5251 state."""),
5257 'upload_data': Action(
5258 function='PerformUpload',
5259 usage='%prog [options] upload_data <directory>',
5260 options=_PerformUploadOptions,
5261 short_desc='Upload data records to datastore.',
5262 long_desc="""
5263 The 'upload_data' command translates input records into datastore entities and
5264 uploads them into your application's datastore.""",
5265 uses_basepath=False),
5267 'download_data': Action(
5268 function='PerformDownload',
5269 usage='%prog [options] download_data <directory>',
5270 options=_PerformDownloadOptions,
5271 short_desc='Download entities from datastore.',
5272 long_desc="""
5273 The 'download_data' command downloads datastore entities and writes them to
5274 file as CSV or developer defined format.""",
5275 uses_basepath=False),
5277 'create_bulkloader_config': Action(
5278 function='CreateBulkloadConfig',
5279 usage='%prog [options] create_bulkload_config <directory>',
5280 options=_CreateBulkloadConfigOptions,
5281 short_desc='Create a bulkloader.yaml from a running application.',
5282 long_desc="""
5283 The 'create_bulkloader_config' command creates a bulkloader.yaml configuration
5284 template for use with upload_data or download_data.""",
5285 uses_basepath=False),
5288 'set_default_version': Action(
5289 function='SetDefaultVersion',
5290 usage='%prog [options] set_default_version [directory]',
5291 short_desc='Set the default (serving) version.',
5292 long_desc="""
5293 The 'set_default_version' command sets the default (serving) version of the app.
5294 Defaults to using the application, version and module specified in app.yaml;
5295 use the --application, --version and --module flags to override these values.
5296 The --module flag can also be a comma-delimited string of several modules. (ex.
5297 module1,module2,module3) In this case, the default version of each module will
5298 be changed to the version specified.
5300 The 'migrate_traffic' command can be thought of as a safer version of this
5301 command.""",
5302 uses_basepath=False),
5304 'migrate_traffic': Action(
5305 function='MigrateTraffic',
5306 usage='%prog [options] migrate_traffic [directory]',
5307 short_desc='Migrates traffic to another version.',
5308 long_desc="""
5309 The 'migrate_traffic' command gradually gradually sends an increasing fraction
5310 of traffic your app's traffic from the current default version to another
5311 version. Once all traffic has been migrated, the new version is set as the
5312 default version.
5314 app.yaml specifies the target application, version, and (optionally) module; use
5315 the --application, --version and --module flags to override these values.
5317 Can be thought of as an enhanced version of the 'set_default_version'
5318 command.""",
5320 uses_basepath=False,
5322 hidden=True),
5324 'resource_limits_info': Action(
5325 function='ResourceLimitsInfo',
5326 usage='%prog [options] resource_limits_info <directory>',
5327 short_desc='Get the resource limits.',
5328 long_desc="""
5329 The 'resource_limits_info' command prints the current resource limits that
5330 are enforced."""),
5332 'list_versions': Action(
5333 function='ListVersions',
5334 usage='%prog [options] list_versions [directory]',
5335 short_desc='List all uploaded versions for an app.',
5336 long_desc="""
5337 The 'list_versions' command outputs the uploaded versions for each module of
5338 an application in YAML. The YAML is in formatted as an associative array,
5339 mapping module_ids to the list of versions uploaded for that module. The
5340 default version will be first in the list.""",
5341 uses_basepath=False),
5343 'delete_version': Action(
5344 function='DeleteVersion',
5345 usage='%prog [options] delete_version -A app_id -V version '
5346 '[-M module]',
5347 uses_basepath=False,
5348 short_desc='Delete the specified version for an app.',
5349 long_desc="""
5350 The 'delete_version' command deletes the specified version for the specified
5351 application."""),
5353 'debug': Action(
5354 function='DebugAction',
5355 usage='%prog [options] debug [-A app_id] [-V version]'
5356 ' [-M module] [-I instance] [directory]',
5357 options=_LockActionOptions,
5358 short_desc='Debug a vm runtime application.',
5359 hidden=True,
5360 uses_basepath=False,
5361 long_desc="""
5362 The 'debug' command configures a vm runtime application to be accessable
5363 for debugging."""),
5365 'lock': Action(
5366 function='LockAction',
5367 usage='%prog [options] lock [-A app_id] [-V version]'
5368 ' [-M module] [-I instance] [directory]',
5369 options=_LockActionOptions,
5370 short_desc='Lock a debugged vm runtime application.',
5371 hidden=True,
5372 uses_basepath=False,
5373 long_desc="""
5374 The 'lock' command relocks a debugged vm runtime application."""),
5376 'prepare_vm_runtime': Action(
5377 function='PrepareVmRuntimeAction',
5378 usage='%prog [options] prepare_vm_runtime -A app_id',
5379 short_desc='Prepare an application for the VM runtime.',
5380 hidden=True,
5381 uses_basepath=False,
5382 long_desc="""
5383 The 'prepare_vm_runtime' prepares an application for the VM runtime."""),
5387 def main(argv):
5388 logging.basicConfig(format=('%(asctime)s %(levelname)s %(filename)s:'
5389 '%(lineno)s %(message)s '))
5390 try:
5391 result = AppCfgApp(argv).Run()
5392 if result:
5393 sys.exit(result)
5394 except KeyboardInterrupt:
5395 StatusUpdate('Interrupted.')
5396 sys.exit(1)
5399 if __name__ == '__main__':
5400 main(sys.argv)