App Engine Python SDK version 1.8.8
[gae.git] / python / google / appengine / tools / appcfg.py
blobcdecb18344e16bd5b8397eeaf0e40d7bfba0e31f
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.
21 """Tool for deploying apps to an app server.
23 Currently, the application only uploads new appversions. To do this, it first
24 walks the directory tree rooted at the path the user specifies, adding all the
25 files it finds to a list. It then uploads the application configuration
26 (app.yaml) to the server using HTTP, followed by uploading each of the files.
27 It then commits the transaction with another request.
29 The bulk of this work is handled by the AppVersionUpload class, which exposes
30 methods to add to the list of files, fetch a list of modified files, upload
31 files, and commit or rollback the transaction.
32 """
33 from __future__ import with_statement
36 import calendar
37 import contextlib
38 import copy
39 import datetime
40 import errno
41 import getpass
42 import hashlib
43 import logging
44 import mimetypes
45 import optparse
46 import os
47 import random
48 import re
49 import shutil
50 import subprocess
51 import sys
52 import tempfile
53 import time
54 import urllib
55 import urllib2
59 import google
60 import yaml
62 from google.appengine.cron import groctimespecification
63 from google.appengine.api import appinfo
64 from google.appengine.api import appinfo_includes
65 from google.appengine.api import backendinfo
66 from google.appengine.api import croninfo
67 from google.appengine.api import dispatchinfo
68 from google.appengine.api import dosinfo
69 from google.appengine.api import queueinfo
70 from google.appengine.api import yaml_errors
71 from google.appengine.api import yaml_object
72 from google.appengine.datastore import datastore_index
73 from google.appengine.tools import appcfg_java
74 from google.appengine.tools import appengine_rpc
75 try:
78 from google.appengine.tools import appengine_rpc_httplib2
79 except ImportError:
80 appengine_rpc_httplib2 = None
81 from google.appengine.tools import bulkloader
82 from google.appengine.tools import sdk_update_checker
85 LIST_DELIMITER = '\n'
86 TUPLE_DELIMITER = '|'
87 BACKENDS_ACTION = 'backends'
88 BACKENDS_MESSAGE = ('Looks like you\'re using Backends. We suggest that you '
89 'start looking at App Engine Modules. See the Modules '
90 'documentation to learn more about converting: ')
91 _CONVERTING_URL = (
92 'https://developers.google.com/appengine/docs/%s/modules/converting')
95 MAX_LOG_LEVEL = 4
98 MAX_BATCH_SIZE = 3200000
99 MAX_BATCH_COUNT = 100
100 MAX_BATCH_FILE_SIZE = 200000
101 BATCH_OVERHEAD = 500
108 verbosity = 1
111 PREFIXED_BY_ADMIN_CONSOLE_RE = '^(?:admin-console)(.*)'
114 SDK_PRODUCT = 'appcfg_py'
117 DAY = 24*3600
118 SUNDAY = 6
120 SUPPORTED_RUNTIMES = ('go', 'php', 'python', 'python27', 'java', 'java7')
125 MEGA = 1024 * 1024
126 MILLION = 1000 * 1000
127 DEFAULT_RESOURCE_LIMITS = {
128 'max_file_size': 32 * MILLION,
129 'max_blob_size': 32 * MILLION,
130 'max_files_to_clone': 100,
131 'max_total_file_size': 150 * MEGA,
132 'max_file_count': 10000,
135 # Client ID and secrets are managed in the Google API console.
141 APPCFG_CLIENT_ID = '550516889912.apps.googleusercontent.com'
142 APPCFG_CLIENT_NOTSOSECRET = 'ykPq-0UYfKNprLRjVx1hBBar'
143 APPCFG_SCOPES = ('https://www.googleapis.com/auth/appengine.admin',)
146 STATIC_FILE_PREFIX = '__static__'
150 METADATA_BASE = 'http://metadata.google.internal'
151 SERVICE_ACCOUNT_BASE = (
152 'computeMetadata/v1beta1/instance/service-accounts/default')
155 class Error(Exception):
156 pass
159 class OAuthNotAvailable(Error):
160 """The appengine_rpc_httplib2 module could not be imported."""
161 pass
164 class CannotStartServingError(Error):
165 """We could not start serving the version being uploaded."""
166 pass
169 def PrintUpdate(msg):
170 """Print a message to stderr.
172 If 'verbosity' is greater than 0, print the message.
174 Args:
175 msg: The string to print.
177 if verbosity > 0:
178 timestamp = datetime.datetime.now()
179 print >>sys.stderr, '%s %s' % (timestamp.strftime('%I:%M %p'), msg)
182 def StatusUpdate(msg):
183 """Print a status message to stderr."""
184 PrintUpdate(msg)
187 def BackendsStatusUpdate(runtime):
188 """Print the Backends status message based on current runtime.
190 Args:
191 runtime: String name of current runtime.
193 language = runtime
194 if language == 'python27':
195 language = 'python'
196 elif language == 'java7':
197 language = 'java'
198 if language == 'python' or language == 'java':
199 StatusUpdate(BACKENDS_MESSAGE + (_CONVERTING_URL % language))
202 def ErrorUpdate(msg):
203 """Print an error message to stderr."""
204 PrintUpdate(msg)
207 def _PrintErrorAndExit(stream, msg, exit_code=2):
208 """Prints the given error message and exists the program.
210 Args:
211 stream: The stream (e.g. StringIO or file) to write the message to.
212 msg: The error message to display as a string.
213 exit_code: The integer code to pass to sys.exit().
215 stream.write(msg)
216 sys.exit(exit_code)
219 @contextlib.contextmanager
220 def TempChangeField(obj, field_name, new_value):
221 """Context manager to change a field value on an object temporarily.
223 Args:
224 obj: The object to change the field on.
225 field_name: The field name to change.
226 new_value: The new value.
228 Yields:
229 The old value.
231 old_value = getattr(obj, field_name)
232 setattr(obj, field_name, new_value)
233 yield old_value
234 setattr(obj, field_name, old_value)
237 class FileClassification(object):
238 """A class to hold a file's classification.
240 This class both abstracts away the details of how we determine
241 whether a file is a regular, static or error file as well as acting
242 as a container for various metadata about the file.
245 def __init__(self, config, filename):
246 """Initializes a FileClassification instance.
248 Args:
249 config: The app.yaml object to check the filename against.
250 filename: The name of the file.
252 self.__static_mime_type = self.__GetMimeTypeIfStaticFile(config, filename)
253 self.__static_app_readable = self.__GetAppReadableIfStaticFile(config,
254 filename)
255 self.__error_mime_type, self.__error_code = self.__LookupErrorBlob(config,
256 filename)
258 @staticmethod
259 def __GetMimeTypeIfStaticFile(config, filename):
260 """Looks up the mime type for 'filename'.
262 Uses the handlers in 'config' to determine if the file should
263 be treated as a static file.
265 Args:
266 config: The app.yaml object to check the filename against.
267 filename: The name of the file.
269 Returns:
270 The mime type string. For example, 'text/plain' or 'image/gif'.
271 None if this is not a static file.
273 if FileClassification.__FileNameImpliesStaticFile(filename):
274 return FileClassification.__MimeType(filename)
275 for handler in config.handlers:
276 handler_type = handler.GetHandlerType()
277 if handler_type in ('static_dir', 'static_files'):
278 if handler_type == 'static_dir':
279 regex = os.path.join(re.escape(handler.GetHandler()), '.*')
280 else:
281 regex = handler.upload
282 if re.match(regex, filename):
283 return handler.mime_type or FileClassification.__MimeType(filename)
284 return None
286 @staticmethod
287 def __FileNameImpliesStaticFile(filename):
288 """True if the name of a file implies that it is a static resource.
290 For Java applications specified with web.xml and appengine-web.xml, we
291 create a staging directory that includes a __static__ hierarchy containing
292 links to all files that are implied static by the contents of those XML
293 files. So if a file has been copied into that directory then we can assume
294 it is static.
296 Args:
297 filename: The full path to the file.
299 Returns:
300 True if the file should be considered a static resource based on its name.
302 return ('__static__' + os.sep) in filename
304 @staticmethod
305 def __GetAppReadableIfStaticFile(config, filename):
306 """Looks up whether a static file is readable by the application.
308 Uses the handlers in 'config' to determine if the file should
309 be treated as a static file and if so, if the file should be readable by the
310 application.
312 Args:
313 config: The AppInfoExternal object to check the filename against.
314 filename: The name of the file.
316 Returns:
317 True if the file is static and marked as app readable, False otherwise.
319 for handler in config.handlers:
320 handler_type = handler.GetHandlerType()
321 if handler_type in ('static_dir', 'static_files'):
322 if handler_type == 'static_dir':
323 regex = os.path.join(re.escape(handler.GetHandler()), '.*')
324 else:
325 regex = handler.upload
326 if re.match(regex, filename):
327 return handler.application_readable
328 return False
330 @staticmethod
331 def __LookupErrorBlob(config, filename):
332 """Looks up the mime type and error_code for 'filename'.
334 Uses the error handlers in 'config' to determine if the file should
335 be treated as an error blob.
337 Args:
338 config: The app.yaml object to check the filename against.
339 filename: The name of the file.
341 Returns:
343 A tuple of (mime_type, error_code), or (None, None) if this is not an
344 error blob. For example, ('text/plain', default) or ('image/gif',
345 timeout) or (None, None).
347 if not config.error_handlers:
348 return (None, None)
349 for error_handler in config.error_handlers:
350 if error_handler.file == filename:
351 error_code = error_handler.error_code
352 error_code = error_code or 'default'
353 if error_handler.mime_type:
354 return (error_handler.mime_type, error_code)
355 else:
356 return (FileClassification.__MimeType(filename), error_code)
357 return (None, None)
359 @staticmethod
360 def __MimeType(filename, default='application/octet-stream'):
361 guess = mimetypes.guess_type(filename)[0]
362 if guess is None:
363 print >>sys.stderr, ('Could not guess mimetype for %s. Using %s.'
364 % (filename, default))
365 return default
366 return guess
368 def IsApplicationFile(self):
369 return bool((not self.IsStaticFile() or self.__static_app_readable) and
370 not self.IsErrorFile())
372 def IsStaticFile(self):
373 return bool(self.__static_mime_type)
375 def StaticMimeType(self):
376 return self.__static_mime_type
378 def IsErrorFile(self):
379 return bool(self.__error_mime_type)
381 def ErrorMimeType(self):
382 return self.__error_mime_type
384 def ErrorCode(self):
385 return self.__error_code
388 def BuildClonePostBody(file_tuples):
389 """Build the post body for the /api/clone{files,blobs,errorblobs} urls.
391 Args:
392 file_tuples: A list of tuples. Each tuple should contain the entries
393 appropriate for the endpoint in question.
395 Returns:
396 A string containing the properly delimited tuples.
398 file_list = []
399 for tup in file_tuples:
400 path = tup[1]
401 tup = tup[2:]
402 file_list.append(TUPLE_DELIMITER.join([path] + list(tup)))
403 return LIST_DELIMITER.join(file_list)
406 def GetRemoteResourceLimits(rpcserver, config):
407 """Get the resource limit as reported by the admin console.
409 Get the resource limits by querying the admin_console/appserver. The
410 actual limits returned depends on the server we are talking to and
411 could be missing values we expect or include extra values.
413 Args:
414 rpcserver: The RPC server to use.
415 config: The appyaml configuration.
417 Returns:
418 A dictionary.
420 try:
421 StatusUpdate('Getting current resource limits.')
422 yaml_data = rpcserver.Send('/api/appversion/getresourcelimits',
423 app_id=config.application,
424 version=config.version)
426 except urllib2.HTTPError, err:
430 if err.code != 404:
431 raise
432 return {}
434 return yaml.safe_load(yaml_data)
437 def GetResourceLimits(rpcserver, config):
438 """Gets the resource limits.
440 Gets the resource limits that should be applied to apps. Any values
441 that the server does not know about will have their default value
442 reported (although it is also possible for the server to report
443 values we don't know about).
445 Args:
446 rpcserver: The RPC server to use.
447 config: The appyaml configuration.
449 Returns:
450 A dictionary.
452 resource_limits = DEFAULT_RESOURCE_LIMITS.copy()
453 resource_limits.update(GetRemoteResourceLimits(rpcserver, config))
454 logging.debug('Using resource limits: %s', resource_limits)
455 return resource_limits
458 def RetryWithBackoff(callable_func, retry_notify_func,
459 initial_delay=1, backoff_factor=2,
460 max_delay=60, max_tries=20):
461 """Calls a function multiple times, backing off more and more each time.
463 Args:
464 callable_func: A function that performs some operation that should be
465 retried a number of times up on failure. Signature: () -> (done, value)
466 If 'done' is True, we'll immediately return (True, value)
467 If 'done' is False, we'll delay a bit and try again, unless we've
468 hit the 'max_tries' limit, in which case we'll return (False, value).
469 retry_notify_func: This function will be called immediately before the
470 next retry delay. Signature: (value, delay) -> None
471 'value' is the value returned by the last call to 'callable_func'
472 'delay' is the retry delay, in seconds
473 initial_delay: Initial delay after first try, in seconds.
474 backoff_factor: Delay will be multiplied by this factor after each try.
475 max_delay: Maximum delay, in seconds.
476 max_tries: Maximum number of tries (the first one counts).
478 Returns:
479 What the last call to 'callable_func' returned, which is of the form
480 (done, value). If 'done' is True, you know 'callable_func' returned True
481 before we ran out of retries. If 'done' is False, you know 'callable_func'
482 kept returning False and we ran out of retries.
484 Raises:
485 Whatever the function raises--an exception will immediately stop retries.
488 delay = initial_delay
489 num_tries = 0
491 while True:
492 done, opaque_value = callable_func()
493 num_tries += 1
495 if done:
496 return True, opaque_value
498 if num_tries >= max_tries:
499 return False, opaque_value
501 retry_notify_func(opaque_value, delay)
502 time.sleep(delay)
503 delay = min(delay * backoff_factor, max_delay)
506 def MigratePython27Notice():
507 """Tells the user that Python 2.5 runtime is deprecated.
509 Encourages the user to migrate from Python 2.5 to Python 2.7.
511 Prints a message to sys.stdout. The caller should have tested that the user is
512 using Python 2.5, so as not to spuriously display this message.
514 print (
515 'WARNING: This application is using the Python 2.5 runtime, which is '
516 'deprecated! It should be updated to the Python 2.7 runtime as soon as '
517 'possible, which offers performance improvements and many new features. '
518 'Learn how simple it is to migrate your application to Python 2.7 at '
519 'https://developers.google.com/appengine/docs/python/python25/migrate27.')
522 class IndexDefinitionUpload(object):
523 """Provides facilities to upload index definitions to the hosting service."""
525 def __init__(self, rpcserver, definitions):
526 """Creates a new DatastoreIndexUpload.
528 Args:
529 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
530 or TestRpcServer.
531 definitions: An IndexDefinitions object.
533 self.rpcserver = rpcserver
534 self.definitions = definitions
536 def DoUpload(self):
537 """Uploads the index definitions."""
538 StatusUpdate('Uploading index definitions.')
540 with TempChangeField(self.definitions, 'application', None) as app_id:
541 self.rpcserver.Send('/api/datastore/index/add',
542 app_id=app_id,
543 payload=self.definitions.ToYAML())
546 class CronEntryUpload(object):
547 """Provides facilities to upload cron entries to the hosting service."""
549 def __init__(self, rpcserver, cron):
550 """Creates a new CronEntryUpload.
552 Args:
553 rpcserver: The RPC server to use. Should be an instance of a subclass of
554 AbstractRpcServer
555 cron: The CronInfoExternal object loaded from the cron.yaml file.
557 self.rpcserver = rpcserver
558 self.cron = cron
560 def DoUpload(self):
561 """Uploads the cron entries."""
562 StatusUpdate('Uploading cron entries.')
564 with TempChangeField(self.cron, 'application', None) as app_id:
565 self.rpcserver.Send('/api/cron/update',
566 app_id=app_id,
567 payload=self.cron.ToYAML())
570 class QueueEntryUpload(object):
571 """Provides facilities to upload task queue entries to the hosting service."""
573 def __init__(self, rpcserver, queue):
574 """Creates a new QueueEntryUpload.
576 Args:
577 rpcserver: The RPC server to use. Should be an instance of a subclass of
578 AbstractRpcServer
579 queue: The QueueInfoExternal object loaded from the queue.yaml file.
581 self.rpcserver = rpcserver
582 self.queue = queue
584 def DoUpload(self):
585 """Uploads the task queue entries."""
586 StatusUpdate('Uploading task queue entries.')
588 with TempChangeField(self.queue, 'application', None) as app_id:
589 self.rpcserver.Send('/api/queue/update',
590 app_id=app_id,
591 payload=self.queue.ToYAML())
594 class DosEntryUpload(object):
595 """Provides facilities to upload dos entries to the hosting service."""
597 def __init__(self, rpcserver, dos):
598 """Creates a new DosEntryUpload.
600 Args:
601 rpcserver: The RPC server to use. Should be an instance of a subclass of
602 AbstractRpcServer.
603 dos: The DosInfoExternal object loaded from the dos.yaml file.
605 self.rpcserver = rpcserver
606 self.dos = dos
608 def DoUpload(self):
609 """Uploads the dos entries."""
610 StatusUpdate('Uploading DOS entries.')
612 with TempChangeField(self.dos, 'application', None) as app_id:
613 self.rpcserver.Send('/api/dos/update',
614 app_id=app_id,
615 payload=self.dos.ToYAML())
618 class PagespeedEntryUpload(object):
619 """Provides facilities to upload pagespeed configs to the hosting service."""
621 def __init__(self, rpcserver, config, pagespeed):
622 """Creates a new PagespeedEntryUpload.
624 Args:
625 rpcserver: The RPC server to use. Should be an instance of a subclass of
626 AbstractRpcServer.
627 config: The AppInfoExternal object derived from the app.yaml file.
628 pagespeed: The PagespeedEntry object from config.
630 self.rpcserver = rpcserver
631 self.config = config
632 self.pagespeed = pagespeed
634 def DoUpload(self):
635 """Uploads the pagespeed entries."""
637 pagespeed_yaml = ''
638 if self.pagespeed:
639 StatusUpdate('Uploading PageSpeed configuration.')
640 pagespeed_yaml = self.pagespeed.ToYAML()
641 try:
642 self.rpcserver.Send('/api/appversion/updatepagespeed',
643 app_id=self.config.application,
644 version=self.config.version,
645 payload=pagespeed_yaml)
646 except urllib2.HTTPError, err:
656 if err.code != 404 or self.pagespeed is not None:
657 raise
660 class DefaultVersionSet(object):
661 """Provides facilities to set the default (serving) version."""
663 def __init__(self, rpcserver, app_id, module, version):
664 """Creates a new DefaultVersionSet.
666 Args:
667 rpcserver: The RPC server to use. Should be an instance of a subclass of
668 AbstractRpcServer.
669 app_id: The application to make the change to.
670 module: The module to set the default version of (if any).
671 version: The version to set as the default.
673 self.rpcserver = rpcserver
674 self.app_id = app_id
675 self.module = module
676 self.version = version
678 def SetVersion(self):
679 """Sets the default version."""
680 if self.module:
682 modules = self.module.split(',')
683 if len(modules) > 1:
684 StatusUpdate('Setting the default version of modules %s of application '
685 '%s to %s.' % (', '.join(modules),
686 self.app_id,
687 self.version))
692 params = [('app_id', self.app_id), ('version', self.version)]
693 params.extend(('module', module) for module in modules)
694 url = '/api/appversion/setdefault?' + urllib.urlencode(sorted(params))
695 self.rpcserver.Send(url)
696 return
698 else:
699 StatusUpdate('Setting default version of module %s of application %s '
700 'to %s.' % (self.module, self.app_id, self.version))
701 else:
702 StatusUpdate('Setting default version of application %s to %s.'
703 % (self.app_id, self.version))
704 self.rpcserver.Send('/api/appversion/setdefault',
705 app_id=self.app_id,
706 module=self.module,
707 version=self.version)
710 class TrafficMigrator(object):
711 """Provides facilities to migrate traffic."""
713 def __init__(self, rpcserver, app_id, version):
714 """Creates a new TrafficMigrator.
716 Args:
717 rpcserver: The RPC server to use. Should be an instance of a subclass of
718 AbstractRpcServer.
719 app_id: The application to make the change to.
721 version: The version to set as the default.
723 self.rpcserver = rpcserver
724 self.app_id = app_id
725 self.version = version
727 def MigrateTraffic(self):
728 """Migrates traffic."""
729 StatusUpdate('Migrating traffic of application %s to %s.'
730 % (self.app_id, self.version))
731 self.rpcserver.Send('/api/appversion/migratetraffic',
732 app_id=self.app_id,
733 version=self.version)
736 class IndexOperation(object):
737 """Provide facilities for writing Index operation commands."""
739 def __init__(self, rpcserver):
740 """Creates a new IndexOperation.
742 Args:
743 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
744 or TestRpcServer.
746 self.rpcserver = rpcserver
748 def DoDiff(self, definitions):
749 """Retrieve diff file from the server.
751 Args:
752 definitions: datastore_index.IndexDefinitions as loaded from users
753 index.yaml file.
755 Returns:
756 A pair of datastore_index.IndexDefinitions objects. The first record
757 is the set of indexes that are present in the index.yaml file but missing
758 from the server. The second record is the set of indexes that are
759 present on the server but missing from the index.yaml file (indicating
760 that these indexes should probably be vacuumed).
762 StatusUpdate('Fetching index definitions diff.')
763 with TempChangeField(definitions, 'application', None) as app_id:
764 response = self.rpcserver.Send('/api/datastore/index/diff',
765 app_id=app_id,
766 payload=definitions.ToYAML())
768 return datastore_index.ParseMultipleIndexDefinitions(response)
770 def DoDelete(self, definitions, app_id):
771 """Delete indexes from the server.
773 Args:
774 definitions: Index definitions to delete from datastore.
775 app_id: The application id.
777 Returns:
778 A single datstore_index.IndexDefinitions containing indexes that were
779 not deleted, probably because they were already removed. This may
780 be normal behavior as there is a potential race condition between fetching
781 the index-diff and sending deletion confirmation through.
783 StatusUpdate('Deleting selected index definitions.')
785 response = self.rpcserver.Send('/api/datastore/index/delete',
786 app_id=app_id,
787 payload=definitions.ToYAML())
788 return datastore_index.ParseIndexDefinitions(response)
791 class VacuumIndexesOperation(IndexOperation):
792 """Provide facilities to request the deletion of datastore indexes."""
794 def __init__(self, rpcserver, force, confirmation_fn=raw_input):
795 """Creates a new VacuumIndexesOperation.
797 Args:
798 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
799 or TestRpcServer.
800 force: True to force deletion of indexes, else False.
801 confirmation_fn: Function used for getting input form user.
803 super(VacuumIndexesOperation, self).__init__(rpcserver)
804 self.force = force
805 self.confirmation_fn = confirmation_fn
807 def GetConfirmation(self, index):
808 """Get confirmation from user to delete an index.
810 This method will enter an input loop until the user provides a
811 response it is expecting. Valid input is one of three responses:
813 y: Confirm deletion of index.
814 n: Do not delete index.
815 a: Delete all indexes without asking for further confirmation.
817 If the user enters nothing at all, the default action is to skip
818 that index and do not delete.
820 If the user selects 'a', as a side effect, the 'force' flag is set.
822 Args:
823 index: Index to confirm.
825 Returns:
826 True if user enters 'y' or 'a'. False if user enter 'n'.
828 while True:
830 print 'This index is no longer defined in your index.yaml file.'
831 print
832 print index.ToYAML()
833 print
836 confirmation = self.confirmation_fn(
837 'Are you sure you want to delete this index? (N/y/a): ')
838 confirmation = confirmation.strip().lower()
841 if confirmation == 'y':
842 return True
843 elif confirmation == 'n' or not confirmation:
844 return False
845 elif confirmation == 'a':
846 self.force = True
847 return True
848 else:
849 print 'Did not understand your response.'
851 def DoVacuum(self, definitions):
852 """Vacuum indexes in datastore.
854 This method will query the server to determine which indexes are not
855 being used according to the user's local index.yaml file. Once it has
856 made this determination, it confirms with the user which unused indexes
857 should be deleted. Once confirmation for each index is receives, it
858 deletes those indexes.
860 Because another user may in theory delete the same indexes at the same
861 time as the user, there is a potential race condition. In this rare cases,
862 some of the indexes previously confirmed for deletion will not be found.
863 The user is notified which indexes these were.
865 Args:
866 definitions: datastore_index.IndexDefinitions as loaded from users
867 index.yaml file.
870 unused_new_indexes, notused_indexes = self.DoDiff(definitions)
873 deletions = datastore_index.IndexDefinitions(indexes=[])
874 if notused_indexes.indexes is not None:
875 for index in notused_indexes.indexes:
876 if self.force or self.GetConfirmation(index):
877 deletions.indexes.append(index)
880 if deletions.indexes:
881 not_deleted = self.DoDelete(deletions, definitions.application)
884 if not_deleted.indexes:
885 not_deleted_count = len(not_deleted.indexes)
886 if not_deleted_count == 1:
887 warning_message = ('An index was not deleted. Most likely this is '
888 'because it no longer exists.\n\n')
889 else:
890 warning_message = ('%d indexes were not deleted. Most likely this '
891 'is because they no longer exist.\n\n'
892 % not_deleted_count)
893 for index in not_deleted.indexes:
894 warning_message += index.ToYAML()
895 logging.warning(warning_message)
898 class LogsRequester(object):
899 """Provide facilities to export request logs."""
901 def __init__(self,
902 rpcserver,
903 app_id,
904 module,
905 version_id,
906 output_file,
907 num_days,
908 append,
909 severity,
910 end,
911 vhost,
912 include_vhost,
913 include_all=None,
914 time_func=time.time):
915 """Constructor.
917 Args:
918 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
919 or TestRpcServer.
920 app_id: The application to fetch logs from.
921 module: The module of the app to fetch logs from, optional.
922 version_id: The version of the app to fetch logs for.
923 output_file: Output file name.
924 num_days: Number of days worth of logs to export; 0 for all available.
925 append: True if appending to an existing file.
926 severity: App log severity to request (0-4); None for no app logs.
927 end: date object representing last day of logs to return.
928 vhost: The virtual host of log messages to get. None for all hosts.
929 include_vhost: If true, the virtual host is included in log messages.
930 include_all: If true, we add to the log message everything we know
931 about the request.
932 time_func: A time.time() compatible function, which can be overridden for
933 testing.
936 self.rpcserver = rpcserver
937 self.app_id = app_id
938 self.output_file = output_file
939 self.append = append
940 self.num_days = num_days
941 self.severity = severity
942 self.vhost = vhost
943 self.include_vhost = include_vhost
944 self.include_all = include_all
946 self.module = module
947 self.version_id = version_id
948 self.sentinel = None
949 self.write_mode = 'w'
950 if self.append:
951 self.sentinel = FindSentinel(self.output_file)
952 self.write_mode = 'a'
955 self.skip_until = False
956 now = PacificDate(time_func())
957 if end < now:
958 self.skip_until = end
959 else:
961 end = now
963 self.valid_dates = None
964 if self.num_days:
965 start = end - datetime.timedelta(self.num_days - 1)
966 self.valid_dates = (start, end)
968 def DownloadLogs(self):
969 """Download the requested logs.
971 This will write the logs to the file designated by
972 self.output_file, or to stdout if the filename is '-'.
973 Multiple roundtrips to the server may be made.
975 if self.module:
976 StatusUpdate('Downloading request logs for app %s module %s version %s.' %
977 (self.app_id, self.module, self.version_id))
978 else:
979 StatusUpdate('Downloading request logs for app %s version %s.' %
980 (self.app_id, self.version_id))
986 tf = tempfile.TemporaryFile()
987 last_offset = None
988 try:
989 while True:
990 try:
991 new_offset = self.RequestLogLines(tf, last_offset)
992 if not new_offset or new_offset == last_offset:
993 break
994 last_offset = new_offset
995 except KeyboardInterrupt:
996 StatusUpdate('Keyboard interrupt; saving data downloaded so far.')
997 break
998 StatusUpdate('Copying request logs to %r.' % self.output_file)
999 if self.output_file == '-':
1000 of = sys.stdout
1001 else:
1002 try:
1003 of = open(self.output_file, self.write_mode)
1004 except IOError, err:
1005 StatusUpdate('Can\'t write %r: %s.' % (self.output_file, err))
1006 sys.exit(1)
1007 try:
1008 line_count = CopyReversedLines(tf, of)
1009 finally:
1010 of.flush()
1011 if of is not sys.stdout:
1012 of.close()
1013 finally:
1014 tf.close()
1015 StatusUpdate('Copied %d records.' % line_count)
1017 def RequestLogLines(self, tf, offset):
1018 """Make a single roundtrip to the server.
1020 Args:
1021 tf: Writable binary stream to which the log lines returned by
1022 the server are written, stripped of headers, and excluding
1023 lines skipped due to self.sentinel or self.valid_dates filtering.
1024 offset: Offset string for a continued request; None for the first.
1026 Returns:
1027 The offset string to be used for the next request, if another
1028 request should be issued; or None, if not.
1030 logging.info('Request with offset %r.', offset)
1031 kwds = {'app_id': self.app_id,
1032 'version': self.version_id,
1033 'limit': 1000,
1035 if self.module:
1036 kwds['module'] = self.module
1037 if offset:
1038 kwds['offset'] = offset
1039 if self.severity is not None:
1040 kwds['severity'] = str(self.severity)
1041 if self.vhost is not None:
1042 kwds['vhost'] = str(self.vhost)
1043 if self.include_vhost is not None:
1044 kwds['include_vhost'] = str(self.include_vhost)
1045 if self.include_all is not None:
1046 kwds['include_all'] = str(self.include_all)
1047 response = self.rpcserver.Send('/api/request_logs', payload=None, **kwds)
1048 response = response.replace('\r', '\0')
1049 lines = response.splitlines()
1050 logging.info('Received %d bytes, %d records.', len(response), len(lines))
1051 offset = None
1052 if lines and lines[0].startswith('#'):
1053 match = re.match(r'^#\s*next_offset=(\S+)\s*$', lines[0])
1054 del lines[0]
1055 if match:
1056 offset = match.group(1)
1057 if lines and lines[-1].startswith('#'):
1058 del lines[-1]
1060 valid_dates = self.valid_dates
1061 sentinel = self.sentinel
1062 skip_until = self.skip_until
1063 len_sentinel = None
1064 if sentinel:
1065 len_sentinel = len(sentinel)
1066 for line in lines:
1067 if (sentinel and
1068 line.startswith(sentinel) and
1069 line[len_sentinel : len_sentinel+1] in ('', '\0')):
1070 return None
1072 linedate = DateOfLogLine(line)
1074 if not linedate:
1075 continue
1077 if skip_until:
1078 if linedate > skip_until:
1079 continue
1080 else:
1082 self.skip_until = skip_until = False
1084 if valid_dates and not valid_dates[0] <= linedate <= valid_dates[1]:
1085 return None
1086 tf.write(line + '\n')
1087 if not lines:
1088 return None
1089 return offset
1092 def DateOfLogLine(line):
1093 """Returns a date object representing the log line's timestamp.
1095 Args:
1096 line: a log line string.
1097 Returns:
1098 A date object representing the timestamp or None if parsing fails.
1100 m = re.compile(r'[^[]+\[(\d+/[A-Za-z]+/\d+):[^\d]*').match(line)
1101 if not m:
1102 return None
1103 try:
1104 return datetime.date(*time.strptime(m.group(1), '%d/%b/%Y')[:3])
1105 except ValueError:
1106 return None
1109 def PacificDate(now):
1110 """For a UTC timestamp, return the date in the US/Pacific timezone.
1112 Args:
1113 now: A posix timestamp giving current UTC time.
1115 Returns:
1116 A date object representing what day it is in the US/Pacific timezone.
1119 return datetime.date(*time.gmtime(PacificTime(now))[:3])
1122 def PacificTime(now):
1123 """Helper to return the number of seconds between UTC and Pacific time.
1125 This is needed to compute today's date in Pacific time (more
1126 specifically: Mountain View local time), which is how request logs
1127 are reported. (Google servers always report times in Mountain View
1128 local time, regardless of where they are physically located.)
1130 This takes (post-2006) US DST into account. Pacific time is either
1131 8 hours or 7 hours west of UTC, depending on whether DST is in
1132 effect. Since 2007, US DST starts on the Second Sunday in March
1133 March, and ends on the first Sunday in November. (Reference:
1134 http://aa.usno.navy.mil/faq/docs/daylight_time.php.)
1136 Note that the server doesn't report its local time (the HTTP Date
1137 header uses UTC), and the client's local time is irrelevant.
1139 Args:
1140 now: A posix timestamp giving current UTC time.
1142 Returns:
1143 A pseudo-posix timestamp giving current Pacific time. Passing
1144 this through time.gmtime() will produce a tuple in Pacific local
1145 time.
1147 now -= 8*3600
1148 if IsPacificDST(now):
1149 now += 3600
1150 return now
1153 def IsPacificDST(now):
1154 """Helper for PacificTime to decide whether now is Pacific DST (PDT).
1156 Args:
1157 now: A pseudo-posix timestamp giving current time in PST.
1159 Returns:
1160 True if now falls within the range of DST, False otherwise.
1162 pst = time.gmtime(now)
1163 year = pst[0]
1164 assert year >= 2007
1166 begin = calendar.timegm((year, 3, 8, 2, 0, 0, 0, 0, 0))
1167 while time.gmtime(begin).tm_wday != SUNDAY:
1168 begin += DAY
1170 end = calendar.timegm((year, 11, 1, 2, 0, 0, 0, 0, 0))
1171 while time.gmtime(end).tm_wday != SUNDAY:
1172 end += DAY
1173 return begin <= now < end
1176 def CopyReversedLines(instream, outstream, blocksize=2**16):
1177 r"""Copy lines from input stream to output stream in reverse order.
1179 As a special feature, null bytes in the input are turned into
1180 newlines followed by tabs in the output, but these 'sub-lines'
1181 separated by null bytes are not reversed. E.g. If the input is
1182 'A\0B\nC\0D\n', the output is 'C\n\tD\nA\n\tB\n'.
1184 Args:
1185 instream: A seekable stream open for reading in binary mode.
1186 outstream: A stream open for writing; doesn't have to be seekable or binary.
1187 blocksize: Optional block size for buffering, for unit testing.
1189 Returns:
1190 The number of lines copied.
1192 line_count = 0
1193 instream.seek(0, 2)
1194 last_block = instream.tell() // blocksize
1195 spillover = ''
1196 for iblock in xrange(last_block + 1, -1, -1):
1197 instream.seek(iblock * blocksize)
1198 data = instream.read(blocksize)
1199 lines = data.splitlines(True)
1200 lines[-1:] = ''.join(lines[-1:] + [spillover]).splitlines(True)
1201 if lines and not lines[-1].endswith('\n'):
1203 lines[-1] += '\n'
1204 lines.reverse()
1205 if lines and iblock > 0:
1206 spillover = lines.pop()
1207 if lines:
1208 line_count += len(lines)
1209 data = ''.join(lines).replace('\0', '\n\t')
1210 outstream.write(data)
1211 return line_count
1214 def FindSentinel(filename, blocksize=2**16):
1215 """Return the sentinel line from the output file.
1217 Args:
1218 filename: The filename of the output file. (We'll read this file.)
1219 blocksize: Optional block size for buffering, for unit testing.
1221 Returns:
1222 The contents of the last line in the file that doesn't start with
1223 a tab, with its trailing newline stripped; or None if the file
1224 couldn't be opened or no such line could be found by inspecting
1225 the last 'blocksize' bytes of the file.
1227 if filename == '-':
1228 StatusUpdate('Can\'t combine --append with output to stdout.')
1229 sys.exit(2)
1230 try:
1231 fp = open(filename, 'rb')
1232 except IOError, err:
1233 StatusUpdate('Append mode disabled: can\'t read %r: %s.' % (filename, err))
1234 return None
1235 try:
1236 fp.seek(0, 2)
1237 fp.seek(max(0, fp.tell() - blocksize))
1238 lines = fp.readlines()
1239 del lines[:1]
1240 sentinel = None
1241 for line in lines:
1242 if not line.startswith('\t'):
1243 sentinel = line
1244 if not sentinel:
1246 StatusUpdate('Append mode disabled: can\'t find sentinel in %r.' %
1247 filename)
1248 return None
1249 return sentinel.rstrip('\n')
1250 finally:
1251 fp.close()
1254 class UploadBatcher(object):
1255 """Helper to batch file uploads."""
1257 def __init__(self, what, rpcserver, params):
1258 """Constructor.
1260 Args:
1261 what: Either 'file' or 'blob' or 'errorblob' indicating what kind of
1262 objects this batcher uploads. Used in messages and URLs.
1263 rpcserver: The RPC server.
1264 params: A dictionary object containing URL params to add to HTTP requests.
1266 assert what in ('file', 'blob', 'errorblob'), repr(what)
1267 self.what = what
1268 self.params = params
1269 self.rpcserver = rpcserver
1270 self.single_url = '/api/appversion/add' + what
1271 self.batch_url = self.single_url + 's'
1272 self.batching = True
1273 self.batch = []
1274 self.batch_size = 0
1276 def SendBatch(self):
1277 """Send the current batch on its way.
1279 If successful, resets self.batch and self.batch_size.
1281 Raises:
1282 HTTPError with code=404 if the server doesn't support batching.
1284 boundary = 'boundary'
1285 parts = []
1286 for path, payload, mime_type in self.batch:
1287 while boundary in payload:
1288 boundary += '%04x' % random.randint(0, 0xffff)
1289 assert len(boundary) < 80, 'Unexpected error, please try again.'
1290 part = '\n'.join(['',
1291 'X-Appcfg-File: %s' % urllib.quote(path),
1292 'X-Appcfg-Hash: %s' % _Hash(payload),
1293 'Content-Type: %s' % mime_type,
1294 'Content-Length: %d' % len(payload),
1295 'Content-Transfer-Encoding: 8bit',
1297 payload,
1299 parts.append(part)
1300 parts.insert(0,
1301 'MIME-Version: 1.0\n'
1302 'Content-Type: multipart/mixed; boundary="%s"\n'
1303 '\n'
1304 'This is a message with multiple parts in MIME format.' %
1305 boundary)
1306 parts.append('--\n')
1307 delimiter = '\n--%s' % boundary
1308 payload = delimiter.join(parts)
1309 logging.info('Uploading batch of %d %ss to %s with boundary="%s".',
1310 len(self.batch), self.what, self.batch_url, boundary)
1311 self.rpcserver.Send(self.batch_url,
1312 payload=payload,
1313 content_type='message/rfc822',
1314 **self.params)
1315 self.batch = []
1316 self.batch_size = 0
1318 def SendSingleFile(self, path, payload, mime_type):
1319 """Send a single file on its way."""
1320 logging.info('Uploading %s %s (%s bytes, type=%s) to %s.',
1321 self.what, path, len(payload), mime_type, self.single_url)
1322 self.rpcserver.Send(self.single_url,
1323 payload=payload,
1324 content_type=mime_type,
1325 path=path,
1326 **self.params)
1328 def Flush(self):
1329 """Flush the current batch.
1331 This first attempts to send the batch as a single request; if that
1332 fails because the server doesn't support batching, the files are
1333 sent one by one, and self.batching is reset to False.
1335 At the end, self.batch and self.batch_size are reset.
1337 if not self.batch:
1338 return
1339 try:
1340 self.SendBatch()
1341 except urllib2.HTTPError, err:
1342 if err.code != 404:
1343 raise
1346 logging.info('Old server detected; turning off %s batching.', self.what)
1347 self.batching = False
1350 for path, payload, mime_type in self.batch:
1351 self.SendSingleFile(path, payload, mime_type)
1354 self.batch = []
1355 self.batch_size = 0
1357 def AddToBatch(self, path, payload, mime_type):
1358 """Batch a file, possibly flushing first, or perhaps upload it directly.
1360 Args:
1361 path: The name of the file.
1362 payload: The contents of the file.
1363 mime_type: The MIME Content-type of the file, or None.
1365 If mime_type is None, application/octet-stream is substituted.
1367 if not mime_type:
1368 mime_type = 'application/octet-stream'
1369 size = len(payload)
1370 if size <= MAX_BATCH_FILE_SIZE:
1371 if (len(self.batch) >= MAX_BATCH_COUNT or
1372 self.batch_size + size > MAX_BATCH_SIZE):
1373 self.Flush()
1374 if self.batching:
1375 logging.info('Adding %s %s (%s bytes, type=%s) to batch.',
1376 self.what, path, size, mime_type)
1377 self.batch.append((path, payload, mime_type))
1378 self.batch_size += size + BATCH_OVERHEAD
1379 return
1380 self.SendSingleFile(path, payload, mime_type)
1383 def _FormatHash(h):
1384 """Return a string representation of a hash.
1386 The hash is a sha1 hash. It is computed both for files that need to be
1387 pushed to App Engine and for data payloads of requests made to App Engine.
1389 Args:
1390 h: The hash
1392 Returns:
1393 The string representation of the hash.
1395 return '%s_%s_%s_%s_%s' % (h[0:8], h[8:16], h[16:24], h[24:32], h[32:40])
1398 def _Hash(content):
1399 """Compute the sha1 hash of the content.
1401 Args:
1402 content: The data to hash as a string.
1404 Returns:
1405 The string representation of the hash.
1407 h = hashlib.sha1(content).hexdigest()
1408 return _FormatHash(h)
1411 def _HashFromFileHandle(file_handle):
1412 """Compute the hash of the content of the file pointed to by file_handle.
1414 Args:
1415 file_handle: File-like object which provides seek, read and tell.
1417 Returns:
1418 The string representation of the hash.
1427 pos = file_handle.tell()
1428 content_hash = _Hash(file_handle.read())
1429 file_handle.seek(pos, 0)
1430 return content_hash
1433 def EnsureDir(path):
1434 """Makes sure that a directory exists at the given path.
1436 If a directory already exists at that path, nothing is done.
1437 Otherwise, try to create a directory at that path with os.makedirs.
1438 If that fails, propagate the resulting OSError exception.
1440 Args:
1441 path: The path that you want to refer to a directory.
1443 try:
1444 os.makedirs(path)
1445 except OSError, exc:
1448 if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
1449 raise
1452 def DoDownloadApp(rpcserver, out_dir, app_id, module, app_version):
1453 """Downloads the files associated with a particular app version.
1455 Args:
1456 rpcserver: The RPC server to use to download.
1457 out_dir: The directory the files should be downloaded to.
1458 app_id: The app ID of the app whose files we want to download.
1459 module: The module we want to download from. Can be:
1460 - None: We'll download from the default module.
1461 - <module>: We'll download from the specified module.
1462 app_version: The version number we want to download. Can be:
1463 - None: We'll download the latest default version.
1464 - <major>: We'll download the latest minor version.
1465 - <major>/<minor>: We'll download that exact version.
1468 StatusUpdate('Fetching file list...')
1470 url_args = {'app_id': app_id}
1471 if module:
1472 url_args['module'] = module
1473 if app_version is not None:
1474 url_args['version_match'] = app_version
1476 result = rpcserver.Send('/api/files/list', **url_args)
1478 StatusUpdate('Fetching files...')
1480 lines = result.splitlines()
1482 if len(lines) < 1:
1483 logging.error('Invalid response from server: empty')
1484 return
1486 full_version = lines[0]
1487 file_lines = lines[1:]
1489 current_file_number = 0
1490 num_files = len(file_lines)
1492 num_errors = 0
1494 for line in file_lines:
1495 parts = line.split('|', 2)
1496 if len(parts) != 3:
1497 logging.error('Invalid response from server: expecting '
1498 '"<id>|<size>|<path>", found: "%s"\n', line)
1499 return
1501 current_file_number += 1
1503 file_id, size_str, path = parts
1504 try:
1505 size = int(size_str)
1506 except ValueError:
1507 logging.error('Invalid file list entry from server: invalid size: '
1508 '"%s"', size_str)
1509 return
1511 StatusUpdate('[%d/%d] %s' % (current_file_number, num_files, path))
1513 def TryGet():
1514 """A request to /api/files/get which works with the RetryWithBackoff."""
1515 try:
1516 contents = rpcserver.Send('/api/files/get', app_id=app_id,
1517 version=full_version, id=file_id)
1518 return True, contents
1519 except urllib2.HTTPError, exc:
1522 if exc.code == 503:
1523 return False, exc
1524 else:
1525 raise
1527 def PrintRetryMessage(_, delay):
1528 StatusUpdate('Server busy. Will try again in %d seconds.' % delay)
1530 success, contents = RetryWithBackoff(TryGet, PrintRetryMessage)
1531 if not success:
1532 logging.error('Unable to download file "%s".', path)
1533 num_errors += 1
1534 continue
1536 if len(contents) != size:
1537 logging.error('File "%s": server listed as %d bytes but served '
1538 '%d bytes.', path, size, len(contents))
1539 num_errors += 1
1541 full_path = os.path.join(out_dir, path)
1543 if os.path.exists(full_path):
1544 logging.error('Unable to create file "%s": path conflicts with '
1545 'an existing file or directory', path)
1546 num_errors += 1
1547 continue
1549 full_dir = os.path.dirname(full_path)
1550 try:
1551 EnsureDir(full_dir)
1552 except OSError, exc:
1553 logging.error('Couldn\'t create directory "%s": %s', full_dir, exc)
1554 num_errors += 1
1555 continue
1557 try:
1558 out_file = open(full_path, 'wb')
1559 except IOError, exc:
1560 logging.error('Couldn\'t open file "%s": %s', full_path, exc)
1561 num_errors += 1
1562 continue
1564 try:
1565 try:
1566 out_file.write(contents)
1567 except IOError, exc:
1568 logging.error('Couldn\'t write to file "%s": %s', full_path, exc)
1569 num_errors += 1
1570 continue
1571 finally:
1572 out_file.close()
1574 if num_errors > 0:
1575 logging.error('Number of errors: %d. See output for details.', num_errors)
1578 class AppVersionUpload(object):
1579 """Provides facilities to upload a new appversion to the hosting service.
1581 Attributes:
1582 rpcserver: The AbstractRpcServer to use for the upload.
1583 config: The AppInfoExternal object derived from the app.yaml file.
1584 app_id: The application string from 'config'.
1585 version: The version string from 'config'.
1586 backend: The backend to update, if any.
1587 files: A dictionary of files to upload to the rpcserver, mapping path to
1588 hash of the file contents.
1589 in_transaction: True iff a transaction with the server has started.
1590 An AppVersionUpload can do only one transaction at a time.
1591 deployed: True iff the Deploy method has been called.
1592 started: True iff the StartServing method has been called.
1595 def __init__(self, rpcserver, config, module_yaml_path='app.yaml',
1596 backend=None,
1597 error_fh=None,
1598 get_version=sdk_update_checker.GetVersionObject):
1599 """Creates a new AppVersionUpload.
1601 Args:
1602 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1603 or TestRpcServer.
1604 config: An AppInfoExternal object that specifies the configuration for
1605 this application.
1606 module_yaml_path: The (string) path to the yaml file corresponding to
1607 <config>, relative to the bundle directory.
1608 backend: If specified, indicates the update applies to the given backend.
1609 The backend name must match an entry in the backends: stanza.
1610 error_fh: Unexpected HTTPErrors are printed to this file handle.
1611 get_version: Method for determining the current SDK version. The override
1612 is used for testing.
1614 self.rpcserver = rpcserver
1615 self.config = config
1616 self.app_id = self.config.application
1617 self.module = self.config.module
1618 self.backend = backend
1619 self.error_fh = error_fh or sys.stderr
1621 self.version = self.config.version
1623 self.params = {}
1624 if self.app_id:
1625 self.params['app_id'] = self.app_id
1626 if self.module:
1627 self.params['module'] = self.module
1628 if self.backend:
1629 self.params['backend'] = self.backend
1630 elif self.version:
1631 self.params['version'] = self.version
1636 self.files = {}
1639 self.all_files = set()
1641 self.in_transaction = False
1642 self.deployed = False
1643 self.started = False
1644 self.batching = True
1645 self.file_batcher = UploadBatcher('file', self.rpcserver, self.params)
1646 self.blob_batcher = UploadBatcher('blob', self.rpcserver, self.params)
1647 self.errorblob_batcher = UploadBatcher('errorblob', self.rpcserver,
1648 self.params)
1650 if not self.config.vm_settings:
1651 self.config.vm_settings = appinfo.VmSettings()
1652 self.config.vm_settings['module_yaml_path'] = module_yaml_path
1654 if not self.config.vm_settings.get('image'):
1655 sdk_version = get_version()
1656 if sdk_version and sdk_version.get('release'):
1657 self.config.vm_settings['image'] = sdk_version['release']
1659 if not self.config.auto_id_policy:
1660 self.config.auto_id_policy = appinfo.DATASTORE_ID_POLICY_DEFAULT
1662 def Send(self, url, payload=''):
1663 """Sends a request to the server, with common params."""
1664 logging.info('Send: %s, params=%s', url, self.params)
1665 return self.rpcserver.Send(url, payload=payload, **self.params)
1667 def AddFile(self, path, file_handle):
1668 """Adds the provided file to the list to be pushed to the server.
1670 Args:
1671 path: The path the file should be uploaded as.
1672 file_handle: A stream containing data to upload.
1674 assert not self.in_transaction, 'Already in a transaction.'
1675 assert file_handle is not None
1677 reason = appinfo.ValidFilename(path)
1678 if reason:
1679 logging.error(reason)
1680 return
1682 content_hash = _HashFromFileHandle(file_handle)
1684 self.files[path] = content_hash
1685 self.all_files.add(path)
1687 def Describe(self):
1688 """Returns a string describing the object being updated."""
1689 result = 'app: %s' % self.app_id
1690 if self.module is not None and self.module != appinfo.DEFAULT_MODULE:
1691 result += ', module: %s' % self.module
1692 if self.backend:
1693 result += ', backend: %s' % self.backend
1694 elif self.version:
1695 result += ', version: %s' % self.version
1696 return result
1698 @staticmethod
1699 def _ValidateBeginYaml(resp):
1700 """Validates the given /api/appversion/create response string."""
1701 response_dict = yaml.safe_load(resp)
1702 if not response_dict or 'warnings' not in response_dict:
1703 return False
1704 return response_dict
1706 def Begin(self):
1707 """Begins the transaction, returning a list of files that need uploading.
1709 All calls to AddFile must be made before calling Begin().
1711 Returns:
1712 A list of pathnames for files that should be uploaded using UploadFile()
1713 before Commit() can be called.
1715 assert not self.in_transaction, 'Already in a transaction.'
1720 config_copy = copy.deepcopy(self.config)
1721 for url in config_copy.handlers:
1722 handler_type = url.GetHandlerType()
1723 if url.application_readable:
1726 if handler_type == 'static_dir':
1727 url.static_dir = '%s/%s' % (STATIC_FILE_PREFIX, url.static_dir)
1728 elif handler_type == 'static_files':
1729 url.static_files = '%s/%s' % (STATIC_FILE_PREFIX, url.static_files)
1730 url.upload = '%s/%s' % (STATIC_FILE_PREFIX, url.upload)
1732 response = self.Send(
1733 '/api/appversion/create',
1734 payload=config_copy.ToYAML())
1736 result = self._ValidateBeginYaml(response)
1737 if result:
1738 warnings = result.get('warnings')
1739 for warning in warnings:
1740 StatusUpdate('WARNING: %s' % warning)
1742 self.in_transaction = True
1744 files_to_clone = []
1745 blobs_to_clone = []
1746 errorblobs = {}
1747 for path, content_hash in self.files.iteritems():
1748 file_classification = FileClassification(self.config, path)
1750 if file_classification.IsStaticFile():
1751 upload_path = path
1752 if file_classification.IsApplicationFile():
1753 upload_path = '%s/%s' % (STATIC_FILE_PREFIX, path)
1754 blobs_to_clone.append((path, upload_path, content_hash,
1755 file_classification.StaticMimeType()))
1759 if file_classification.IsErrorFile():
1763 errorblobs[path] = content_hash
1765 if file_classification.IsApplicationFile():
1766 files_to_clone.append((path, path, content_hash))
1768 files_to_upload = {}
1770 def CloneFiles(url, files, file_type):
1771 """Sends files to the given url.
1773 Args:
1774 url: the server URL to use.
1775 files: a list of files
1776 file_type: the type of the files
1778 if not files:
1779 return
1781 StatusUpdate('Cloning %d %s file%s.' %
1782 (len(files), file_type, len(files) != 1 and 's' or ''))
1784 max_files = self.resource_limits['max_files_to_clone']
1785 for i in xrange(0, len(files), max_files):
1786 if i > 0 and i % max_files == 0:
1787 StatusUpdate('Cloned %d files.' % i)
1789 chunk = files[i:min(len(files), i + max_files)]
1790 result = self.Send(url, payload=BuildClonePostBody(chunk))
1791 if result:
1792 to_upload = {}
1793 for f in result.split(LIST_DELIMITER):
1794 for entry in files:
1795 real_path, upload_path = entry[:2]
1796 if f == upload_path:
1797 to_upload[real_path] = self.files[real_path]
1798 break
1799 files_to_upload.update(to_upload)
1801 CloneFiles('/api/appversion/cloneblobs', blobs_to_clone, 'static')
1802 CloneFiles('/api/appversion/clonefiles', files_to_clone, 'application')
1804 logging.debug('Files to upload: %s', files_to_upload)
1806 for (path, content_hash) in errorblobs.iteritems():
1807 files_to_upload[path] = content_hash
1808 self.files = files_to_upload
1809 return sorted(files_to_upload.iterkeys())
1811 def UploadFile(self, path, file_handle):
1812 """Uploads a file to the hosting service.
1814 Must only be called after Begin().
1815 The path provided must be one of those that were returned by Begin().
1817 Args:
1818 path: The path the file is being uploaded as.
1819 file_handle: A file-like object containing the data to upload.
1821 Raises:
1822 KeyError: The provided file is not amongst those to be uploaded.
1824 assert self.in_transaction, 'Begin() must be called before UploadFile().'
1825 if path not in self.files:
1826 raise KeyError('File \'%s\' is not in the list of files to be uploaded.'
1827 % path)
1829 del self.files[path]
1831 file_classification = FileClassification(self.config, path)
1832 payload = file_handle.read()
1833 if file_classification.IsStaticFile():
1834 upload_path = path
1835 if file_classification.IsApplicationFile():
1836 upload_path = '%s/%s' % (STATIC_FILE_PREFIX, path)
1837 self.blob_batcher.AddToBatch(upload_path, payload,
1838 file_classification.StaticMimeType())
1842 if file_classification.IsErrorFile():
1845 self.errorblob_batcher.AddToBatch(file_classification.ErrorCode(),
1846 payload,
1847 file_classification.ErrorMimeType())
1849 if file_classification.IsApplicationFile():
1851 self.file_batcher.AddToBatch(path, payload, None)
1853 def Precompile(self):
1854 """Handle precompilation."""
1856 StatusUpdate('Compilation starting.')
1858 files = []
1859 if self.config.runtime == 'go':
1862 for f in self.all_files:
1863 if f.endswith('.go') and not self.config.nobuild_files.match(f):
1864 files.append(f)
1866 while True:
1867 if files:
1868 StatusUpdate('Compilation: %d files left.' % len(files))
1869 files = self.PrecompileBatch(files)
1870 if not files:
1871 break
1872 StatusUpdate('Compilation completed.')
1874 def PrecompileBatch(self, files):
1875 """Precompile a batch of files.
1877 Args:
1878 files: Either an empty list (for the initial request) or a list
1879 of files to be precompiled.
1881 Returns:
1882 Either an empty list (if no more files need to be precompiled)
1883 or a list of files to be precompiled subsequently.
1885 payload = LIST_DELIMITER.join(files)
1886 response = self.Send('/api/appversion/precompile', payload=payload)
1887 if not response:
1888 return []
1889 return response.split(LIST_DELIMITER)
1891 def Commit(self):
1892 """Commits the transaction, making the new app version available.
1894 All the files returned by Begin() must have been uploaded with UploadFile()
1895 before Commit() can be called.
1897 This tries the new 'deploy' method; if that fails it uses the old 'commit'.
1899 Returns:
1900 An appinfo.AppInfoSummary if one was returned from the Deploy, None
1901 otherwise.
1903 Raises:
1904 RuntimeError: Some required files were not uploaded.
1905 CannotStartServingError: Another operation is in progress on this version.
1907 assert self.in_transaction, 'Begin() must be called before Commit().'
1908 if self.files:
1909 raise RuntimeError('Not all required files have been uploaded.')
1911 def PrintRetryMessage(_, delay):
1912 StatusUpdate('Will check again in %s seconds.' % delay)
1914 app_summary = None
1916 app_summary = self.Deploy()
1919 success, unused_contents = RetryWithBackoff(
1920 lambda: (self.IsReady(), None), PrintRetryMessage, 1, 2, 60, 20)
1921 if not success:
1923 logging.warning('Version still not ready to serve, aborting.')
1924 raise RuntimeError('Version not ready.')
1926 result = self.StartServing()
1927 if not result:
1930 self.in_transaction = False
1931 else:
1932 if result == '0':
1933 raise CannotStartServingError(
1934 'Another operation on this version is in progress.')
1935 success, response = RetryWithBackoff(
1936 self.IsServing, PrintRetryMessage, 1, 2, 60, 20)
1937 if not success:
1939 logging.warning('Version still not serving, aborting.')
1940 raise RuntimeError('Version not ready.')
1944 check_config_updated = response.get('check_endpoints_config')
1945 if check_config_updated:
1946 success, unused_contents = RetryWithBackoff(
1947 lambda: (self.IsEndpointsConfigUpdated(), None),
1948 PrintRetryMessage, 1, 2, 60, 20)
1949 if not success:
1950 logging.warning('Failed to update Endpoints configuration. Try '
1951 'updating again.')
1952 raise RuntimeError('Endpoints config update failed.')
1953 self.in_transaction = False
1955 return app_summary
1957 def Deploy(self):
1958 """Deploys the new app version but does not make it default.
1960 All the files returned by Begin() must have been uploaded with UploadFile()
1961 before Deploy() can be called.
1963 Returns:
1964 An appinfo.AppInfoSummary if one was returned from the Deploy, None
1965 otherwise.
1967 Raises:
1968 RuntimeError: Some required files were not uploaded.
1970 assert self.in_transaction, 'Begin() must be called before Deploy().'
1971 if self.files:
1972 raise RuntimeError('Not all required files have been uploaded.')
1974 StatusUpdate('Starting deployment.')
1975 result = self.Send('/api/appversion/deploy')
1976 self.deployed = True
1978 if result:
1979 return yaml_object.BuildSingleObject(appinfo.AppInfoSummary, result)
1980 else:
1981 return None
1983 def IsReady(self):
1984 """Check if the new app version is ready to serve traffic.
1986 Raises:
1987 RuntimeError: Deploy has not yet been called.
1989 Returns:
1990 True if the server returned the app is ready to serve.
1992 assert self.deployed, 'Deploy() must be called before IsReady().'
1994 StatusUpdate('Checking if deployment succeeded.')
1995 result = self.Send('/api/appversion/isready')
1996 return result == '1'
1998 def StartServing(self):
1999 """Start serving with the newly created version.
2001 Raises:
2002 RuntimeError: Deploy has not yet been called.
2004 Returns:
2005 The response body, as a string.
2007 assert self.deployed, 'Deploy() must be called before StartServing().'
2009 StatusUpdate('Deployment successful.')
2010 self.params['willcheckserving'] = '1'
2011 result = self.Send('/api/appversion/startserving')
2012 del self.params['willcheckserving']
2013 self.started = True
2014 return result
2016 @staticmethod
2017 def _ValidateIsServingYaml(resp):
2018 """Validates the given /isserving YAML string.
2020 Args:
2021 resp: the response from an RPC to a URL such as /api/appversion/isserving.
2023 Returns:
2024 The resulting dictionary if the response is valid, or None otherwise.
2026 response_dict = yaml.safe_load(resp)
2027 if 'serving' not in response_dict:
2028 return None
2029 return response_dict
2031 def IsServing(self):
2032 """Check if the new app version is serving.
2034 Raises:
2035 RuntimeError: Deploy has not yet been called.
2036 CannotStartServingError: A bad response was received from the isserving
2037 API call.
2039 Returns:
2040 (serving, response) Where serving is True if the deployed app version is
2041 serving, False otherwise. response is a dict containing the parsed
2042 response from the server, or an empty dict if the server's response was
2043 an old style 0/1 response.
2045 assert self.started, 'StartServing() must be called before IsServing().'
2047 StatusUpdate('Checking if updated app version is serving.')
2049 self.params['new_serving_resp'] = '1'
2050 result = self.Send('/api/appversion/isserving')
2051 del self.params['new_serving_resp']
2052 if result in ['0', '1']:
2053 return result == '1', {}
2054 result = AppVersionUpload._ValidateIsServingYaml(result)
2055 if not result:
2056 raise CannotStartServingError(
2057 'Internal error: Could not parse IsServing response.')
2058 message = result.get('message')
2059 fatal = result.get('fatal')
2060 if message:
2061 StatusUpdate(message)
2062 if fatal:
2063 raise CannotStartServingError(fatal)
2064 return result['serving'], result
2066 @staticmethod
2067 def _ValidateIsEndpointsConfigUpdatedYaml(resp):
2068 """Validates the YAML string response from an isconfigupdated request.
2070 Args:
2071 resp: A string containing the response from the server.
2073 Returns:
2074 The dictionary with the parsed response if the response is valid.
2075 Otherwise returns False.
2077 response_dict = yaml.safe_load(resp)
2078 if 'updated' not in response_dict:
2079 return None
2080 return response_dict
2082 def IsEndpointsConfigUpdated(self):
2083 """Check if the Endpoints configuration for this app has been updated.
2085 This should only be called if the app has a Google Cloud Endpoints
2086 handler, or if it's removing one. The server performs the check to see
2087 if Endpoints support is added/updated/removed, and the response to the
2088 isserving call indicates whether IsEndpointsConfigUpdated should be called.
2090 Raises:
2091 AssertionError: Deploy has not yet been called.
2092 CannotStartServingError: There was an unexpected error with the server
2093 response.
2095 Returns:
2096 True if the configuration has been updated, False if not.
2099 assert self.started, ('StartServing() must be called before '
2100 'IsEndpointsConfigUpdated().')
2102 StatusUpdate('Checking if Endpoints configuration has been updated.')
2104 result = self.Send('/api/isconfigupdated')
2105 result = AppVersionUpload._ValidateIsEndpointsConfigUpdatedYaml(result)
2106 if result is None:
2107 raise CannotStartServingError(
2108 'Internal error: Could not parse IsEndpointsConfigUpdated response.')
2109 return result['updated']
2111 def Rollback(self):
2112 """Rolls back the transaction if one is in progress."""
2113 if not self.in_transaction:
2114 return
2115 StatusUpdate('Rolling back the update.')
2116 self.Send('/api/appversion/rollback')
2117 self.in_transaction = False
2118 self.files = {}
2120 def DoUpload(self, paths, openfunc):
2121 """Uploads a new appversion with the given config and files to the server.
2123 Args:
2124 paths: An iterator that yields the relative paths of the files to upload.
2125 openfunc: A function that takes a path and returns a file-like object.
2127 Returns:
2128 An appinfo.AppInfoSummary if one was returned from the server, None
2129 otherwise.
2131 logging.info('Reading app configuration.')
2133 StatusUpdate('\nStarting update of %s' % self.Describe())
2136 path = ''
2137 try:
2138 self.resource_limits = GetResourceLimits(self.rpcserver, self.config)
2140 StatusUpdate('Scanning files on local disk.')
2141 num_files = 0
2142 for path in paths:
2143 file_handle = openfunc(path)
2144 file_classification = FileClassification(self.config, path)
2145 try:
2146 file_length = GetFileLength(file_handle)
2147 if file_classification.IsApplicationFile():
2148 max_size = self.resource_limits['max_file_size']
2149 else:
2150 max_size = self.resource_limits['max_blob_size']
2151 if file_length > max_size:
2152 logging.error('Ignoring file \'%s\': Too long '
2153 '(max %d bytes, file is %d bytes)',
2154 path, max_size, file_length)
2155 else:
2156 logging.info('Processing file \'%s\'', path)
2157 self.AddFile(path, file_handle)
2158 finally:
2159 file_handle.close()
2160 num_files += 1
2161 if num_files % 500 == 0:
2162 StatusUpdate('Scanned %d files.' % num_files)
2163 except KeyboardInterrupt:
2164 logging.info('User interrupted. Aborting.')
2165 raise
2166 except EnvironmentError, e:
2167 logging.error('An error occurred processing file \'%s\': %s. Aborting.',
2168 path, e)
2169 raise
2171 app_summary = None
2172 try:
2174 missing_files = self.Begin()
2175 if missing_files:
2176 StatusUpdate('Uploading %d files and blobs.' % len(missing_files))
2177 num_files = 0
2178 for missing_file in missing_files:
2179 file_handle = openfunc(missing_file)
2180 try:
2181 self.UploadFile(missing_file, file_handle)
2182 finally:
2183 file_handle.close()
2184 num_files += 1
2185 if num_files % 500 == 0:
2186 StatusUpdate('Processed %d out of %s.' %
2187 (num_files, len(missing_files)))
2189 self.file_batcher.Flush()
2190 self.blob_batcher.Flush()
2191 self.errorblob_batcher.Flush()
2192 StatusUpdate('Uploaded %d files and blobs' % num_files)
2195 if (self.config.derived_file_type and
2196 appinfo.PYTHON_PRECOMPILED in self.config.derived_file_type):
2197 try:
2198 self.Precompile()
2199 except urllib2.HTTPError, e:
2201 ErrorUpdate('Error %d: --- begin server output ---\n'
2202 '%s\n--- end server output ---' %
2203 (e.code, e.read().rstrip('\n')))
2204 if e.code == 422 or self.config.runtime == 'go':
2211 raise
2212 print >>self.error_fh, (
2213 'Precompilation failed. Your app can still serve but may '
2214 'have reduced startup performance. You can retry the update '
2215 'later to retry the precompilation step.')
2218 app_summary = self.Commit()
2219 StatusUpdate('Completed update of %s' % self.Describe())
2221 except KeyboardInterrupt:
2223 logging.info('User interrupted. Aborting.')
2224 self.Rollback()
2225 raise
2226 except urllib2.HTTPError, err:
2228 logging.info('HTTP Error (%s)', err)
2229 self.Rollback()
2230 raise
2231 except CannotStartServingError, err:
2233 logging.error(err.message)
2234 self.Rollback()
2235 raise
2236 except:
2237 logging.exception('An unexpected error occurred. Aborting.')
2238 self.Rollback()
2239 raise
2241 logging.info('Done!')
2242 return app_summary
2245 def FileIterator(base, skip_files, runtime, separator=os.path.sep):
2246 """Walks a directory tree, returning all the files. Follows symlinks.
2248 Args:
2249 base: The base path to search for files under.
2250 skip_files: A regular expression object for files/directories to skip.
2251 runtime: The name of the runtime e.g. "python". If "python27" then .pyc
2252 files with matching .py files will be skipped.
2253 separator: Path separator used by the running system's platform.
2255 Yields:
2256 Paths of files found, relative to base.
2258 dirs = ['']
2259 while dirs:
2260 current_dir = dirs.pop()
2261 entries = set(os.listdir(os.path.join(base, current_dir)))
2262 for entry in sorted(entries):
2263 name = os.path.join(current_dir, entry)
2264 fullname = os.path.join(base, name)
2269 if separator == '\\':
2270 name = name.replace('\\', '/')
2272 if runtime == 'python27' and not skip_files.match(name):
2273 root, extension = os.path.splitext(entry)
2274 if extension == '.pyc' and (root + '.py') in entries:
2275 logging.warning('Ignoring file \'%s\': Cannot upload both '
2276 '<filename>.py and <filename>.pyc', name)
2277 continue
2279 if os.path.isfile(fullname):
2280 if skip_files.match(name):
2281 logging.info('Ignoring file \'%s\': File matches ignore regex.', name)
2282 else:
2283 yield name
2284 elif os.path.isdir(fullname):
2285 if skip_files.match(name):
2286 logging.info(
2287 'Ignoring directory \'%s\': Directory matches ignore regex.',
2288 name)
2289 else:
2290 dirs.append(name)
2293 def GetFileLength(fh):
2294 """Returns the length of the file represented by fh.
2296 This function is capable of finding the length of any seekable stream,
2297 unlike os.fstat, which only works on file streams.
2299 Args:
2300 fh: The stream to get the length of.
2302 Returns:
2303 The length of the stream.
2305 pos = fh.tell()
2307 fh.seek(0, 2)
2308 length = fh.tell()
2309 fh.seek(pos, 0)
2310 return length
2313 def GetUserAgent(get_version=sdk_update_checker.GetVersionObject,
2314 get_platform=appengine_rpc.GetPlatformToken,
2315 sdk_product=SDK_PRODUCT):
2316 """Determines the value of the 'User-agent' header to use for HTTP requests.
2318 If the 'APPCFG_SDK_NAME' environment variable is present, that will be
2319 used as the first product token in the user-agent.
2321 Args:
2322 get_version: Used for testing.
2323 get_platform: Used for testing.
2324 sdk_product: Used as part of sdk/version product token.
2326 Returns:
2327 String containing the 'user-agent' header value, which includes the SDK
2328 version, the platform information, and the version of Python;
2329 e.g., 'appcfg_py/1.0.1 Darwin/9.2.0 Python/2.5.2'.
2331 product_tokens = []
2334 sdk_name = os.environ.get('APPCFG_SDK_NAME')
2335 if sdk_name:
2336 product_tokens.append(sdk_name)
2337 else:
2338 version = get_version()
2339 if version is None:
2340 release = 'unknown'
2341 else:
2342 release = version['release']
2344 product_tokens.append('%s/%s' % (sdk_product, release))
2347 product_tokens.append(get_platform())
2350 python_version = '.'.join(str(i) for i in sys.version_info)
2351 product_tokens.append('Python/%s' % python_version)
2353 return ' '.join(product_tokens)
2356 def GetSourceName(get_version=sdk_update_checker.GetVersionObject):
2357 """Gets the name of this source version."""
2358 version = get_version()
2359 if version is None:
2360 release = 'unknown'
2361 else:
2362 release = version['release']
2363 return 'Google-appcfg-%s' % (release,)
2366 def _ReadUrlContents(url):
2367 """Reads the contents of a URL into a string.
2369 Args:
2370 url: a string that is the URL to read.
2372 Returns:
2373 A string that is the contents read from the URL.
2375 Raises:
2376 urllib2.URLError: If the URL cannot be read.
2378 req = urllib2.Request(url)
2379 return urllib2.urlopen(req).read()
2382 class AppCfgApp(object):
2383 """Singleton class to wrap AppCfg tool functionality.
2385 This class is responsible for parsing the command line and executing
2386 the desired action on behalf of the user. Processing files and
2387 communicating with the server is handled by other classes.
2389 Attributes:
2390 actions: A dictionary mapping action names to Action objects.
2391 action: The Action specified on the command line.
2392 parser: An instance of optparse.OptionParser.
2393 options: The command line options parsed by 'parser'.
2394 argv: The original command line as a list.
2395 args: The positional command line args left over after parsing the options.
2396 raw_input_fn: Function used for getting raw user input, like email.
2397 password_input_fn: Function used for getting user password.
2398 error_fh: Unexpected HTTPErrors are printed to this file handle.
2400 Attributes for testing:
2401 parser_class: The class to use for parsing the command line. Because
2402 OptionsParser will exit the program when there is a parse failure, it
2403 is nice to subclass OptionsParser and catch the error before exiting.
2404 read_url_contents: A function to read the contents of a URL.
2405 override_java_supported: If not None, forces the code to assume that Java
2406 support is (True) or is not (False) present.
2409 def __init__(self, argv, parser_class=optparse.OptionParser,
2410 rpc_server_class=None,
2411 raw_input_fn=raw_input,
2412 password_input_fn=getpass.getpass,
2413 out_fh=sys.stdout,
2414 error_fh=sys.stderr,
2415 update_check_class=sdk_update_checker.SDKUpdateChecker,
2416 throttle_class=None,
2417 opener=open,
2418 file_iterator=FileIterator,
2419 time_func=time.time,
2420 wrap_server_error_message=True,
2421 oauth_client_id=APPCFG_CLIENT_ID,
2422 oauth_client_secret=APPCFG_CLIENT_NOTSOSECRET,
2423 oauth_scopes=APPCFG_SCOPES,
2424 override_java_supported=None):
2425 """Initializer. Parses the cmdline and selects the Action to use.
2427 Initializes all of the attributes described in the class docstring.
2428 Prints help or error messages if there is an error parsing the cmdline.
2430 Args:
2431 argv: The list of arguments passed to this program.
2432 parser_class: Options parser to use for this application.
2433 rpc_server_class: RPC server class to use for this application.
2434 raw_input_fn: Function used for getting user email.
2435 password_input_fn: Function used for getting user password.
2436 out_fh: All normal output is printed to this file handle.
2437 error_fh: Unexpected HTTPErrors are printed to this file handle.
2438 update_check_class: sdk_update_checker.SDKUpdateChecker class (can be
2439 replaced for testing).
2440 throttle_class: A class to use instead of ThrottledHttpRpcServer
2441 (only used in the bulkloader).
2442 opener: Function used for opening files.
2443 file_iterator: Callable that takes (basepath, skip_files, file_separator)
2444 and returns a generator that yields all filenames in the file tree
2445 rooted at that path, skipping files that match the skip_files compiled
2446 regular expression.
2447 time_func: A time.time() compatible function, which can be overridden for
2448 testing.
2449 wrap_server_error_message: If true, the error messages from
2450 urllib2.HTTPError exceptions in Run() are wrapped with
2451 '--- begin server output ---' and '--- end server output ---',
2452 otherwise the error message is printed as is.
2453 oauth_client_id: The client ID of the project providing Auth. Defaults to
2454 the SDK default project client ID, the constant APPCFG_CLIENT_ID.
2455 oauth_client_secret: The client secret of the project providing Auth.
2456 Defaults to the SDK default project client secret, the constant
2457 APPCFG_CLIENT_NOTSOSECRET.
2458 oauth_scopes: The scope or set of scopes to be accessed by the OAuth2
2459 token retrieved. Defaults to APPCFG_SCOPES. Can be a string or
2460 iterable of strings, representing the scope(s) to request.
2461 override_java_supported: If not None, forces the code to assume that Java
2462 support is (True) or is not (False) present.
2464 self.parser_class = parser_class
2465 self.argv = argv
2466 self.rpc_server_class = rpc_server_class
2467 self.raw_input_fn = raw_input_fn
2468 self.password_input_fn = password_input_fn
2469 self.out_fh = out_fh
2470 self.error_fh = error_fh
2471 self.update_check_class = update_check_class
2472 self.throttle_class = throttle_class
2473 self.time_func = time_func
2474 self.wrap_server_error_message = wrap_server_error_message
2475 self.oauth_client_id = oauth_client_id
2476 self.oauth_client_secret = oauth_client_secret
2477 self.oauth_scopes = oauth_scopes
2478 self.override_java_supported = override_java_supported
2480 self.read_url_contents = _ReadUrlContents
2486 self.parser = self._GetOptionParser()
2487 for action in self.actions.itervalues():
2488 action.options(self, self.parser)
2491 self.options, self.args = self.parser.parse_args(argv[1:])
2493 if len(self.args) < 1:
2494 self._PrintHelpAndExit()
2496 if self.options.allow_any_runtime:
2500 appinfo.AppInfoExternal._skip_runtime_checks = True
2501 else:
2502 if self.options.runtime:
2503 if self.options.runtime not in SUPPORTED_RUNTIMES:
2504 _PrintErrorAndExit(self.error_fh,
2505 '"%s" is not a supported runtime\n' %
2506 self.options.runtime)
2507 else:
2508 appinfo.AppInfoExternal.ATTRIBUTES[appinfo.RUNTIME] = (
2509 '|'.join(SUPPORTED_RUNTIMES))
2511 action = self.args.pop(0)
2513 def RaiseParseError(actionname, action):
2516 self.parser, self.options = self._MakeSpecificParser(action)
2517 error_desc = action.error_desc
2518 if not error_desc:
2519 error_desc = "Expected a <directory> argument after '%s'." % (
2520 actionname.split(' ')[0])
2521 self.parser.error(error_desc)
2526 if action == BACKENDS_ACTION:
2527 if len(self.args) < 1:
2528 RaiseParseError(action, self.actions[BACKENDS_ACTION])
2530 backend_action_first = BACKENDS_ACTION + ' ' + self.args[0]
2531 if backend_action_first in self.actions:
2532 self.args.pop(0)
2533 action = backend_action_first
2535 elif len(self.args) > 1:
2536 backend_directory_first = BACKENDS_ACTION + ' ' + self.args[1]
2537 if backend_directory_first in self.actions:
2538 self.args.pop(1)
2539 action = backend_directory_first
2542 if len(self.args) < 1 or action == BACKENDS_ACTION:
2543 RaiseParseError(action, self.actions[action])
2545 if action not in self.actions:
2546 self.parser.error("Unknown action: '%s'\n%s" %
2547 (action, self.parser.get_description()))
2550 self.action = self.actions[action]
2555 if not self.action.uses_basepath or self.options.help:
2556 self.basepath = None
2557 else:
2558 if not self.args:
2559 RaiseParseError(action, self.action)
2560 self.basepath = self.args.pop(0)
2566 self.parser, self.options = self._MakeSpecificParser(self.action)
2570 if self.options.help:
2571 self._PrintHelpAndExit()
2573 if self.options.verbose == 2:
2574 logging.getLogger().setLevel(logging.INFO)
2575 elif self.options.verbose == 3:
2576 logging.getLogger().setLevel(logging.DEBUG)
2581 global verbosity
2582 verbosity = self.options.verbose
2586 if any((self.options.oauth2_refresh_token, self.options.oauth2_access_token,
2587 self.options.authenticate_service_account)):
2588 self.options.oauth2 = True
2591 if self.options.oauth2_client_id:
2592 self.oauth_client_id = self.options.oauth2_client_id
2593 if self.options.oauth2_client_secret:
2594 self.oauth_client_secret = self.options.oauth2_client_secret
2599 self.opener = opener
2600 self.file_iterator = file_iterator
2602 def Run(self):
2603 """Executes the requested action.
2605 Catches any HTTPErrors raised by the action and prints them to stderr.
2607 Returns:
2608 1 on error, 0 if successful.
2610 try:
2611 self.action(self)
2612 except urllib2.HTTPError, e:
2613 body = e.read()
2614 if self.wrap_server_error_message:
2615 error_format = ('Error %d: --- begin server output ---\n'
2616 '%s\n--- end server output ---')
2617 else:
2618 error_format = 'Error %d: %s'
2620 print >>self.error_fh, (error_format % (e.code, body.rstrip('\n')))
2621 return 1
2622 except yaml_errors.EventListenerError, e:
2623 print >>self.error_fh, ('Error parsing yaml file:\n%s' % e)
2624 return 1
2625 except CannotStartServingError:
2626 print >>self.error_fh, 'Could not start serving the given version.'
2627 return 1
2628 return 0
2630 def _JavaSupported(self):
2631 """True if this SDK supports uploading Java apps."""
2632 if self.override_java_supported is not None:
2633 return self.override_java_supported
2634 tools_java_dir = os.path.join(os.path.dirname(appcfg_java.__file__), 'java')
2635 return os.path.isdir(tools_java_dir)
2637 def _GetActionDescriptions(self):
2638 """Returns a formatted string containing the short_descs for all actions."""
2639 action_names = self.actions.keys()
2640 action_names.sort()
2641 desc = ''
2642 for action_name in action_names:
2643 if not self.actions[action_name].hidden:
2644 desc += ' %s: %s\n' % (action_name,
2645 self.actions[action_name].short_desc)
2646 return desc
2648 def _GetOptionParser(self):
2649 """Creates an OptionParser with generic usage and description strings.
2651 Returns:
2652 An OptionParser instance.
2655 class Formatter(optparse.IndentedHelpFormatter):
2656 """Custom help formatter that does not reformat the description."""
2658 def format_description(self, description):
2659 """Very simple formatter."""
2660 return description + '\n'
2662 class AppCfgOption(optparse.Option):
2663 """Custom Option for AppCfg.
2665 Adds an 'update' action for storing key-value pairs as a dict.
2668 _ACTION = 'update'
2669 ACTIONS = optparse.Option.ACTIONS + (_ACTION,)
2670 STORE_ACTIONS = optparse.Option.STORE_ACTIONS + (_ACTION,)
2671 TYPED_ACTIONS = optparse.Option.TYPED_ACTIONS + (_ACTION,)
2672 ALWAYS_TYPED_ACTIONS = optparse.Option.ALWAYS_TYPED_ACTIONS + (_ACTION,)
2674 def take_action(self, action, dest, opt, value, values, parser):
2675 if action != self._ACTION:
2676 return optparse.Option.take_action(
2677 self, action, dest, opt, value, values, parser)
2678 try:
2679 key, value = value.split(':', 1)
2680 except ValueError:
2681 raise optparse.OptionValueError(
2682 'option %s: invalid value: %s (must match NAME:VALUE)' % (
2683 opt, value))
2684 values.ensure_value(dest, {})[key] = value
2686 desc = self._GetActionDescriptions()
2687 desc = ('Action must be one of:\n%s'
2688 'Use \'help <action>\' for a detailed description.') % desc
2692 parser = self.parser_class(usage='%prog [options] <action>',
2693 description=desc,
2694 formatter=Formatter(),
2695 conflict_handler='resolve',
2696 option_class=AppCfgOption)
2701 parser.add_option('-h', '--help', action='store_true',
2702 dest='help', help='Show the help message and exit.')
2703 parser.add_option('-q', '--quiet', action='store_const', const=0,
2704 dest='verbose', help='Print errors only.')
2705 parser.add_option('-v', '--verbose', action='store_const', const=2,
2706 dest='verbose', default=1,
2707 help='Print info level logs.')
2708 parser.add_option('--noisy', action='store_const', const=3,
2709 dest='verbose', help='Print all logs.')
2710 parser.add_option('-s', '--server', action='store', dest='server',
2711 default='appengine.google.com',
2712 metavar='SERVER', help='The App Engine server.')
2713 parser.add_option('--secure', action='store_true', dest='secure',
2714 default=True, help=optparse.SUPPRESS_HELP)
2715 parser.add_option('--ignore_bad_cert', action='store_true',
2716 dest='ignore_certs', default=False,
2717 help=optparse.SUPPRESS_HELP)
2718 parser.add_option('--insecure', action='store_false', dest='secure',
2719 help=optparse.SUPPRESS_HELP)
2720 parser.add_option('-e', '--email', action='store', dest='email',
2721 metavar='EMAIL', default=None,
2722 help='The username to use. Will prompt if omitted.')
2723 parser.add_option('-H', '--host', action='store', dest='host',
2724 metavar='HOST', default=None,
2725 help='Overrides the Host header sent with all RPCs.')
2726 parser.add_option('--no_cookies', action='store_false',
2727 dest='save_cookies', default=True,
2728 help='Do not save authentication cookies to local disk.')
2729 parser.add_option('--skip_sdk_update_check', action='store_true',
2730 dest='skip_sdk_update_check', default=False,
2731 help='Do not check for SDK updates.')
2732 parser.add_option('--passin', action='store_true',
2733 dest='passin', default=False,
2734 help='Read the login password from stdin.')
2735 parser.add_option('-A', '--application', action='store', dest='app_id',
2736 help=('Set the application, overriding the application '
2737 'value from app.yaml file.'))
2738 parser.add_option('-M', '--module', action='store', dest='module',
2739 help=optparse.SUPPRESS_HELP)
2740 parser.add_option('-V', '--version', action='store', dest='version',
2741 help=('Set the (major) version, overriding the version '
2742 'value from app.yaml file.'))
2743 parser.add_option('-r', '--runtime', action='store', dest='runtime',
2744 help='Override runtime from app.yaml file.')
2745 parser.add_option('-E', '--env_variable', action='update',
2746 dest='env_variables', metavar='NAME:VALUE',
2747 help=('Set an environment variable, potentially '
2748 'overriding an env_variable value from app.yaml '
2749 'file (flag may be repeated to set multiple '
2750 'variables).'))
2751 parser.add_option('-R', '--allow_any_runtime', action='store_true',
2752 dest='allow_any_runtime', default=False,
2753 help='Do not validate the runtime in app.yaml')
2754 parser.add_option('--oauth2', action='store_true', dest='oauth2',
2755 default=False,
2756 help='Use OAuth2 instead of password auth.')
2757 parser.add_option('--oauth2_refresh_token', action='store',
2758 dest='oauth2_refresh_token', default=None,
2759 help='An existing OAuth2 refresh token to use. Will '
2760 'not attempt interactive OAuth approval.')
2761 parser.add_option('--oauth2_access_token', action='store',
2762 dest='oauth2_access_token', default=None,
2763 help='An existing OAuth2 access token to use. Will '
2764 'not attempt interactive OAuth approval.')
2765 parser.add_option('--oauth2_client_id', action='store',
2766 dest='oauth2_client_id', default=None,
2767 help=optparse.SUPPRESS_HELP)
2768 parser.add_option('--oauth2_client_secret', action='store',
2769 dest='oauth2_client_secret', default=None,
2770 help=optparse.SUPPRESS_HELP)
2771 parser.add_option('--oauth2_credential_file', action='store',
2772 dest='oauth2_credential_file', default=None,
2773 help=optparse.SUPPRESS_HELP)
2774 parser.add_option('--authenticate_service_account', action='store_true',
2775 dest='authenticate_service_account', default=False,
2776 help='Authenticate using the default service account '
2777 'for the Google Compute Engine VM in which appcfg is '
2778 'being called')
2779 parser.add_option('--noauth_local_webserver', action='store_false',
2780 dest='auth_local_webserver', default=True,
2781 help='Do not run a local web server to handle redirects '
2782 'during OAuth authorization.')
2783 return parser
2785 def _MakeSpecificParser(self, action):
2786 """Creates a new parser with documentation specific to 'action'.
2788 Args:
2789 action: An Action instance to be used when initializing the new parser.
2791 Returns:
2792 A tuple containing:
2793 parser: An instance of OptionsParser customized to 'action'.
2794 options: The command line options after re-parsing.
2796 parser = self._GetOptionParser()
2797 parser.set_usage(action.usage)
2798 parser.set_description('%s\n%s' % (action.short_desc, action.long_desc))
2799 action.options(self, parser)
2800 options, unused_args = parser.parse_args(self.argv[1:])
2801 return parser, options
2803 def _PrintHelpAndExit(self, exit_code=2):
2804 """Prints the parser's help message and exits the program.
2806 Args:
2807 exit_code: The integer code to pass to sys.exit().
2809 self.parser.print_help()
2810 sys.exit(exit_code)
2812 def _GetRpcServer(self):
2813 """Returns an instance of an AbstractRpcServer.
2815 Returns:
2816 A new AbstractRpcServer, on which RPC calls can be made.
2818 Raises:
2819 OAuthNotAvailable: OAuth is requested but the dependecies aren't imported.
2820 RuntimeError: The user has request non-interactive authentication but the
2821 environment is not correct for that to work.
2824 def GetUserCredentials():
2825 """Prompts the user for a username and password."""
2826 email = self.options.email
2827 if email is None:
2828 email = self.raw_input_fn('Email: ')
2830 password_prompt = 'Password for %s: ' % email
2833 if self.options.passin:
2834 password = self.raw_input_fn(password_prompt)
2835 else:
2836 password = self.password_input_fn(password_prompt)
2838 return (email, password)
2840 StatusUpdate('Host: %s' % self.options.server)
2842 source = GetSourceName()
2846 dev_appserver = self.options.host == 'localhost'
2847 if self.options.oauth2 and not dev_appserver:
2848 if not appengine_rpc_httplib2:
2850 raise OAuthNotAvailable()
2851 if not self.rpc_server_class:
2852 self.rpc_server_class = appengine_rpc_httplib2.HttpRpcServerOAuth2
2855 get_user_credentials = (
2856 appengine_rpc_httplib2.HttpRpcServerOAuth2.OAuth2Parameters(
2857 access_token=self.options.oauth2_access_token,
2858 client_id=self.oauth_client_id,
2859 client_secret=self.oauth_client_secret,
2860 scope=self.oauth_scopes,
2861 refresh_token=self.options.oauth2_refresh_token,
2862 credential_file=self.options.oauth2_credential_file,
2863 token_uri=self._GetTokenUri()))
2865 if hasattr(appengine_rpc_httplib2.tools, 'FLAGS'):
2866 appengine_rpc_httplib2.tools.FLAGS.auth_local_webserver = (
2867 self.options.auth_local_webserver)
2868 else:
2869 if not self.rpc_server_class:
2870 self.rpc_server_class = appengine_rpc.HttpRpcServerWithOAuth2Suggestion
2871 if hasattr(self, 'runtime'):
2872 self.rpc_server_class.RUNTIME = self.runtime
2873 get_user_credentials = GetUserCredentials
2876 if dev_appserver:
2877 email = self.options.email
2878 if email is None:
2879 email = 'test@example.com'
2880 logging.info('Using debug user %s. Override with --email', email)
2881 rpcserver = self.rpc_server_class(
2882 self.options.server,
2883 lambda: (email, 'password'),
2884 GetUserAgent(),
2885 source,
2886 host_override=self.options.host,
2887 save_cookies=self.options.save_cookies,
2889 secure=False)
2891 rpcserver.authenticated = True
2892 return rpcserver
2895 if self.options.passin:
2896 auth_tries = 1
2897 else:
2898 auth_tries = 3
2900 return self.rpc_server_class(self.options.server, get_user_credentials,
2901 GetUserAgent(), source,
2902 host_override=self.options.host,
2903 save_cookies=self.options.save_cookies,
2904 auth_tries=auth_tries,
2905 account_type='HOSTED_OR_GOOGLE',
2906 secure=self.options.secure,
2907 ignore_certs=self.options.ignore_certs)
2909 def _GetTokenUri(self):
2910 """Returns the OAuth2 token_uri, or None to use the default URI.
2912 Returns:
2913 A string that is the token_uri, or None.
2915 Raises:
2916 RuntimeError: The user has requested authentication for a service account
2917 but the environment is not correct for that to work.
2919 if self.options.authenticate_service_account:
2923 url = '%s/%s/scopes' % (METADATA_BASE, SERVICE_ACCOUNT_BASE)
2924 try:
2925 vm_scopes_string = self.read_url_contents(url)
2926 except urllib2.URLError, e:
2927 raise RuntimeError('Could not obtain scope list from metadata service: '
2928 '%s: %s. This may be because we are not running in '
2929 'a Google Compute Engine VM.' % (url, e))
2930 vm_scopes = vm_scopes_string.split()
2931 missing = list(set(self.oauth_scopes).difference(vm_scopes))
2932 if missing:
2933 raise RuntimeError('Required scopes %s missing from %s. '
2934 'This VM instance probably needs to be recreated '
2935 'with the missing scopes.' % (missing, vm_scopes))
2936 return '%s/%s/token' % (METADATA_BASE, SERVICE_ACCOUNT_BASE)
2937 else:
2938 return None
2940 def _FindYaml(self, basepath, file_name):
2941 """Find yaml files in application directory.
2943 Args:
2944 basepath: Base application directory.
2945 file_name: Relative file path from basepath, without extension, to search
2946 for.
2948 Returns:
2949 Path to located yaml file if one exists, else None.
2951 if not os.path.isdir(basepath):
2952 self.parser.error('Not a directory: %s' % basepath)
2956 alt_basepath = os.path.join(basepath, 'WEB-INF', 'appengine-generated')
2958 for yaml_basepath in (basepath, alt_basepath):
2959 for yaml_file in (file_name + '.yaml', file_name + '.yml'):
2960 yaml_path = os.path.join(yaml_basepath, yaml_file)
2961 if os.path.isfile(yaml_path):
2962 return yaml_path
2964 return None
2966 def _ParseAppInfoFromYaml(self, basepath, basename='app'):
2967 """Parses the app.yaml file.
2969 Args:
2970 basepath: The directory of the application.
2971 basename: The relative file path, from basepath, to search for.
2973 Returns:
2974 An AppInfoExternal object.
2976 try:
2977 appyaml = self._ParseYamlFile(basepath, basename, appinfo_includes.Parse)
2978 except yaml_errors.EventListenerError, e:
2979 self.parser.error('Error parsing %s.yaml: %s.' % (
2980 os.path.join(basepath, basename), e))
2981 if not appyaml:
2982 if self._JavaSupported():
2983 if appcfg_java.IsWarFileWithoutYaml(basepath):
2984 java_app_update = appcfg_java.JavaAppUpdate(basepath, self.options)
2985 appyaml_string = java_app_update.GenerateAppYamlString(basepath, [])
2986 appyaml = appinfo.LoadSingleAppInfo(appyaml_string)
2987 if not appyaml:
2988 self.parser.error('Directory contains neither an %s.yaml '
2989 'configuration file nor a WEB-INF subdirectory '
2990 'with web.xml and appengine-web.xml.' % basename)
2991 else:
2992 self.parser.error('Directory does not contain an %s.yaml configuration '
2993 'file' % basename)
2995 orig_application = appyaml.application
2996 orig_module = appyaml.module
2997 orig_version = appyaml.version
2998 if self.options.app_id:
2999 appyaml.application = self.options.app_id
3000 if self.options.module:
3001 appyaml.module = self.options.module
3002 if self.options.version:
3003 appyaml.version = self.options.version
3004 if self.options.runtime:
3005 appyaml.runtime = self.options.runtime
3006 if self.options.env_variables:
3007 if appyaml.env_variables is None:
3008 appyaml.env_variables = appinfo.EnvironmentVariables()
3009 appyaml.env_variables.update(self.options.env_variables)
3011 if not appyaml.application:
3012 self.parser.error('Expected -A app_id when application property in file '
3013 '%s.yaml is not set.' % basename)
3015 msg = 'Application: %s' % appyaml.application
3016 if appyaml.application != orig_application:
3017 msg += ' (was: %s)' % orig_application
3018 if self.action.function is 'Update':
3020 if (appyaml.module is not None and
3021 appyaml.module != appinfo.DEFAULT_MODULE):
3022 msg += '; module: %s' % appyaml.module
3023 if appyaml.module != orig_module:
3024 msg += ' (was: %s)' % orig_module
3025 msg += '; version: %s' % appyaml.version
3026 if appyaml.version != orig_version:
3027 msg += ' (was: %s)' % orig_version
3028 StatusUpdate(msg)
3029 return appyaml
3031 def _ParseYamlFile(self, basepath, basename, parser):
3032 """Parses a yaml file.
3034 Args:
3035 basepath: The base directory of the application.
3036 basename: The relative file path, from basepath, (with the '.yaml'
3037 stripped off).
3038 parser: the function or method used to parse the file.
3040 Returns:
3041 A single parsed yaml file or None if the file does not exist.
3043 file_name = self._FindYaml(basepath, basename)
3044 if file_name is not None:
3045 fh = self.opener(file_name, 'r')
3046 try:
3047 defns = parser(fh, open_fn=self.opener)
3048 finally:
3049 fh.close()
3050 return defns
3051 return None
3053 def _ParseBackendsYaml(self, basepath):
3054 """Parses the backends.yaml file.
3056 Args:
3057 basepath: the directory of the application.
3059 Returns:
3060 A BackendsInfoExternal object or None if the file does not exist.
3062 return self._ParseYamlFile(basepath, 'backends',
3063 backendinfo.LoadBackendInfo)
3065 def _ParseIndexYaml(self, basepath, appyaml=None):
3066 """Parses the index.yaml file.
3068 Args:
3069 basepath: the directory of the application.
3070 appyaml: The app.yaml, if present.
3071 Returns:
3072 A single parsed yaml file or None if the file does not exist.
3074 index_yaml = self._ParseYamlFile(basepath,
3075 'index',
3076 datastore_index.ParseIndexDefinitions)
3077 if not index_yaml:
3078 return None
3079 self._SetApplication(index_yaml, 'index', appyaml)
3081 return index_yaml
3083 def _SetApplication(self, dest_yaml, basename, appyaml=None):
3084 """Parses and sets the application property onto the dest_yaml parameter.
3086 The order of precendence is:
3087 1. Command line (-A application)
3088 2. Specified dest_yaml file
3089 3. App.yaml file
3091 This exits with a parse error if application is not present in any of these
3092 locations.
3094 Args:
3095 dest_yaml: The yaml object to set 'application' on.
3096 basename: The name of the dest_yaml file for use in errors.
3097 appyaml: The already parsed appyaml, if present. If none, this method will
3098 attempt to parse app.yaml.
3100 if self.options.app_id:
3101 dest_yaml.application = self.options.app_id
3102 if not dest_yaml.application:
3103 if not appyaml:
3104 appyaml = self._ParseYamlFile(self.basepath,
3105 'app',
3106 appinfo_includes.Parse)
3107 if appyaml:
3108 dest_yaml.application = appyaml.application
3109 else:
3110 self.parser.error('Expected -A app_id when %s.yaml.application is not '
3111 'set and app.yaml is not present.' % basename)
3113 def _ParseCronYaml(self, basepath, appyaml=None):
3114 """Parses the cron.yaml file.
3116 Args:
3117 basepath: the directory of the application.
3118 appyaml: The app.yaml, if present.
3120 Returns:
3121 A CronInfoExternal object or None if the file does not exist.
3123 cron_yaml = self._ParseYamlFile(basepath, 'cron', croninfo.LoadSingleCron)
3124 if not cron_yaml:
3125 return None
3126 self._SetApplication(cron_yaml, 'cron', appyaml)
3128 return cron_yaml
3130 def _ParseQueueYaml(self, basepath, appyaml=None):
3131 """Parses the queue.yaml file.
3133 Args:
3134 basepath: the directory of the application.
3135 appyaml: The app.yaml, if present.
3137 Returns:
3138 A QueueInfoExternal object or None if the file does not exist.
3140 queue_yaml = self._ParseYamlFile(basepath,
3141 'queue',
3142 queueinfo.LoadSingleQueue)
3143 if not queue_yaml:
3144 return None
3146 self._SetApplication(queue_yaml, 'queue', appyaml)
3147 return queue_yaml
3149 def _ParseDispatchYaml(self, basepath):
3150 """Parses the dispatch.yaml file.
3152 Args:
3153 basepath: the directory of the application.
3155 Returns:
3156 A DispatchInfoExternal object or None if the file does not exist.
3158 return self._ParseYamlFile(basepath, 'dispatch',
3159 dispatchinfo.LoadSingleDispatch)
3161 def _ParseDosYaml(self, basepath, appyaml=None):
3162 """Parses the dos.yaml file.
3164 Args:
3165 basepath: the directory of the application.
3166 appyaml: The app.yaml, if present.
3168 Returns:
3169 A DosInfoExternal object or None if the file does not exist.
3171 dos_yaml = self._ParseYamlFile(basepath, 'dos', dosinfo.LoadSingleDos)
3172 if not dos_yaml:
3173 return None
3175 self._SetApplication(dos_yaml, 'dos', appyaml)
3176 return dos_yaml
3178 def Help(self, action=None):
3179 """Prints help for a specific action.
3181 Args:
3182 action: If provided, print help for the action provided.
3184 Expects self.args[0], or 'action', to contain the name of the action in
3185 question. Exits the program after printing the help message.
3187 if not action:
3188 if len(self.args) > 1:
3189 self.args = [' '.join(self.args)]
3191 if len(self.args) != 1 or self.args[0] not in self.actions:
3192 self.parser.error('Expected a single action argument. '
3193 ' Must be one of:\n' +
3194 self._GetActionDescriptions())
3195 action = self.args[0]
3196 action = self.actions[action]
3197 self.parser, unused_options = self._MakeSpecificParser(action)
3198 self._PrintHelpAndExit(exit_code=0)
3200 def DownloadApp(self):
3201 """Downloads the given app+version."""
3202 if len(self.args) != 1:
3203 self.parser.error('\"download_app\" expects one non-option argument, '
3204 'found ' + str(len(self.args)) + '.')
3206 out_dir = self.args[0]
3208 app_id = self.options.app_id
3209 if app_id is None:
3210 self.parser.error('You must specify an app ID via -A or --application.')
3212 module = self.options.module
3213 app_version = self.options.version
3217 if os.path.exists(out_dir):
3218 if not os.path.isdir(out_dir):
3219 self.parser.error('Cannot download to path "%s": '
3220 'there\'s a file in the way.' % out_dir)
3221 elif os.listdir(out_dir):
3222 self.parser.error('Cannot download to path "%s": directory already '
3223 'exists and it isn\'t empty.' % out_dir)
3225 rpcserver = self._GetRpcServer()
3227 DoDownloadApp(rpcserver, out_dir, app_id, module, app_version)
3229 def UpdateVersion(self, rpcserver, basepath, appyaml, module_yaml_path,
3230 backend=None):
3231 """Updates and deploys a new appversion.
3233 Args:
3234 rpcserver: An AbstractRpcServer instance on which RPC calls can be made.
3235 basepath: The root directory of the version to update.
3236 appyaml: The AppInfoExternal object parsed from an app.yaml-like file.
3237 module_yaml_path: The (string) path to the yaml file, relative to the
3238 bundle directory.
3239 backend: The name of the backend to update, if any.
3241 Returns:
3242 An appinfo.AppInfoSummary if one was returned from the Deploy, None
3243 otherwise.
3245 Raises:
3246 RuntimeError: If go-app-builder fails to generate a mapping from relative
3247 paths to absolute paths, its stderr is raised.
3250 if not self.options.precompilation and appyaml.runtime == 'go':
3251 logging.warning('Precompilation is required for Go apps; '
3252 'ignoring --no_precompilation')
3253 self.options.precompilation = True
3255 if self.options.precompilation:
3256 if not appyaml.derived_file_type:
3257 appyaml.derived_file_type = []
3258 if appinfo.PYTHON_PRECOMPILED not in appyaml.derived_file_type:
3259 appyaml.derived_file_type.append(appinfo.PYTHON_PRECOMPILED)
3261 paths = self.file_iterator(basepath, appyaml.skip_files, appyaml.runtime)
3262 openfunc = lambda path: self.opener(os.path.join(basepath, path), 'rb')
3264 if appyaml.runtime == 'go':
3267 goroot = os.path.join(os.path.dirname(google.appengine.__file__),
3268 '../../goroot')
3269 gopath = os.environ.get('GOPATH')
3270 if os.path.isdir(goroot) and gopath:
3271 app_paths = list(paths)
3272 go_files = [f for f in app_paths
3273 if f.endswith('.go') and not appyaml.nobuild_files.match(f)]
3274 if not go_files:
3275 raise RuntimeError('no Go source files to upload '
3276 '(-nobuild_files applied)')
3277 gab_argv = [
3278 os.path.join(goroot, 'bin', 'go-app-builder'),
3279 '-app_base', self.basepath,
3280 '-arch', '6',
3281 '-gopath', gopath,
3282 '-goroot', goroot,
3283 '-print_extras',
3284 ] + go_files
3285 try:
3286 p = subprocess.Popen(gab_argv, stdout=subprocess.PIPE,
3287 stderr=subprocess.PIPE, env={})
3288 (stdout, stderr) = p.communicate()
3289 except Exception, e:
3290 raise RuntimeError('failed running go-app-builder', e)
3291 if p.returncode != 0:
3292 raise RuntimeError(stderr)
3297 overlay = dict([l.split('|') for l in stdout.split('\n') if l])
3298 logging.info('GOPATH overlay: %s', overlay)
3300 def ofunc(path):
3301 if path in overlay:
3302 return self.opener(overlay[path], 'rb')
3303 return self.opener(os.path.join(basepath, path), 'rb')
3304 paths = app_paths + overlay.keys()
3305 openfunc = ofunc
3307 appversion = AppVersionUpload(rpcserver,
3308 appyaml,
3309 module_yaml_path=module_yaml_path,
3310 backend=backend,
3311 error_fh=self.error_fh)
3312 return appversion.DoUpload(paths, openfunc)
3314 def UpdateUsingSpecificFiles(self):
3315 """Updates and deploys new app versions based on given config files."""
3316 rpcserver = self._GetRpcServer()
3317 all_files = [self.basepath] + self.args
3318 has_python25_version = False
3320 for yaml_path in all_files:
3321 file_name = os.path.basename(yaml_path)
3322 self.basepath = os.path.dirname(yaml_path)
3323 if not self.basepath:
3324 self.basepath = '.'
3325 module_yaml = self._ParseAppInfoFromYaml(self.basepath,
3326 os.path.splitext(file_name)[0])
3327 if module_yaml.runtime == 'python':
3328 has_python25_version = True
3332 if not module_yaml.module and file_name != 'app.yaml':
3333 ErrorUpdate("Error: 'module' parameter not specified in %s" %
3334 yaml_path)
3335 continue
3336 self.UpdateVersion(rpcserver, self.basepath, module_yaml, file_name)
3337 if has_python25_version:
3338 MigratePython27Notice()
3340 def Update(self):
3341 """Updates and deploys a new appversion and global app configs."""
3342 if not os.path.isdir(self.basepath):
3344 self.UpdateUsingSpecificFiles()
3345 return
3348 yaml_file_basename = 'app.yaml'
3350 if appcfg_java.IsWarFileWithoutYaml(self.basepath):
3351 java_app_update = appcfg_java.JavaAppUpdate(self.basepath, self.options)
3352 sdk_root = os.path.dirname(appcfg_java.__file__)
3353 self.options.compile_jsps = True
3361 self.stage_dir = java_app_update.CreateStagingDirectory(sdk_root)
3362 try:
3363 appyaml = self._ParseAppInfoFromYaml(
3364 self.stage_dir,
3365 basename=os.path.splitext(yaml_file_basename)[0])
3366 self._UpdateWithParsedAppYaml(
3367 appyaml, self.stage_dir, yaml_file_basename)
3368 finally:
3369 if self.options.retain_upload_dir:
3370 StatusUpdate(
3371 'Temporary staging directory left in %s' % self.stage_dir)
3372 else:
3373 shutil.rmtree(self.stage_dir)
3374 else:
3375 appyaml = self._ParseAppInfoFromYaml(
3376 self.basepath,
3377 basename=os.path.splitext(yaml_file_basename)[0])
3378 self._UpdateWithParsedAppYaml(appyaml, self.basepath, yaml_file_basename)
3380 def _UpdateWithParsedAppYaml(self, appyaml, basepath, yaml_file_basename):
3381 self.runtime = appyaml.runtime
3382 rpcserver = self._GetRpcServer()
3387 if self.options.skip_sdk_update_check:
3388 logging.info('Skipping update check')
3389 else:
3390 updatecheck = self.update_check_class(rpcserver, appyaml)
3391 updatecheck.CheckForUpdates()
3393 def _AbortAppMismatch(yaml_name):
3394 StatusUpdate('Error: Aborting upload because application in %s does not '
3395 'match application in app.yaml' % yaml_name)
3398 dos_yaml = self._ParseDosYaml(basepath, appyaml)
3399 if dos_yaml and dos_yaml.application != appyaml.application:
3400 return _AbortAppMismatch('dos.yaml')
3402 queue_yaml = self._ParseQueueYaml(basepath, appyaml)
3403 if queue_yaml and queue_yaml.application != appyaml.application:
3404 return _AbortAppMismatch('queue.yaml')
3406 cron_yaml = self._ParseCronYaml(basepath, appyaml)
3407 if cron_yaml and cron_yaml.application != appyaml.application:
3408 return _AbortAppMismatch('cron.yaml')
3410 index_defs = self._ParseIndexYaml(basepath, appyaml)
3411 if index_defs and index_defs.application != appyaml.application:
3412 return _AbortAppMismatch('index.yaml')
3413 self.UpdateVersion(rpcserver, basepath, appyaml, yaml_file_basename)
3415 if appyaml.runtime == 'python':
3416 MigratePython27Notice()
3419 if self.options.backends:
3420 self.BackendsUpdate()
3427 if index_defs:
3428 index_upload = IndexDefinitionUpload(rpcserver, index_defs)
3429 try:
3430 index_upload.DoUpload()
3431 except urllib2.HTTPError, e:
3432 ErrorUpdate('Error %d: --- begin server output ---\n'
3433 '%s\n--- end server output ---' %
3434 (e.code, e.read().rstrip('\n')))
3435 print >> self.error_fh, (
3436 'Your app was updated, but there was an error updating your '
3437 'indexes. Please retry later with appcfg.py update_indexes.')
3440 if cron_yaml:
3441 cron_upload = CronEntryUpload(rpcserver, cron_yaml)
3442 cron_upload.DoUpload()
3445 if queue_yaml:
3446 queue_upload = QueueEntryUpload(rpcserver, queue_yaml)
3447 queue_upload.DoUpload()
3450 if dos_yaml:
3451 dos_upload = DosEntryUpload(rpcserver, dos_yaml)
3452 dos_upload.DoUpload()
3455 if appyaml:
3456 pagespeed_upload = PagespeedEntryUpload(
3457 rpcserver, appyaml, appyaml.pagespeed)
3458 try:
3459 pagespeed_upload.DoUpload()
3460 except urllib2.HTTPError, e:
3461 ErrorUpdate('Error %d: --- begin server output ---\n'
3462 '%s\n--- end server output ---' %
3463 (e.code, e.read().rstrip('\n')))
3464 print >> self.error_fh, (
3465 'Your app was updated, but there was an error updating PageSpeed. '
3466 'Please try the update again later.')
3468 def _UpdateOptions(self, parser):
3469 """Adds update-specific options to 'parser'.
3471 Args:
3472 parser: An instance of OptionsParser.
3474 parser.add_option('--no_precompilation', action='store_false',
3475 dest='precompilation', default=True,
3476 help='Disable automatic precompilation '
3477 '(ignored for Go apps).')
3478 parser.add_option('--backends', action='store_true',
3479 dest='backends', default=False,
3480 help='Update backends when performing appcfg update.')
3481 if self._JavaSupported():
3482 appcfg_java.AddUpdateOptions(parser)
3484 def VacuumIndexes(self):
3485 """Deletes unused indexes."""
3486 if self.args:
3487 self.parser.error('Expected a single <directory> argument.')
3490 index_defs = self._ParseIndexYaml(self.basepath)
3491 if index_defs is None:
3492 index_defs = datastore_index.IndexDefinitions()
3494 rpcserver = self._GetRpcServer()
3495 vacuum = VacuumIndexesOperation(rpcserver,
3496 self.options.force_delete)
3497 vacuum.DoVacuum(index_defs)
3499 def _VacuumIndexesOptions(self, parser):
3500 """Adds vacuum_indexes-specific options to 'parser'.
3502 Args:
3503 parser: An instance of OptionsParser.
3505 parser.add_option('-f', '--force', action='store_true', dest='force_delete',
3506 default=False,
3507 help='Force deletion without being prompted.')
3509 def UpdateCron(self):
3510 """Updates any new or changed cron definitions."""
3511 if self.args:
3512 self.parser.error('Expected a single <directory> argument.')
3514 rpcserver = self._GetRpcServer()
3517 cron_yaml = self._ParseCronYaml(self.basepath)
3518 if cron_yaml:
3519 cron_upload = CronEntryUpload(rpcserver, cron_yaml)
3520 cron_upload.DoUpload()
3521 else:
3522 print >>sys.stderr, 'Could not find cron configuration. No action taken.'
3524 def UpdateIndexes(self):
3525 """Updates indexes."""
3526 if self.args:
3527 self.parser.error('Expected a single <directory> argument.')
3529 rpcserver = self._GetRpcServer()
3532 index_defs = self._ParseIndexYaml(self.basepath)
3533 if index_defs:
3534 index_upload = IndexDefinitionUpload(rpcserver, index_defs)
3535 index_upload.DoUpload()
3536 else:
3537 print >>sys.stderr, 'Could not find index configuration. No action taken.'
3539 def UpdateQueues(self):
3540 """Updates any new or changed task queue definitions."""
3541 if self.args:
3542 self.parser.error('Expected a single <directory> argument.')
3543 rpcserver = self._GetRpcServer()
3546 queue_yaml = self._ParseQueueYaml(self.basepath)
3547 if queue_yaml:
3548 queue_upload = QueueEntryUpload(rpcserver, queue_yaml)
3549 queue_upload.DoUpload()
3550 else:
3551 print >>sys.stderr, 'Could not find queue configuration. No action taken.'
3553 def UpdateDispatch(self):
3554 """Updates new or changed dispatch definitions."""
3555 if self.args:
3556 self.parser.error('Expected a single <directory> argument.')
3558 rpcserver = self._GetRpcServer()
3561 dispatch_yaml = self._ParseDispatchYaml(self.basepath)
3562 if dispatch_yaml:
3563 if self.options.app_id:
3564 dispatch_yaml.application = self.options.app_id
3565 if not dispatch_yaml.application:
3566 self.parser.error('Expected -A app_id when dispatch.yaml.application'
3567 ' is not set.')
3568 StatusUpdate('Uploading dispatch entries.')
3569 rpcserver.Send('/api/dispatch/update',
3570 app_id=dispatch_yaml.application,
3571 payload=dispatch_yaml.ToYAML())
3572 else:
3573 print >>sys.stderr, ('Could not find dispatch configuration. No action'
3574 ' taken.')
3576 def UpdateDos(self):
3577 """Updates any new or changed dos definitions."""
3578 if self.args:
3579 self.parser.error('Expected a single <directory> argument.')
3580 rpcserver = self._GetRpcServer()
3583 dos_yaml = self._ParseDosYaml(self.basepath)
3584 if dos_yaml:
3585 dos_upload = DosEntryUpload(rpcserver, dos_yaml)
3586 dos_upload.DoUpload()
3587 else:
3588 print >>sys.stderr, 'Could not find dos configuration. No action taken.'
3590 def BackendsAction(self):
3591 """Placeholder; we never expect this action to be invoked."""
3592 pass
3594 def BackendsPhpCheck(self, appyaml):
3595 """Don't support backends with the PHP runtime.
3597 This should be used to prevent use of backends update/start/configure
3598 with the PHP runtime. We continue to allow backends
3599 stop/delete/list/rollback just in case there are existing PHP backends.
3601 Args:
3602 appyaml: A parsed app.yaml file.
3604 if appyaml.runtime == 'php':
3605 _PrintErrorAndExit(
3606 self.error_fh,
3607 'Error: Backends are not supported with the PHP runtime. '
3608 'Please use Modules instead.\n')
3610 def BackendsYamlCheck(self, appyaml, backend=None):
3611 """Check the backends.yaml file is sane and which backends to update."""
3614 if appyaml.backends:
3615 self.parser.error('Backends are not allowed in app.yaml.')
3617 backends_yaml = self._ParseBackendsYaml(self.basepath)
3618 appyaml.backends = backends_yaml.backends
3620 if not appyaml.backends:
3621 self.parser.error('No backends found in backends.yaml.')
3623 backends = []
3624 for backend_entry in appyaml.backends:
3625 entry = backendinfo.LoadBackendEntry(backend_entry.ToYAML())
3626 if entry.name in backends:
3627 self.parser.error('Duplicate entry for backend: %s.' % entry.name)
3628 else:
3629 backends.append(entry.name)
3631 backends_to_update = []
3633 if backend:
3635 if backend in backends:
3636 backends_to_update = [backend]
3637 else:
3638 self.parser.error("Backend '%s' not found in backends.yaml." %
3639 backend)
3640 else:
3642 backends_to_update = backends
3644 return backends_to_update
3646 def BackendsUpdate(self):
3647 """Updates a backend."""
3648 self.backend = None
3649 if len(self.args) == 1:
3650 self.backend = self.args[0]
3651 elif len(self.args) > 1:
3652 self.parser.error('Expected an optional <backend> argument.')
3654 yaml_file_basename = 'app'
3655 appyaml = self._ParseAppInfoFromYaml(self.basepath,
3656 basename=yaml_file_basename)
3657 BackendsStatusUpdate(appyaml.runtime)
3658 self.BackendsPhpCheck(appyaml)
3659 rpcserver = self._GetRpcServer()
3661 backends_to_update = self.BackendsYamlCheck(appyaml, self.backend)
3662 for backend in backends_to_update:
3663 self.UpdateVersion(rpcserver, self.basepath, appyaml, yaml_file_basename,
3664 backend=backend)
3666 def BackendsList(self):
3667 """Lists all backends for an app."""
3668 if self.args:
3669 self.parser.error('Expected no arguments.')
3674 appyaml = self._ParseAppInfoFromYaml(self.basepath)
3675 BackendsStatusUpdate(appyaml.runtime)
3676 rpcserver = self._GetRpcServer()
3677 response = rpcserver.Send('/api/backends/list', app_id=appyaml.application)
3678 print >> self.out_fh, response
3680 def BackendsRollback(self):
3681 """Does a rollback of an existing transaction on this backend."""
3682 if len(self.args) != 1:
3683 self.parser.error('Expected a single <backend> argument.')
3685 self._Rollback(self.args[0])
3687 def BackendsStart(self):
3688 """Starts a backend."""
3689 if len(self.args) != 1:
3690 self.parser.error('Expected a single <backend> argument.')
3692 backend = self.args[0]
3693 appyaml = self._ParseAppInfoFromYaml(self.basepath)
3694 BackendsStatusUpdate(appyaml.runtime)
3695 self.BackendsPhpCheck(appyaml)
3696 rpcserver = self._GetRpcServer()
3697 response = rpcserver.Send('/api/backends/start',
3698 app_id=appyaml.application,
3699 backend=backend)
3700 print >> self.out_fh, response
3702 def BackendsStop(self):
3703 """Stops a backend."""
3704 if len(self.args) != 1:
3705 self.parser.error('Expected a single <backend> argument.')
3707 backend = self.args[0]
3708 appyaml = self._ParseAppInfoFromYaml(self.basepath)
3709 BackendsStatusUpdate(appyaml.runtime)
3710 rpcserver = self._GetRpcServer()
3711 response = rpcserver.Send('/api/backends/stop',
3712 app_id=appyaml.application,
3713 backend=backend)
3714 print >> self.out_fh, response
3716 def BackendsDelete(self):
3717 """Deletes a backend."""
3718 if len(self.args) != 1:
3719 self.parser.error('Expected a single <backend> argument.')
3721 backend = self.args[0]
3722 appyaml = self._ParseAppInfoFromYaml(self.basepath)
3723 BackendsStatusUpdate(appyaml.runtime)
3724 rpcserver = self._GetRpcServer()
3725 response = rpcserver.Send('/api/backends/delete',
3726 app_id=appyaml.application,
3727 backend=backend)
3728 print >> self.out_fh, response
3730 def BackendsConfigure(self):
3731 """Changes the configuration of an existing backend."""
3732 if len(self.args) != 1:
3733 self.parser.error('Expected a single <backend> argument.')
3735 backend = self.args[0]
3736 appyaml = self._ParseAppInfoFromYaml(self.basepath)
3737 BackendsStatusUpdate(appyaml.runtime)
3738 self.BackendsPhpCheck(appyaml)
3739 backends_yaml = self._ParseBackendsYaml(self.basepath)
3740 rpcserver = self._GetRpcServer()
3741 response = rpcserver.Send('/api/backends/configure',
3742 app_id=appyaml.application,
3743 backend=backend,
3744 payload=backends_yaml.ToYAML())
3745 print >> self.out_fh, response
3747 def ListVersions(self):
3748 """Lists all versions for an app."""
3749 if self.args:
3750 self.parser.error('Expected no arguments.')
3752 appyaml = self._ParseAppInfoFromYaml(self.basepath)
3753 rpcserver = self._GetRpcServer()
3754 response = rpcserver.Send('/api/versions/list', app_id=appyaml.application)
3756 parsed_response = yaml.safe_load(response)
3757 if not parsed_response:
3758 print >> self.out_fh, ('No versions uploaded for app: %s.' %
3759 appyaml.application)
3760 else:
3761 print >> self.out_fh, response
3763 def DeleteVersion(self):
3764 """Deletes the specified version for an app."""
3765 if not (self.options.app_id and self.options.version):
3766 self.parser.error('Expected an <app_id> argument, a <version> argument '
3767 'and an optional <module> argument.')
3768 if self.options.module:
3769 module = self.options.module
3770 else:
3771 module = ''
3773 rpcserver = self._GetRpcServer()
3774 response = rpcserver.Send('/api/versions/delete',
3775 app_id=self.options.app_id,
3776 version_match=self.options.version,
3777 module=module)
3779 print >> self.out_fh, response
3781 def _ParseAndValidateModuleYamls(self, yaml_paths):
3782 """Validates given yaml paths and returns the parsed yaml objects.
3784 Args:
3785 yaml_paths: List of paths to AppInfo yaml files.
3787 Returns:
3788 List of parsed AppInfo yamls.
3790 results = []
3791 app_id = None
3792 last_yaml_path = None
3793 for yaml_path in yaml_paths:
3794 if not os.path.isfile(yaml_path):
3795 _PrintErrorAndExit(
3796 self.error_fh,
3797 ("Error: The given path '%s' is not to a YAML configuration "
3798 "file.\n") % yaml_path)
3799 file_name = os.path.basename(yaml_path)
3800 base_path = os.path.dirname(yaml_path)
3801 if not base_path:
3802 base_path = '.'
3803 module_yaml = self._ParseAppInfoFromYaml(base_path,
3804 os.path.splitext(file_name)[0])
3806 if not module_yaml.module and file_name != 'app.yaml':
3807 _PrintErrorAndExit(
3808 self.error_fh,
3809 "Error: 'module' parameter not specified in %s" % yaml_path)
3813 if app_id is not None and module_yaml.application != app_id:
3814 _PrintErrorAndExit(
3815 self.error_fh,
3816 "Error: 'application' value '%s' in %s does not match the value "
3817 "'%s', found in %s" % (module_yaml.application,
3818 yaml_path,
3819 app_id,
3820 last_yaml_path))
3821 app_id = module_yaml.application
3822 last_yaml_path = yaml_path
3823 results.append(module_yaml)
3825 return results
3827 def _ModuleAction(self, action_path):
3828 """Process flags and yaml files and make a call to the given path.
3830 The 'start' and 'stop' actions are extremely similar in how they process
3831 input to appcfg.py and only really differ in what path they hit on the
3832 RPCServer.
3834 Args:
3835 action_path: Path on the RPCServer to send the call to.
3838 modules_to_process = []
3839 if not self.args:
3841 if not (self.options.app_id and
3842 self.options.module and
3843 self.options.version):
3844 _PrintErrorAndExit(self.error_fh,
3845 'Expected at least one <file> argument or the '
3846 '--application, --module and --version flags to'
3847 ' be set.')
3848 else:
3849 modules_to_process.append((self.options.app_id,
3850 self.options.module,
3851 self.options.version))
3852 else:
3855 if self.options.module:
3857 _PrintErrorAndExit(self.error_fh,
3858 'You may not specify a <file> argument with the '
3859 '--module flag.')
3861 module_yamls = self._ParseAndValidateModuleYamls(self.args)
3862 for serv_yaml in module_yamls:
3865 app_id = serv_yaml.application
3866 modules_to_process.append((self.options.app_id or serv_yaml.application,
3867 serv_yaml.module or appinfo.DEFAULT_MODULE,
3868 self.options.version or serv_yaml.version))
3870 rpcserver = self._GetRpcServer()
3873 for app_id, module, version in modules_to_process:
3874 response = rpcserver.Send(action_path,
3875 app_id=app_id,
3876 module=module,
3877 version=version)
3878 print >> self.out_fh, response
3880 def Start(self):
3881 """Starts one or more modules."""
3882 self._ModuleAction('/api/modules/start')
3884 def Stop(self):
3885 """Stops one or more modules."""
3886 self._ModuleAction('/api/modules/stop')
3888 def Rollback(self):
3889 """Does a rollback of an existing transaction for this app version."""
3890 if self.args:
3891 self.parser.error('Expected a single <directory> or <file> argument.')
3892 self._Rollback()
3894 def _Rollback(self, backend=None):
3895 """Does a rollback of an existing transaction.
3897 Args:
3898 backend: name of a backend to rollback, or None
3900 If a backend is specified the rollback will affect only that backend, if no
3901 backend is specified the rollback will affect the current app version.
3903 if os.path.isdir(self.basepath):
3904 module_yaml = self._ParseAppInfoFromYaml(self.basepath)
3905 else:
3907 file_name = os.path.basename(self.basepath)
3908 self.basepath = os.path.dirname(self.basepath)
3909 if not self.basepath:
3910 self.basepath = '.'
3911 module_yaml = self._ParseAppInfoFromYaml(self.basepath,
3912 os.path.splitext(file_name)[0])
3914 appversion = AppVersionUpload(self._GetRpcServer(), module_yaml,
3915 module_yaml_path='app.yaml',
3916 backend=backend)
3918 appversion.in_transaction = True
3919 appversion.Rollback()
3921 def SetDefaultVersion(self):
3922 """Sets the default version."""
3923 module = ''
3924 if len(self.args) == 1:
3928 stored_modules = self.options.module
3929 self.options.module = None
3930 try:
3931 appyaml = self._ParseAppInfoFromYaml(self.args[0])
3932 finally:
3933 self.options.module = stored_modules
3935 app_id = appyaml.application
3936 module = appyaml.module or ''
3937 version = appyaml.version
3938 elif not self.args:
3939 if not (self.options.app_id and self.options.version):
3940 self.parser.error(
3941 ('Expected a <directory> argument or both --application and '
3942 '--version flags.'))
3943 else:
3944 self._PrintHelpAndExit()
3947 if self.options.app_id:
3948 app_id = self.options.app_id
3949 if self.options.module:
3950 module = self.options.module
3951 if self.options.version:
3952 version = self.options.version
3954 version_setter = DefaultVersionSet(self._GetRpcServer(),
3955 app_id,
3956 module,
3957 version)
3958 version_setter.SetVersion()
3961 def MigrateTraffic(self):
3962 """Migrates traffic."""
3963 if len(self.args) == 1:
3964 appyaml = self._ParseAppInfoFromYaml(self.args[0])
3965 app_id = appyaml.application
3966 version = appyaml.version
3967 elif not self.args:
3968 if not (self.options.app_id and self.options.version):
3969 self.parser.error(
3970 ('Expected a <directory> argument or both --application and '
3971 '--version flags.'))
3972 else:
3973 self._PrintHelpAndExit()
3976 if self.options.app_id:
3977 app_id = self.options.app_id
3978 if self.options.version:
3979 version = self.options.version
3981 traffic_migrator = TrafficMigrator(
3982 self._GetRpcServer(), app_id, version)
3983 traffic_migrator.MigrateTraffic()
3985 def RequestLogs(self):
3986 """Write request logs to a file."""
3988 args_length = len(self.args)
3989 module = ''
3990 if args_length == 2:
3991 appyaml = self._ParseAppInfoFromYaml(self.args.pop(0))
3992 app_id = appyaml.application
3993 module = appyaml.module or ''
3994 version = appyaml.version
3995 elif args_length == 1:
3996 if not (self.options.app_id and self.options.version):
3997 self.parser.error(
3998 ('Expected the --application and --version flags if <directory> '
3999 'argument is not specified.'))
4000 else:
4001 self._PrintHelpAndExit()
4004 if self.options.app_id:
4005 app_id = self.options.app_id
4006 if self.options.module:
4007 module = self.options.module
4008 if self.options.version:
4009 version = self.options.version
4011 if (self.options.severity is not None and
4012 not 0 <= self.options.severity <= MAX_LOG_LEVEL):
4013 self.parser.error(
4014 'Severity range is 0 (DEBUG) through %s (CRITICAL).' % MAX_LOG_LEVEL)
4016 if self.options.num_days is None:
4017 self.options.num_days = int(not self.options.append)
4019 try:
4020 end_date = self._ParseEndDate(self.options.end_date)
4021 except (TypeError, ValueError):
4022 self.parser.error('End date must be in the format YYYY-MM-DD.')
4024 rpcserver = self._GetRpcServer()
4026 logs_requester = LogsRequester(rpcserver,
4027 app_id,
4028 module,
4029 version,
4030 self.args[0],
4031 self.options.num_days,
4032 self.options.append,
4033 self.options.severity,
4034 end_date,
4035 self.options.vhost,
4036 self.options.include_vhost,
4037 self.options.include_all,
4038 time_func=self.time_func)
4039 logs_requester.DownloadLogs()
4041 @staticmethod
4042 def _ParseEndDate(date, time_func=time.time):
4043 """Translates an ISO 8601 date to a date object.
4045 Args:
4046 date: A date string as YYYY-MM-DD.
4047 time_func: A time.time() compatible function, which can be overridden for
4048 testing.
4050 Returns:
4051 A date object representing the last day of logs to get.
4052 If no date is given, returns today in the US/Pacific timezone.
4054 if not date:
4055 return PacificDate(time_func())
4056 return datetime.date(*[int(i) for i in date.split('-')])
4058 def _RequestLogsOptions(self, parser):
4059 """Adds request_logs-specific options to 'parser'.
4061 Args:
4062 parser: An instance of OptionsParser.
4064 parser.add_option('-n', '--num_days', type='int', dest='num_days',
4065 action='store', default=None,
4066 help='Number of days worth of log data to get. '
4067 'The cut-off point is midnight US/Pacific. '
4068 'Use 0 to get all available logs. '
4069 'Default is 1, unless --append is also given; '
4070 'then the default is 0.')
4071 parser.add_option('-a', '--append', dest='append',
4072 action='store_true', default=False,
4073 help='Append to existing file.')
4074 parser.add_option('--severity', type='int', dest='severity',
4075 action='store', default=None,
4076 help='Severity of app-level log messages to get. '
4077 'The range is 0 (DEBUG) through 4 (CRITICAL). '
4078 'If omitted, only request logs are returned.')
4079 parser.add_option('--vhost', type='string', dest='vhost',
4080 action='store', default=None,
4081 help='The virtual host of log messages to get. '
4082 'If omitted, all log messages are returned.')
4083 parser.add_option('--include_vhost', dest='include_vhost',
4084 action='store_true', default=False,
4085 help='Include virtual host in log messages.')
4086 parser.add_option('--include_all', dest='include_all',
4087 action='store_true', default=None,
4088 help='Include everything in log messages.')
4089 parser.add_option('--end_date', dest='end_date',
4090 action='store', default='',
4091 help='End date (as YYYY-MM-DD) of period for log data. '
4092 'Defaults to today.')
4094 def CronInfo(self, now=None, output=sys.stdout):
4095 """Displays information about cron definitions.
4097 Args:
4098 now: used for testing.
4099 output: Used for testing.
4101 if self.args:
4102 self.parser.error('Expected a single <directory> argument.')
4103 if now is None:
4104 now = datetime.datetime.utcnow()
4106 cron_yaml = self._ParseCronYaml(self.basepath)
4107 if cron_yaml and cron_yaml.cron:
4108 for entry in cron_yaml.cron:
4109 description = entry.description
4110 if not description:
4111 description = '<no description>'
4112 if not entry.timezone:
4113 entry.timezone = 'UTC'
4115 print >>output, '\n%s:\nURL: %s\nSchedule: %s (%s)' % (description,
4116 entry.url,
4117 entry.schedule,
4118 entry.timezone)
4119 if entry.timezone != 'UTC':
4120 print >>output, ('Note: Schedules with timezones won\'t be calculated'
4121 ' correctly here')
4122 schedule = groctimespecification.GrocTimeSpecification(entry.schedule)
4124 matches = schedule.GetMatches(now, self.options.num_runs)
4125 for match in matches:
4126 print >>output, '%s, %s from now' % (
4127 match.strftime('%Y-%m-%d %H:%M:%SZ'), match - now)
4129 def _CronInfoOptions(self, parser):
4130 """Adds cron_info-specific options to 'parser'.
4132 Args:
4133 parser: An instance of OptionsParser.
4135 parser.add_option('-n', '--num_runs', type='int', dest='num_runs',
4136 action='store', default=5,
4137 help='Number of runs of each cron job to display'
4138 'Default is 5')
4140 def _CheckRequiredLoadOptions(self):
4141 """Checks that upload/download options are present."""
4142 for option in ['filename']:
4143 if getattr(self.options, option) is None:
4144 self.parser.error('Option \'%s\' is required.' % option)
4145 if not self.options.url:
4146 self.parser.error('You must have google.appengine.ext.remote_api.handler '
4147 'assigned to an endpoint in app.yaml, or provide '
4148 'the url of the handler via the \'url\' option.')
4150 def InferRemoteApiUrl(self, appyaml):
4151 """Uses app.yaml to determine the remote_api endpoint.
4153 Args:
4154 appyaml: A parsed app.yaml file.
4156 Returns:
4157 The url of the remote_api endpoint as a string, or None
4160 handlers = appyaml.handlers
4161 handler_suffixes = ['remote_api/handler.py',
4162 'remote_api.handler.application']
4163 app_id = appyaml.application
4164 for handler in handlers:
4165 if hasattr(handler, 'script') and handler.script:
4166 if any(handler.script.endswith(suffix) for suffix in handler_suffixes):
4167 server = self.options.server
4168 url = handler.url
4169 if url.endswith('(/.*)?'):
4172 url = url[:-6]
4173 if server == 'appengine.google.com':
4174 return 'http://%s.appspot.com%s' % (app_id, url)
4175 else:
4176 match = re.match(PREFIXED_BY_ADMIN_CONSOLE_RE, server)
4177 if match:
4178 return 'http://%s%s%s' % (app_id, match.group(1), url)
4179 else:
4180 return 'http://%s%s' % (server, url)
4181 return None
4183 def RunBulkloader(self, arg_dict):
4184 """Invokes the bulkloader with the given keyword arguments.
4186 Args:
4187 arg_dict: Dictionary of arguments to pass to bulkloader.Run().
4190 try:
4192 import sqlite3
4193 except ImportError:
4194 logging.error('upload_data action requires SQLite3 and the python '
4195 'sqlite3 module (included in python since 2.5).')
4196 sys.exit(1)
4198 sys.exit(bulkloader.Run(arg_dict))
4200 def _SetupLoad(self):
4201 """Performs common verification and set up for upload and download."""
4203 if len(self.args) != 1 and not self.options.url:
4204 self.parser.error('Expected either --url or a single <directory> '
4205 'argument.')
4207 if len(self.args) == 1:
4208 self.basepath = self.args[0]
4209 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4211 self.options.app_id = appyaml.application
4213 if not self.options.url:
4214 url = self.InferRemoteApiUrl(appyaml)
4215 if url is not None:
4216 self.options.url = url
4218 self._CheckRequiredLoadOptions()
4220 if self.options.batch_size < 1:
4221 self.parser.error('batch_size must be 1 or larger.')
4225 if verbosity == 1:
4226 logging.getLogger().setLevel(logging.INFO)
4227 self.options.debug = False
4228 else:
4229 logging.getLogger().setLevel(logging.DEBUG)
4230 self.options.debug = True
4232 def _MakeLoaderArgs(self):
4233 args = dict([(arg_name, getattr(self.options, arg_name, None)) for
4234 arg_name in (
4235 'url',
4236 'filename',
4237 'batch_size',
4238 'kind',
4239 'num_threads',
4240 'bandwidth_limit',
4241 'rps_limit',
4242 'http_limit',
4243 'db_filename',
4244 'config_file',
4245 'auth_domain',
4246 'has_header',
4247 'loader_opts',
4248 'log_file',
4249 'passin',
4250 'email',
4251 'debug',
4252 'exporter_opts',
4253 'mapper_opts',
4254 'result_db_filename',
4255 'mapper_opts',
4256 'dry_run',
4257 'dump',
4258 'restore',
4259 'namespace',
4260 'create_config',
4262 args['application'] = self.options.app_id
4263 args['throttle_class'] = self.throttle_class
4264 return args
4266 def PerformDownload(self, run_fn=None):
4267 """Performs a datastore download via the bulkloader.
4269 Args:
4270 run_fn: Function to invoke the bulkloader, used for testing.
4272 if run_fn is None:
4273 run_fn = self.RunBulkloader
4274 self._SetupLoad()
4276 StatusUpdate('Downloading data records.')
4278 args = self._MakeLoaderArgs()
4279 args['download'] = bool(args['config_file'])
4280 args['has_header'] = False
4281 args['map'] = False
4282 args['dump'] = not args['config_file']
4283 args['restore'] = False
4284 args['create_config'] = False
4286 run_fn(args)
4288 def PerformUpload(self, run_fn=None):
4289 """Performs a datastore upload via the bulkloader.
4291 Args:
4292 run_fn: Function to invoke the bulkloader, used for testing.
4294 if run_fn is None:
4295 run_fn = self.RunBulkloader
4296 self._SetupLoad()
4298 StatusUpdate('Uploading data records.')
4300 args = self._MakeLoaderArgs()
4301 args['download'] = False
4302 args['map'] = False
4303 args['dump'] = False
4304 args['restore'] = not args['config_file']
4305 args['create_config'] = False
4307 run_fn(args)
4309 def CreateBulkloadConfig(self, run_fn=None):
4310 """Create a bulkloader config via the bulkloader wizard.
4312 Args:
4313 run_fn: Function to invoke the bulkloader, used for testing.
4315 if run_fn is None:
4316 run_fn = self.RunBulkloader
4317 self._SetupLoad()
4319 StatusUpdate('Creating bulkloader configuration.')
4321 args = self._MakeLoaderArgs()
4322 args['download'] = False
4323 args['has_header'] = False
4324 args['map'] = False
4325 args['dump'] = False
4326 args['restore'] = False
4327 args['create_config'] = True
4329 run_fn(args)
4331 def _PerformLoadOptions(self, parser):
4332 """Adds options common to 'upload_data' and 'download_data'.
4334 Args:
4335 parser: An instance of OptionsParser.
4337 parser.add_option('--url', type='string', dest='url',
4338 action='store',
4339 help='The location of the remote_api endpoint.')
4340 parser.add_option('--batch_size', type='int', dest='batch_size',
4341 action='store', default=10,
4342 help='Number of records to post in each request.')
4343 parser.add_option('--bandwidth_limit', type='int', dest='bandwidth_limit',
4344 action='store', default=250000,
4345 help='The maximum bytes/second bandwidth for transfers.')
4346 parser.add_option('--rps_limit', type='int', dest='rps_limit',
4347 action='store', default=20,
4348 help='The maximum records/second for transfers.')
4349 parser.add_option('--http_limit', type='int', dest='http_limit',
4350 action='store', default=8,
4351 help='The maximum requests/second for transfers.')
4352 parser.add_option('--db_filename', type='string', dest='db_filename',
4353 action='store',
4354 help='Name of the progress database file.')
4355 parser.add_option('--auth_domain', type='string', dest='auth_domain',
4356 action='store', default='gmail.com',
4357 help='The name of the authorization domain to use.')
4358 parser.add_option('--log_file', type='string', dest='log_file',
4359 help='File to write bulkloader logs. If not supplied '
4360 'then a new log file will be created, named: '
4361 'bulkloader-log-TIMESTAMP.')
4362 parser.add_option('--dry_run', action='store_true',
4363 dest='dry_run', default=False,
4364 help='Do not execute any remote_api calls')
4365 parser.add_option('--namespace', type='string', dest='namespace',
4366 action='store', default='',
4367 help='Namespace to use when accessing datastore.')
4368 parser.add_option('--num_threads', type='int', dest='num_threads',
4369 action='store', default=10,
4370 help='Number of threads to transfer records with.')
4372 def _PerformUploadOptions(self, parser):
4373 """Adds 'upload_data' specific options to the 'parser' passed in.
4375 Args:
4376 parser: An instance of OptionsParser.
4378 self._PerformLoadOptions(parser)
4379 parser.add_option('--filename', type='string', dest='filename',
4380 action='store',
4381 help='The name of the file containing the input data.'
4382 ' (Required)')
4383 parser.add_option('--kind', type='string', dest='kind',
4384 action='store',
4385 help='The kind of the entities to store.')
4386 parser.add_option('--has_header', dest='has_header',
4387 action='store_true', default=False,
4388 help='Whether the first line of the input file should be'
4389 ' skipped')
4390 parser.add_option('--loader_opts', type='string', dest='loader_opts',
4391 help='A string to pass to the Loader.initialize method.')
4392 parser.add_option('--config_file', type='string', dest='config_file',
4393 action='store',
4394 help='Name of the configuration file.')
4396 def _PerformDownloadOptions(self, parser):
4397 """Adds 'download_data' specific options to the 'parser' passed in.
4399 Args:
4400 parser: An instance of OptionsParser.
4402 self._PerformLoadOptions(parser)
4403 parser.add_option('--filename', type='string', dest='filename',
4404 action='store',
4405 help='The name of the file where output data is to be'
4406 ' written. (Required)')
4407 parser.add_option('--kind', type='string', dest='kind',
4408 action='store',
4409 help='The kind of the entities to retrieve.')
4410 parser.add_option('--exporter_opts', type='string', dest='exporter_opts',
4411 help='A string to pass to the Exporter.initialize method.'
4413 parser.add_option('--result_db_filename', type='string',
4414 dest='result_db_filename',
4415 action='store',
4416 help='Database to write entities to for download.')
4417 parser.add_option('--config_file', type='string', dest='config_file',
4418 action='store',
4419 help='Name of the configuration file.')
4421 def _CreateBulkloadConfigOptions(self, parser):
4422 """Adds 'download_data' specific options to the 'parser' passed in.
4424 Args:
4425 parser: An instance of OptionsParser.
4427 self._PerformLoadOptions(parser)
4428 parser.add_option('--filename', type='string', dest='filename',
4429 action='store',
4430 help='The name of the file where the generated template'
4431 ' is to be written. (Required)')
4433 def ResourceLimitsInfo(self, output=None):
4434 """Outputs the current resource limits.
4436 Args:
4437 output: The file handle to write the output to (used for testing).
4439 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4440 resource_limits = GetResourceLimits(self._GetRpcServer(), appyaml)
4443 for attr_name in sorted(resource_limits):
4444 print >>output, '%s: %s' % (attr_name, resource_limits[attr_name])
4446 class Action(object):
4447 """Contains information about a command line action.
4449 Attributes:
4450 function: The name of a function defined on AppCfg or its subclasses
4451 that will perform the appropriate action.
4452 usage: A command line usage string.
4453 short_desc: A one-line description of the action.
4454 long_desc: A detailed description of the action. Whitespace and
4455 formatting will be preserved.
4456 error_desc: An error message to display when the incorrect arguments are
4457 given.
4458 options: A function that will add extra options to a given OptionParser
4459 object.
4460 uses_basepath: Does the action use a basepath/app-directory (and hence
4461 app.yaml).
4462 hidden: Should this command be shown in the help listing.
4470 def __init__(self, function, usage, short_desc, long_desc='',
4471 error_desc=None, options=lambda obj, parser: None,
4472 uses_basepath=True, hidden=False):
4473 """Initializer for the class attributes."""
4474 self.function = function
4475 self.usage = usage
4476 self.short_desc = short_desc
4477 self.long_desc = long_desc
4478 self.error_desc = error_desc
4479 self.options = options
4480 self.uses_basepath = uses_basepath
4481 self.hidden = hidden
4483 def __call__(self, appcfg):
4484 """Invoke this Action on the specified AppCfg.
4486 This calls the function of the appropriate name on AppCfg, and
4487 respects polymophic overrides.
4489 Args:
4490 appcfg: The appcfg to use.
4491 Returns:
4492 The result of the function call.
4494 method = getattr(appcfg, self.function)
4495 return method()
4497 actions = {
4499 'help': Action(
4500 function='Help',
4501 usage='%prog help <action>',
4502 short_desc='Print help for a specific action.',
4503 uses_basepath=False),
4505 'update': Action(
4506 function='Update',
4507 usage='%prog [options] update <directory> | [file, ...]',
4508 options=_UpdateOptions,
4509 short_desc='Create or update an app version.',
4510 long_desc="""
4511 Specify a directory that contains all of the files required by
4512 the app, and appcfg.py will create/update the app version referenced
4513 in the app.yaml file at the top level of that directory. appcfg.py
4514 will follow symlinks and recursively upload all files to the server.
4515 Temporary or source control files (e.g. foo~, .svn/*) will be skipped.
4517 If you are using the Modules feature, then you may prefer to pass multiple files
4518 to update, rather than a directory, to specify which modules you would like
4519 updated."""),
4521 'download_app': Action(
4522 function='DownloadApp',
4523 usage='%prog [options] download_app -A app_id [ -V version ]'
4524 ' <out-dir>',
4525 short_desc='Download a previously-uploaded app.',
4526 long_desc="""
4527 Download a previously-uploaded app to the specified directory. The app
4528 ID is specified by the \"-A\" option. The optional version is specified
4529 by the \"-V\" option.""",
4530 uses_basepath=False),
4532 'update_cron': Action(
4533 function='UpdateCron',
4534 usage='%prog [options] update_cron <directory>',
4535 short_desc='Update application cron definitions.',
4536 long_desc="""
4537 The 'update_cron' command will update any new, removed or changed cron
4538 definitions from the optional cron.yaml file."""),
4540 'update_indexes': Action(
4541 function='UpdateIndexes',
4542 usage='%prog [options] update_indexes <directory>',
4543 short_desc='Update application indexes.',
4544 long_desc="""
4545 The 'update_indexes' command will add additional indexes which are not currently
4546 in production as well as restart any indexes that were not completed."""),
4548 'update_queues': Action(
4549 function='UpdateQueues',
4550 usage='%prog [options] update_queues <directory>',
4551 short_desc='Update application task queue definitions.',
4552 long_desc="""
4553 The 'update_queue' command will update any new, removed or changed task queue
4554 definitions from the optional queue.yaml file."""),
4556 'update_dispatch': Action(
4557 function='UpdateDispatch',
4558 usage='%prog [options] update_dispatch <directory>',
4559 short_desc='Update application dispatch definitions.',
4560 long_desc="""
4561 The 'update_dispatch' command will update any new, removed or changed dispatch
4562 definitions from the optional dispatch.yaml file."""),
4564 'update_dos': Action(
4565 function='UpdateDos',
4566 usage='%prog [options] update_dos <directory>',
4567 short_desc='Update application dos definitions.',
4568 long_desc="""
4569 The 'update_dos' command will update any new, removed or changed dos
4570 definitions from the optional dos.yaml file."""),
4572 'backends': Action(
4573 function='BackendsAction',
4574 usage='%prog [options] backends <directory> <action>',
4575 short_desc='Perform a backend action.',
4576 long_desc="""
4577 The 'backends' command will perform a backends action.""",
4578 error_desc="""\
4579 Expected a <directory> and <action> argument."""),
4581 'backends list': Action(
4582 function='BackendsList',
4583 usage='%prog [options] backends <directory> list',
4584 short_desc='List all backends configured for the app.',
4585 long_desc="""
4586 The 'backends list' command will list all backends configured for the app."""),
4588 'backends update': Action(
4589 function='BackendsUpdate',
4590 usage='%prog [options] backends <directory> update [<backend>]',
4591 options=_UpdateOptions,
4592 short_desc='Update one or more backends.',
4593 long_desc="""
4594 The 'backends update' command updates one or more backends. This command
4595 updates backend configuration settings and deploys new code to the server. Any
4596 existing instances will stop and be restarted. Updates all backends, or a
4597 single backend if the <backend> argument is provided."""),
4599 'backends rollback': Action(
4600 function='BackendsRollback',
4601 usage='%prog [options] backends <directory> rollback <backend>',
4602 short_desc='Roll back an update of a backend.',
4603 long_desc="""
4604 The 'backends update' command requires a server-side transaction.
4605 Use 'backends rollback' if you experience an error during 'backends update'
4606 and want to start the update over again."""),
4608 'backends start': Action(
4609 function='BackendsStart',
4610 usage='%prog [options] backends <directory> start <backend>',
4611 short_desc='Start a backend.',
4612 long_desc="""
4613 The 'backends start' command will put a backend into the START state."""),
4615 'backends stop': Action(
4616 function='BackendsStop',
4617 usage='%prog [options] backends <directory> stop <backend>',
4618 short_desc='Stop a backend.',
4619 long_desc="""
4620 The 'backends start' command will put a backend into the STOP state."""),
4622 'backends delete': Action(
4623 function='BackendsDelete',
4624 usage='%prog [options] backends <directory> delete <backend>',
4625 short_desc='Delete a backend.',
4626 long_desc="""
4627 The 'backends delete' command will delete a backend."""),
4629 'backends configure': Action(
4630 function='BackendsConfigure',
4631 usage='%prog [options] backends <directory> configure <backend>',
4632 short_desc='Reconfigure a backend without stopping it.',
4633 long_desc="""
4634 The 'backends configure' command performs an online update of a backend, without
4635 stopping instances that are currently running. No code or handlers are updated,
4636 only certain configuration settings specified in backends.yaml. Valid settings
4637 are: instances, options: public, and options: failfast."""),
4639 'vacuum_indexes': Action(
4640 function='VacuumIndexes',
4641 usage='%prog [options] vacuum_indexes <directory>',
4642 options=_VacuumIndexesOptions,
4643 short_desc='Delete unused indexes from application.',
4644 long_desc="""
4645 The 'vacuum_indexes' command will help clean up indexes which are no longer
4646 in use. It does this by comparing the local index configuration with
4647 indexes that are actually defined on the server. If any indexes on the
4648 server do not exist in the index configuration file, the user is given the
4649 option to delete them."""),
4651 'rollback': Action(
4652 function='Rollback',
4653 usage='%prog [options] rollback <directory> | <file>',
4654 short_desc='Rollback an in-progress update.',
4655 long_desc="""
4656 The 'update' command requires a server-side transaction.
4657 Use 'rollback' if you experience an error during 'update'
4658 and want to begin a new update transaction."""),
4660 'request_logs': Action(
4661 function='RequestLogs',
4662 usage='%prog [options] request_logs [<directory>] <output_file>',
4663 options=_RequestLogsOptions,
4664 uses_basepath=False,
4665 short_desc='Write request logs in Apache common log format.',
4666 long_desc="""
4667 The 'request_logs' command exports the request logs from your application
4668 to a file. It will write Apache common log format records ordered
4669 chronologically. If output file is '-' stdout will be written.""",
4670 error_desc="""\
4671 Expected an optional <directory> and mandatory <output_file> argument."""),
4673 'cron_info': Action(
4674 function='CronInfo',
4675 usage='%prog [options] cron_info <directory>',
4676 options=_CronInfoOptions,
4677 short_desc='Display information about cron jobs.',
4678 long_desc="""
4679 The 'cron_info' command will display the next 'number' runs (default 5) for
4680 each cron job defined in the cron.yaml file."""),
4682 'start': Action(
4683 function='Start',
4684 uses_basepath=False,
4685 usage='%prog [options] start [file, ...]',
4686 short_desc='Start a module version.',
4687 long_desc="""
4688 The 'start' command will put a module version into the START state."""),
4690 'stop': Action(
4691 function='Stop',
4692 uses_basepath=False,
4693 usage='%prog [options] stop [file, ...]',
4694 short_desc='Stop a module version.',
4695 long_desc="""
4696 The 'stop' command will put a module version into the STOP state."""),
4702 'upload_data': Action(
4703 function='PerformUpload',
4704 usage='%prog [options] upload_data <directory>',
4705 options=_PerformUploadOptions,
4706 short_desc='Upload data records to datastore.',
4707 long_desc="""
4708 The 'upload_data' command translates input records into datastore entities and
4709 uploads them into your application's datastore.""",
4710 uses_basepath=False),
4712 'download_data': Action(
4713 function='PerformDownload',
4714 usage='%prog [options] download_data <directory>',
4715 options=_PerformDownloadOptions,
4716 short_desc='Download entities from datastore.',
4717 long_desc="""
4718 The 'download_data' command downloads datastore entities and writes them to
4719 file as CSV or developer defined format.""",
4720 uses_basepath=False),
4722 'create_bulkloader_config': Action(
4723 function='CreateBulkloadConfig',
4724 usage='%prog [options] create_bulkload_config <directory>',
4725 options=_CreateBulkloadConfigOptions,
4726 short_desc='Create a bulkloader.yaml from a running application.',
4727 long_desc="""
4728 The 'create_bulkloader_config' command creates a bulkloader.yaml configuration
4729 template for use with upload_data or download_data.""",
4730 uses_basepath=False),
4733 'set_default_version': Action(
4734 function='SetDefaultVersion',
4735 usage='%prog [options] set_default_version [directory]',
4736 short_desc='Set the default (serving) version.',
4737 long_desc="""
4738 The 'set_default_version' command sets the default (serving) version of the app.
4739 Defaults to using the application, version and module specified in app.yaml;
4740 use the --application, --version and --module flags to override these values.
4741 The --module flag can also be a comma-delimited string of several modules. (ex.
4742 module1,module2,module2) In this case, the default version of each module will
4743 be changed to the version specified.
4745 The 'migrate_traffic' command can be thought of as a safer version of this
4746 command.""",
4747 uses_basepath=False),
4749 'migrate_traffic': Action(
4750 function='MigrateTraffic',
4751 usage='%prog [options] migrate_traffic [directory]',
4752 short_desc='Migrates traffic to another version.',
4753 long_desc="""
4754 The 'migrate_traffic' command gradually gradually sends an increasing fraction
4755 of traffic your app's traffic from the current default version to another
4756 version. Once all traffic has been migrated, the new version is set as the
4757 default version.
4759 app.yaml specifies the target application, version, and (optionally) module; use
4760 the --application, --version and --module flags to override these values.
4762 Can be thought of as an enhanced version of the 'set_default_version'
4763 command.""",
4765 uses_basepath=False,
4767 hidden=True),
4769 'resource_limits_info': Action(
4770 function='ResourceLimitsInfo',
4771 usage='%prog [options] resource_limits_info <directory>',
4772 short_desc='Get the resource limits.',
4773 long_desc="""
4774 The 'resource_limits_info' command prints the current resource limits that
4775 are enforced."""),
4777 'list_versions': Action(
4778 function='ListVersions',
4779 usage='%prog [options] list_versions <directory>',
4780 short_desc='List all uploaded versions for an app.',
4781 long_desc="""
4782 The 'list_versions' command outputs the uploaded versions for each module of
4783 an application in YAML."""),
4785 'delete_version': Action(
4786 function='DeleteVersion',
4787 usage='%prog [options] delete_version -A app_id -V version '
4788 '[-M module]',
4789 uses_basepath=False,
4790 short_desc='Delete the specified version for an app.',
4791 long_desc="""
4792 The 'delete_version' command deletes the specified version for the specified
4793 application."""),
4797 def main(argv):
4798 logging.basicConfig(format=('%(asctime)s %(levelname)s %(filename)s:'
4799 '%(lineno)s %(message)s '))
4800 try:
4801 result = AppCfgApp(argv).Run()
4802 if result:
4803 sys.exit(result)
4804 except KeyboardInterrupt:
4805 StatusUpdate('Interrupted.')
4806 sys.exit(1)
4809 if __name__ == '__main__':
4810 main(sys.argv)