1.9.30 sync.
[gae.git] / python / google / appengine / tools / appcfg.py
blob64714891bfe27552b9a35a5c5d334340446438c7
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Tool for deploying apps to an app server.
19 Currently, the application only uploads new appversions. To do this, it first
20 walks the directory tree rooted at the path the user specifies, adding all the
21 files it finds to a list. It then uploads the application configuration
22 (app.yaml) to the server using HTTP, followed by uploading each of the files.
23 It then commits the transaction with another request.
25 The bulk of this work is handled by the AppVersionUpload class, which exposes
26 methods to add to the list of files, fetch a list of modified files, upload
27 files, and commit or rollback the transaction.
28 """
29 from __future__ import with_statement
33 import calendar
34 import contextlib
35 import copy
36 import datetime
37 import errno
38 import getpass
39 import hashlib
40 import logging
41 import mimetypes
42 import optparse
43 import os
44 import random
45 import re
46 import shutil
47 import subprocess
48 import sys
49 import tempfile
50 import time
51 import urllib
52 import urllib2
56 import google
57 import yaml
59 from google.appengine.cron import groctimespecification
60 from google.appengine.api import appinfo
61 from google.appengine.api import appinfo_includes
62 from google.appengine.api import backendinfo
63 from google.appengine.api import client_deployinfo
64 from google.appengine.api import croninfo
65 from google.appengine.api import dispatchinfo
66 from google.appengine.api import dosinfo
67 from google.appengine.api import queueinfo
68 from google.appengine.api import yaml_errors
69 from google.appengine.api import yaml_object
70 from google.appengine.datastore import datastore_index
71 from google.appengine.tools import appengine_rpc
73 try:
76 from google.appengine.tools import appengine_rpc_httplib2
77 except ImportError:
78 appengine_rpc_httplib2 = None
79 if sys.version_info[:2] >= (2, 7):
83 from google.appengine.tools import appcfg_java
84 else:
85 appcfg_java = None
87 from google.appengine.tools import augment_mimetypes
88 from google.appengine.tools import bulkloader
89 from google.appengine.tools import sdk_update_checker
93 LIST_DELIMITER = '\n'
94 TUPLE_DELIMITER = '|'
95 BACKENDS_ACTION = 'backends'
96 BACKENDS_MESSAGE = ('Warning: This application uses Backends, a deprecated '
97 'feature that has been replaced by Modules, which '
98 'offers additional functionality. Please convert your '
99 'backends to modules as described at: ')
100 _CONVERTING_URL = (
101 'https://developers.google.com/appengine/docs/%s/modules/converting')
104 MAX_LOG_LEVEL = 4
107 MAX_BATCH_SIZE = 3200000
108 MAX_BATCH_COUNT = 100
109 MAX_BATCH_FILE_SIZE = 200000
110 BATCH_OVERHEAD = 500
117 verbosity = 1
120 PREFIXED_BY_ADMIN_CONSOLE_RE = '^(?:admin-console)(.*)'
123 SDK_PRODUCT = 'appcfg_py'
126 DAY = 24*3600
127 SUNDAY = 6
129 SUPPORTED_RUNTIMES = (
130 'contrib-dart', 'dart', 'go', 'php', 'python', 'python27', 'java', 'java7',
131 'vm', 'custom')
136 MEGA = 1024 * 1024
137 MILLION = 1000 * 1000
138 DEFAULT_RESOURCE_LIMITS = {
139 'max_file_size': 32 * MILLION,
140 'max_blob_size': 32 * MILLION,
141 'max_files_to_clone': 100,
142 'max_total_file_size': 150 * MEGA,
143 'max_file_count': 10000,
146 # Client ID and secrets are managed in the Google API console.
152 APPCFG_CLIENT_ID = '550516889912.apps.googleusercontent.com'
153 APPCFG_CLIENT_NOTSOSECRET = 'ykPq-0UYfKNprLRjVx1hBBar'
154 APPCFG_SCOPES = ('https://www.googleapis.com/auth/appengine.admin',
155 'https://www.googleapis.com/auth/cloud-platform')
158 STATIC_FILE_PREFIX = '__static__'
162 METADATA_BASE = 'http://metadata.google.internal'
163 SERVICE_ACCOUNT_BASE = (
164 'computeMetadata/v1beta1/instance/service-accounts/default')
167 APP_YAML_FILENAME = 'app.yaml'
172 GO_APP_BUILDER = os.path.join('goroot', 'bin', 'go-app-builder')
173 if sys.platform.startswith('win'):
174 GO_APP_BUILDER += '.exe'
177 augment_mimetypes.init()
180 class Error(Exception):
181 pass
184 class OAuthNotAvailable(Error):
185 """The appengine_rpc_httplib2 module could not be imported."""
186 pass
189 class CannotStartServingError(Error):
190 """We could not start serving the version being uploaded."""
191 pass
194 def PrintUpdate(msg, error_fh=sys.stderr):
195 """Print a message to stderr or the given file-like object.
197 If 'verbosity' is greater than 0, print the message.
199 Args:
200 msg: The string to print.
201 error_fh: Where to send the message.
203 if verbosity > 0:
204 timestamp = datetime.datetime.now()
205 print >>error_fh, '%s %s' % (timestamp.strftime('%I:%M %p'), msg)
208 def StatusUpdate(msg, error_fh=sys.stderr):
209 """Print a status message to stderr or the given file-like object."""
210 PrintUpdate(msg, error_fh)
213 def BackendsStatusUpdate(runtime, error_fh=sys.stderr):
214 """Print the Backends status message based on current runtime.
216 Args:
217 runtime: String name of current runtime.
218 error_fh: Where to send the message.
220 language = runtime
221 if language == 'python27':
222 language = 'python'
223 elif language == 'java7':
224 language = 'java'
225 if language == 'python' or language == 'java':
226 StatusUpdate(BACKENDS_MESSAGE + (_CONVERTING_URL % language), error_fh)
229 def ErrorUpdate(msg, error_fh=sys.stderr):
230 """Print an error message to stderr."""
231 PrintUpdate(msg, error_fh)
234 def _PrintErrorAndExit(stream, msg, exit_code=2):
235 """Prints the given error message and exists the program.
237 Args:
238 stream: The stream (e.g. StringIO or file) to write the message to.
239 msg: The error message to display as a string.
240 exit_code: The integer code to pass to sys.exit().
242 stream.write(msg)
243 sys.exit(exit_code)
246 def JavaSupported():
247 """True if Java is supported by this SDK."""
250 tools_java_dir = os.path.join(os.path.dirname(appcfg_java.__file__), 'java')
251 return os.path.isdir(tools_java_dir)
254 @contextlib.contextmanager
255 def TempChangeField(obj, field_name, new_value):
256 """Context manager to change a field value on an object temporarily.
258 Args:
259 obj: The object to change the field on.
260 field_name: The field name to change.
261 new_value: The new value.
263 Yields:
264 The old value.
266 old_value = getattr(obj, field_name)
267 setattr(obj, field_name, new_value)
268 yield old_value
269 setattr(obj, field_name, old_value)
272 class FileClassification(object):
273 """A class to hold a file's classification.
275 This class both abstracts away the details of how we determine
276 whether a file is a regular, static or error file as well as acting
277 as a container for various metadata about the file.
280 def __init__(self, config, filename, error_fh=sys.stderr):
281 """Initializes a FileClassification instance.
283 Args:
284 config: The app.yaml object to check the filename against.
285 filename: The name of the file.
286 error_fh: Where to send status and error messages.
288 self.__error_fh = error_fh
289 self.__static_mime_type = self.__GetMimeTypeIfStaticFile(config, filename)
290 self.__static_app_readable = self.__GetAppReadableIfStaticFile(config,
291 filename)
292 self.__error_mime_type, self.__error_code = self.__LookupErrorBlob(config,
293 filename)
295 def __GetMimeTypeIfStaticFile(self, config, filename):
296 """Looks up the mime type for 'filename'.
298 Uses the handlers in 'config' to determine if the file should
299 be treated as a static file.
301 Args:
302 config: The app.yaml object to check the filename against.
303 filename: The name of the file.
305 Returns:
306 The mime type string. For example, 'text/plain' or 'image/gif'.
307 None if this is not a static file.
309 if self.__FileNameImpliesStaticFile(filename):
310 return self.__MimeType(filename)
311 for handler in config.handlers:
312 handler_type = handler.GetHandlerType()
313 if handler_type in ('static_dir', 'static_files'):
314 if handler_type == 'static_dir':
315 regex = os.path.join(re.escape(handler.GetHandler()), '.*')
316 else:
317 regex = handler.upload
318 if re.match(regex, filename):
319 return handler.mime_type or self.__MimeType(filename)
320 return None
322 @staticmethod
323 def __FileNameImpliesStaticFile(filename):
324 """True if the name of a file implies that it is a static resource.
326 For Java applications specified with web.xml and appengine-web.xml, we
327 create a staging directory that includes a __static__ hierarchy containing
328 links to all files that are implied static by the contents of those XML
329 files. So if a file has been copied into that directory then we can assume
330 it is static.
332 Args:
333 filename: The full path to the file.
335 Returns:
336 True if the file should be considered a static resource based on its name.
338 static = '__static__' + os.sep
339 return static in filename
341 @staticmethod
342 def __GetAppReadableIfStaticFile(config, filename):
343 """Looks up whether a static file is readable by the application.
345 Uses the handlers in 'config' to determine if the file should
346 be treated as a static file and if so, if the file should be readable by the
347 application.
349 Args:
350 config: The AppInfoExternal object to check the filename against.
351 filename: The name of the file.
353 Returns:
354 True if the file is static and marked as app readable, False otherwise.
356 for handler in config.handlers:
357 handler_type = handler.GetHandlerType()
358 if handler_type in ('static_dir', 'static_files'):
359 if handler_type == 'static_dir':
360 regex = os.path.join(re.escape(handler.GetHandler()), '.*')
361 else:
362 regex = handler.upload
363 if re.match(regex, filename):
364 return handler.application_readable
365 return False
367 def __LookupErrorBlob(self, config, filename):
368 """Looks up the mime type and error_code for 'filename'.
370 Uses the error handlers in 'config' to determine if the file should
371 be treated as an error blob.
373 Args:
374 config: The app.yaml object to check the filename against.
375 filename: The name of the file.
377 Returns:
379 A tuple of (mime_type, error_code), or (None, None) if this is not an
380 error blob. For example, ('text/plain', default) or ('image/gif',
381 timeout) or (None, None).
383 if not config.error_handlers:
384 return (None, None)
385 for error_handler in config.error_handlers:
386 if error_handler.file == filename:
387 error_code = error_handler.error_code
388 error_code = error_code or 'default'
389 if error_handler.mime_type:
390 return (error_handler.mime_type, error_code)
391 else:
392 return (self.__MimeType(filename), error_code)
393 return (None, None)
395 def __MimeType(self, filename, default='application/octet-stream'):
396 guess = mimetypes.guess_type(filename)[0]
397 if guess is None:
398 print >>self.__error_fh, ('Could not guess mimetype for %s. Using %s.'
399 % (filename, default))
400 return default
401 return guess
403 def IsApplicationFile(self):
404 return bool((not self.IsStaticFile() or self.__static_app_readable) and
405 not self.IsErrorFile())
407 def IsStaticFile(self):
408 return bool(self.__static_mime_type)
410 def StaticMimeType(self):
411 return self.__static_mime_type
413 def IsErrorFile(self):
414 return bool(self.__error_mime_type)
416 def ErrorMimeType(self):
417 return self.__error_mime_type
419 def ErrorCode(self):
420 return self.__error_code
423 def BuildClonePostBody(file_tuples):
424 """Build the post body for the /api/clone{files,blobs,errorblobs} urls.
426 Args:
427 file_tuples: A list of tuples. Each tuple should contain the entries
428 appropriate for the endpoint in question.
430 Returns:
431 A string containing the properly delimited tuples.
433 file_list = []
434 for tup in file_tuples:
435 path = tup[1]
436 tup = tup[2:]
437 file_list.append(TUPLE_DELIMITER.join([path] + list(tup)))
438 return LIST_DELIMITER.join(file_list)
441 def _GetRemoteResourceLimits(logging_context):
442 """Get the resource limit as reported by the admin console.
444 Get the resource limits by querying the admin_console/appserver. The
445 actual limits returned depends on the server we are talking to and
446 could be missing values we expect or include extra values.
448 Args:
449 logging_context: The _ClientDeployLoggingContext for this upload.
451 Returns:
452 A dictionary.
454 try:
455 yaml_data = logging_context.Send('/api/appversion/getresourcelimits')
457 except urllib2.HTTPError, err:
461 if err.code != 404:
462 raise
463 return {}
465 return yaml.safe_load(yaml_data)
468 def GetResourceLimits(logging_context, error_fh=sys.stderr):
469 """Gets the resource limits.
471 Gets the resource limits that should be applied to apps. Any values
472 that the server does not know about will have their default value
473 reported (although it is also possible for the server to report
474 values we don't know about).
476 Args:
477 logging_context: The _ClientDeployLoggingContext for this upload.
478 error_fh: Where to send status and error messages.
480 Returns:
481 A dictionary.
483 resource_limits = DEFAULT_RESOURCE_LIMITS.copy()
484 StatusUpdate('Getting current resource limits.', error_fh)
485 resource_limits.update(_GetRemoteResourceLimits(logging_context))
486 logging.debug('Using resource limits: %s', resource_limits)
487 return resource_limits
490 def RetryWithBackoff(callable_func, retry_notify_func,
491 initial_delay=1, backoff_factor=2,
492 max_delay=60, max_tries=20):
493 """Calls a function multiple times, backing off more and more each time.
495 Args:
496 callable_func: A function that performs some operation that should be
497 retried a number of times upon failure. Signature: () -> (done, value)
498 If 'done' is True, we'll immediately return (True, value)
499 If 'done' is False, we'll delay a bit and try again, unless we've
500 hit the 'max_tries' limit, in which case we'll return (False, value).
501 retry_notify_func: This function will be called immediately before the
502 next retry delay. Signature: (value, delay) -> None
503 'value' is the value returned by the last call to 'callable_func'
504 'delay' is the retry delay, in seconds
505 initial_delay: Initial delay after first try, in seconds.
506 backoff_factor: Delay will be multiplied by this factor after each try.
507 max_delay: Maximum delay, in seconds.
508 max_tries: Maximum number of tries (the first one counts).
510 Returns:
511 What the last call to 'callable_func' returned, which is of the form
512 (done, value). If 'done' is True, you know 'callable_func' returned True
513 before we ran out of retries. If 'done' is False, you know 'callable_func'
514 kept returning False and we ran out of retries.
516 Raises:
517 Whatever the function raises--an exception will immediately stop retries.
520 delay = initial_delay
521 num_tries = 0
523 while True:
524 done, opaque_value = callable_func()
525 num_tries += 1
527 if done:
528 return True, opaque_value
530 if num_tries >= max_tries:
531 return False, opaque_value
533 retry_notify_func(opaque_value, delay)
534 time.sleep(delay)
535 delay = min(delay * backoff_factor, max_delay)
538 def RetryNoBackoff(callable_func,
539 retry_notify_func,
540 delay=5,
541 max_tries=200):
542 """Calls a function multiple times, with the same delay each time.
544 Args:
545 callable_func: A function that performs some operation that should be
546 retried a number of times upon failure. Signature: () -> (done, value)
547 If 'done' is True, we'll immediately return (True, value)
548 If 'done' is False, we'll delay a bit and try again, unless we've
549 hit the 'max_tries' limit, in which case we'll return (False, value).
550 retry_notify_func: This function will be called immediately before the
551 next retry delay. Signature: (value, delay) -> None
552 'value' is the value returned by the last call to 'callable_func'
553 'delay' is the retry delay, in seconds
554 delay: Delay between tries, in seconds.
555 max_tries: Maximum number of tries (the first one counts).
557 Returns:
558 What the last call to 'callable_func' returned, which is of the form
559 (done, value). If 'done' is True, you know 'callable_func' returned True
560 before we ran out of retries. If 'done' is False, you know 'callable_func'
561 kept returning False and we ran out of retries.
563 Raises:
564 Whatever the function raises--an exception will immediately stop retries.
567 return RetryWithBackoff(callable_func,
568 retry_notify_func,
569 delay,
571 delay,
572 max_tries)
575 def MigratePython27Notice():
576 """Tells the user that Python 2.5 runtime is deprecated.
578 Encourages the user to migrate from Python 2.5 to Python 2.7.
580 Prints a message to sys.stdout. The caller should have tested that the user is
581 using Python 2.5, so as not to spuriously display this message.
583 print (
584 'WARNING: This application is using the Python 2.5 runtime, which is '
585 'deprecated! It should be updated to the Python 2.7 runtime as soon as '
586 'possible, which offers performance improvements and many new features. '
587 'Learn how simple it is to migrate your application to Python 2.7 at '
588 'https://developers.google.com/appengine/docs/python/python25/migrate27.')
591 class IndexDefinitionUpload(object):
592 """Provides facilities to upload index definitions to the hosting service."""
594 def __init__(self, rpcserver, definitions, error_fh=sys.stderr):
595 """Creates a new DatastoreIndexUpload.
597 Args:
598 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
599 or TestRpcServer.
600 definitions: An IndexDefinitions object.
601 error_fh: Where to send status and error messages.
603 self.rpcserver = rpcserver
604 self.definitions = definitions
605 self.error_fh = error_fh
607 def DoUpload(self):
608 """Uploads the index definitions."""
609 StatusUpdate('Uploading index definitions.', self.error_fh)
611 with TempChangeField(self.definitions, 'application', None) as app_id:
612 self.rpcserver.Send('/api/datastore/index/add',
613 app_id=app_id,
614 payload=self.definitions.ToYAML())
617 class CronEntryUpload(object):
618 """Provides facilities to upload cron entries to the hosting service."""
620 def __init__(self, rpcserver, cron, error_fh=sys.stderr):
621 """Creates a new CronEntryUpload.
623 Args:
624 rpcserver: The RPC server to use. Should be an instance of a subclass of
625 AbstractRpcServer
626 cron: The CronInfoExternal object loaded from the cron.yaml file.
627 error_fh: Where to send status and error messages.
629 self.rpcserver = rpcserver
630 self.cron = cron
631 self.error_fh = error_fh
633 def DoUpload(self):
634 """Uploads the cron entries."""
635 StatusUpdate('Uploading cron entries.', self.error_fh)
637 with TempChangeField(self.cron, 'application', None) as app_id:
638 self.rpcserver.Send('/api/cron/update',
639 app_id=app_id,
640 payload=self.cron.ToYAML())
643 class QueueEntryUpload(object):
644 """Provides facilities to upload task queue entries to the hosting service."""
646 def __init__(self, rpcserver, queue, error_fh=sys.stderr):
647 """Creates a new QueueEntryUpload.
649 Args:
650 rpcserver: The RPC server to use. Should be an instance of a subclass of
651 AbstractRpcServer
652 queue: The QueueInfoExternal object loaded from the queue.yaml file.
653 error_fh: Where to send status and error messages.
655 self.rpcserver = rpcserver
656 self.queue = queue
657 self.error_fh = error_fh
659 def DoUpload(self):
660 """Uploads the task queue entries."""
661 StatusUpdate('Uploading task queue entries.', self.error_fh)
663 with TempChangeField(self.queue, 'application', None) as app_id:
664 self.rpcserver.Send('/api/queue/update',
665 app_id=app_id,
666 payload=self.queue.ToYAML())
669 class DispatchEntryUpload(object):
670 """Provides facilities to upload dispatch entries to the hosting service."""
672 def __init__(self, rpcserver, dispatch, error_fh=sys.stderr):
673 """Creates a new DispatchEntryUpload.
675 Args:
676 rpcserver: The RPC server to use. Should be an instance of a subclass of
677 AbstractRpcServer
678 dispatch: The DispatchInfoExternal object loaded from the dispatch.yaml
679 file.
680 error_fh: Where to send status and error messages.
682 self.rpcserver = rpcserver
683 self.dispatch = dispatch
684 self.error_fh = error_fh
686 def DoUpload(self):
687 """Uploads the dispatch entries."""
688 StatusUpdate('Uploading dispatch entries.', self.error_fh)
689 self.rpcserver.Send('/api/dispatch/update',
690 app_id=self.dispatch.application,
691 payload=self.dispatch.ToYAML())
694 class DosEntryUpload(object):
695 """Provides facilities to upload dos entries to the hosting service."""
697 def __init__(self, rpcserver, dos, error_fh=sys.stderr):
698 """Creates a new DosEntryUpload.
700 Args:
701 rpcserver: The RPC server to use. Should be an instance of a subclass of
702 AbstractRpcServer.
703 dos: The DosInfoExternal object loaded from the dos.yaml file.
704 error_fh: Where to send status and error messages.
706 self.rpcserver = rpcserver
707 self.dos = dos
708 self.error_fh = error_fh
710 def DoUpload(self):
711 """Uploads the dos entries."""
712 StatusUpdate('Uploading DOS entries.', self.error_fh)
714 with TempChangeField(self.dos, 'application', None) as app_id:
715 self.rpcserver.Send('/api/dos/update',
716 app_id=app_id,
717 payload=self.dos.ToYAML())
720 class PagespeedEntryUpload(object):
721 """Provides facilities to upload pagespeed configs to the hosting service."""
723 def __init__(self, rpcserver, config, pagespeed, error_fh=sys.stderr):
724 """Creates a new PagespeedEntryUpload.
726 Args:
727 rpcserver: The RPC server to use. Should be an instance of a subclass of
728 AbstractRpcServer.
729 config: The AppInfoExternal object derived from the app.yaml file.
730 pagespeed: The PagespeedEntry object from config.
731 error_fh: Where to send status and error messages.
733 self.rpcserver = rpcserver
734 self.config = config
735 self.pagespeed = pagespeed
736 self.error_fh = error_fh
738 def DoUpload(self):
739 """Uploads the pagespeed entries."""
741 pagespeed_yaml = ''
742 if self.pagespeed:
743 StatusUpdate('Uploading PageSpeed configuration.', self.error_fh)
744 pagespeed_yaml = self.pagespeed.ToYAML()
745 try:
746 self.rpcserver.Send('/api/appversion/updatepagespeed',
747 app_id=self.config.application,
748 version=self.config.version,
749 payload=pagespeed_yaml)
750 except urllib2.HTTPError, err:
760 if err.code != 404 or self.pagespeed is not None:
761 raise
764 class DefaultVersionSet(object):
765 """Provides facilities to set the default (serving) version."""
767 def __init__(self, rpcserver, app_id, module, version, error_fh=sys.stderr):
768 """Creates a new DefaultVersionSet.
770 Args:
771 rpcserver: The RPC server to use. Should be an instance of a subclass of
772 AbstractRpcServer.
773 app_id: The application to make the change to.
774 module: The module to set the default version of (if any).
775 version: The version to set as the default.
776 error_fh: Where to send status and error messages.
778 self.rpcserver = rpcserver
779 self.app_id = app_id
780 self.module = module
781 self.version = version
782 self.error_fh = error_fh
784 def SetVersion(self):
785 """Sets the default version."""
786 if self.module:
788 modules = self.module.split(',')
789 if len(modules) > 1:
790 StatusUpdate('Setting the default version of modules %s of application '
791 '%s to %s.' % (', '.join(modules),
792 self.app_id,
793 self.version),
794 self.error_fh)
799 params = [('app_id', self.app_id), ('version', self.version)]
800 params.extend(('module', module) for module in modules)
801 url = '/api/appversion/setdefault?' + urllib.urlencode(sorted(params))
802 self.rpcserver.Send(url)
803 return
805 else:
806 StatusUpdate('Setting default version of module %s of application %s '
807 'to %s.' % (self.module, self.app_id, self.version),
808 self.error_fh)
809 else:
810 StatusUpdate('Setting default version of application %s to %s.'
811 % (self.app_id, self.version), self.error_fh)
812 self.rpcserver.Send('/api/appversion/setdefault',
813 app_id=self.app_id,
814 module=self.module,
815 version=self.version)
818 class TrafficMigrator(object):
819 """Provides facilities to migrate traffic."""
821 def __init__(self, rpcserver, app_id, version, error_fh=sys.stderr):
822 """Creates a new TrafficMigrator.
824 Args:
825 rpcserver: The RPC server to use. Should be an instance of a subclass of
826 AbstractRpcServer.
827 app_id: The application to make the change to.
828 version: The version to set as the default.
829 error_fh: Where to send status and error messages.
831 self.rpcserver = rpcserver
832 self.app_id = app_id
833 self.version = version
834 self.error_fh = error_fh
836 def MigrateTraffic(self):
837 """Migrates traffic."""
838 StatusUpdate('Migrating traffic of application %s to %s.'
839 % (self.app_id, self.version), self.error_fh)
840 self.rpcserver.Send('/api/appversion/migratetraffic',
841 app_id=self.app_id,
842 version=self.version)
845 class IndexOperation(object):
846 """Provide facilities for writing Index operation commands."""
848 def __init__(self, rpcserver, error_fh=sys.stderr):
849 """Creates a new IndexOperation.
851 Args:
852 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
853 or TestRpcServer.
854 error_fh: Where to send status and error messages.
856 self.rpcserver = rpcserver
857 self.error_fh = error_fh
859 def DoDiff(self, definitions):
860 """Retrieve diff file from the server.
862 Args:
863 definitions: datastore_index.IndexDefinitions as loaded from users
864 index.yaml file.
866 Returns:
867 A pair of datastore_index.IndexDefinitions objects. The first record
868 is the set of indexes that are present in the index.yaml file but missing
869 from the server. The second record is the set of indexes that are
870 present on the server but missing from the index.yaml file (indicating
871 that these indexes should probably be vacuumed).
873 StatusUpdate('Fetching index definitions diff.', self.error_fh)
874 with TempChangeField(definitions, 'application', None) as app_id:
875 response = self.rpcserver.Send('/api/datastore/index/diff',
876 app_id=app_id,
877 payload=definitions.ToYAML())
879 return datastore_index.ParseMultipleIndexDefinitions(response)
881 def DoDelete(self, definitions, app_id):
882 """Delete indexes from the server.
884 Args:
885 definitions: Index definitions to delete from datastore.
886 app_id: The application id.
888 Returns:
889 A single datstore_index.IndexDefinitions containing indexes that were
890 not deleted, probably because they were already removed. This may
891 be normal behavior as there is a potential race condition between fetching
892 the index-diff and sending deletion confirmation through.
894 StatusUpdate('Deleting selected index definitions.', self.error_fh)
896 response = self.rpcserver.Send('/api/datastore/index/delete',
897 app_id=app_id,
898 payload=definitions.ToYAML())
899 return datastore_index.ParseIndexDefinitions(response)
902 class VacuumIndexesOperation(IndexOperation):
903 """Provide facilities to request the deletion of datastore indexes."""
905 def __init__(self, rpcserver, force, confirmation_fn=raw_input,
906 error_fh=sys.stderr):
907 """Creates a new VacuumIndexesOperation.
909 Args:
910 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
911 or TestRpcServer.
912 force: True to force deletion of indexes, else False.
913 confirmation_fn: Function used for getting input form user.
914 error_fh: Where to send status and error messages.
916 super(VacuumIndexesOperation, self).__init__(rpcserver, error_fh)
917 self.force = force
918 self.confirmation_fn = confirmation_fn
920 def GetConfirmation(self, index):
921 """Get confirmation from user to delete an index.
923 This method will enter an input loop until the user provides a
924 response it is expecting. Valid input is one of three responses:
926 y: Confirm deletion of index.
927 n: Do not delete index.
928 a: Delete all indexes without asking for further confirmation.
930 If the user enters nothing at all, the default action is to skip
931 that index and do not delete.
933 If the user selects 'a', as a side effect, the 'force' flag is set.
935 Args:
936 index: Index to confirm.
938 Returns:
939 True if user enters 'y' or 'a'. False if user enter 'n'.
941 while True:
943 print 'This index is no longer defined in your index.yaml file.'
944 print
945 print index.ToYAML()
946 print
949 confirmation = self.confirmation_fn(
950 'Are you sure you want to delete this index? (N/y/a): ')
951 confirmation = confirmation.strip().lower()
954 if confirmation == 'y':
955 return True
956 elif confirmation == 'n' or not confirmation:
957 return False
958 elif confirmation == 'a':
959 self.force = True
960 return True
961 else:
962 print 'Did not understand your response.'
964 def DoVacuum(self, definitions):
965 """Vacuum indexes in datastore.
967 This method will query the server to determine which indexes are not
968 being used according to the user's local index.yaml file. Once it has
969 made this determination, it confirms with the user which unused indexes
970 should be deleted. Once confirmation for each index is receives, it
971 deletes those indexes.
973 Because another user may in theory delete the same indexes at the same
974 time as the user, there is a potential race condition. In this rare cases,
975 some of the indexes previously confirmed for deletion will not be found.
976 The user is notified which indexes these were.
978 Args:
979 definitions: datastore_index.IndexDefinitions as loaded from users
980 index.yaml file.
983 unused_new_indexes, notused_indexes = self.DoDiff(definitions)
986 deletions = datastore_index.IndexDefinitions(indexes=[])
987 if notused_indexes.indexes is not None:
988 for index in notused_indexes.indexes:
989 if self.force or self.GetConfirmation(index):
990 deletions.indexes.append(index)
993 if deletions.indexes:
994 not_deleted = self.DoDelete(deletions, definitions.application)
997 if not_deleted.indexes:
998 not_deleted_count = len(not_deleted.indexes)
999 if not_deleted_count == 1:
1000 warning_message = ('An index was not deleted. Most likely this is '
1001 'because it no longer exists.\n\n')
1002 else:
1003 warning_message = ('%d indexes were not deleted. Most likely this '
1004 'is because they no longer exist.\n\n'
1005 % not_deleted_count)
1006 for index in not_deleted.indexes:
1007 warning_message += index.ToYAML()
1008 logging.warning(warning_message)
1011 class LogsRequester(object):
1012 """Provide facilities to export request logs."""
1014 def __init__(self,
1015 rpcserver,
1016 app_id,
1017 module,
1018 version_id,
1019 output_file,
1020 num_days,
1021 append,
1022 severity,
1023 end,
1024 vhost,
1025 include_vhost,
1026 include_all=None,
1027 time_func=time.time,
1028 error_fh=sys.stderr):
1029 """Constructor.
1031 Args:
1032 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1033 or TestRpcServer.
1034 app_id: The application to fetch logs from.
1035 module: The module of the app to fetch logs from, optional.
1036 version_id: The version of the app to fetch logs for.
1037 output_file: Output file name.
1038 num_days: Number of days worth of logs to export; 0 for all available.
1039 append: True if appending to an existing file.
1040 severity: App log severity to request (0-4); None for no app logs.
1041 end: date object representing last day of logs to return.
1042 vhost: The virtual host of log messages to get. None for all hosts.
1043 include_vhost: If true, the virtual host is included in log messages.
1044 include_all: If true, we add to the log message everything we know
1045 about the request.
1046 time_func: A time.time() compatible function, which can be overridden for
1047 testing.
1048 error_fh: Where to send status and error messages.
1051 self.rpcserver = rpcserver
1052 self.app_id = app_id
1053 self.output_file = output_file
1054 self.append = append
1055 self.num_days = num_days
1056 self.severity = severity
1057 self.vhost = vhost
1058 self.include_vhost = include_vhost
1059 self.include_all = include_all
1060 self.error_fh = error_fh
1062 self.module = module
1063 self.version_id = version_id
1064 self.sentinel = None
1065 self.write_mode = 'w'
1066 if self.append:
1067 self.sentinel = FindSentinel(self.output_file)
1068 self.write_mode = 'a'
1071 self.skip_until = False
1072 now = PacificDate(time_func())
1073 if end < now:
1074 self.skip_until = end
1075 else:
1077 end = now
1079 self.valid_dates = None
1080 if self.num_days:
1081 start = end - datetime.timedelta(self.num_days - 1)
1082 self.valid_dates = (start, end)
1084 def DownloadLogs(self):
1085 """Download the requested logs.
1087 This will write the logs to the file designated by
1088 self.output_file, or to stdout if the filename is '-'.
1089 Multiple roundtrips to the server may be made.
1091 if self.module:
1092 StatusUpdate('Downloading request logs for app %s module %s version %s.' %
1093 (self.app_id, self.module, self.version_id), self.error_fh)
1094 else:
1095 StatusUpdate('Downloading request logs for app %s version %s.' %
1096 (self.app_id, self.version_id), self.error_fh)
1102 tf = tempfile.TemporaryFile()
1103 last_offset = None
1104 try:
1105 while True:
1106 try:
1107 new_offset = self.RequestLogLines(tf, last_offset)
1108 if not new_offset or new_offset == last_offset:
1109 break
1110 last_offset = new_offset
1111 except KeyboardInterrupt:
1112 StatusUpdate('Keyboard interrupt; saving data downloaded so far.',
1113 self.error_fh)
1114 break
1115 StatusUpdate('Copying request logs to %r.' % self.output_file,
1116 self.error_fh)
1117 if self.output_file == '-':
1118 of = sys.stdout
1119 else:
1120 try:
1121 of = open(self.output_file, self.write_mode)
1122 except IOError, err:
1123 StatusUpdate('Can\'t write %r: %s.' % (self.output_file, err))
1124 sys.exit(1)
1125 try:
1126 line_count = CopyReversedLines(tf, of)
1127 finally:
1128 of.flush()
1129 if of is not sys.stdout:
1130 of.close()
1131 finally:
1132 tf.close()
1133 StatusUpdate('Copied %d records.' % line_count, self.error_fh)
1135 def RequestLogLines(self, tf, offset):
1136 """Make a single roundtrip to the server.
1138 Args:
1139 tf: Writable binary stream to which the log lines returned by
1140 the server are written, stripped of headers, and excluding
1141 lines skipped due to self.sentinel or self.valid_dates filtering.
1142 offset: Offset string for a continued request; None for the first.
1144 Returns:
1145 The offset string to be used for the next request, if another
1146 request should be issued; or None, if not.
1148 logging.info('Request with offset %r.', offset)
1149 kwds = {'app_id': self.app_id,
1150 'version': self.version_id,
1151 'limit': 1000,
1152 'no_header': 1,
1154 if self.module:
1155 kwds['module'] = self.module
1156 if offset:
1157 kwds['offset'] = offset
1158 if self.severity is not None:
1159 kwds['severity'] = str(self.severity)
1160 if self.vhost is not None:
1161 kwds['vhost'] = str(self.vhost)
1162 if self.include_vhost is not None:
1163 kwds['include_vhost'] = str(self.include_vhost)
1164 if self.include_all is not None:
1165 kwds['include_all'] = str(self.include_all)
1166 response = self.rpcserver.Send('/api/request_logs', payload=None, **kwds)
1167 response = response.replace('\r', '\0')
1168 lines = response.splitlines()
1169 logging.info('Received %d bytes, %d records.', len(response), len(lines))
1170 offset = None
1172 valid_dates = self.valid_dates
1173 sentinel = self.sentinel
1174 skip_until = self.skip_until
1175 len_sentinel = None
1176 if sentinel:
1177 len_sentinel = len(sentinel)
1178 for line in lines:
1179 if line.startswith('#'):
1180 match = re.match(r'^#\s*next_offset=(\S+)\s*$', line)
1183 if match and match.group(1) != 'None':
1184 offset = match.group(1)
1185 continue
1187 if (sentinel and
1188 line.startswith(sentinel) and
1189 line[len_sentinel : len_sentinel+1] in ('', '\0')):
1190 return None
1192 linedate = DateOfLogLine(line)
1194 if not linedate:
1195 continue
1197 if skip_until:
1198 if linedate > skip_until:
1199 continue
1200 else:
1202 self.skip_until = skip_until = False
1204 if valid_dates and not valid_dates[0] <= linedate <= valid_dates[1]:
1205 return None
1206 tf.write(line + '\n')
1207 if not lines:
1208 return None
1209 return offset
1212 def DateOfLogLine(line):
1213 """Returns a date object representing the log line's timestamp.
1215 Args:
1216 line: a log line string.
1217 Returns:
1218 A date object representing the timestamp or None if parsing fails.
1220 m = re.compile(r'[^[]+\[(\d+/[A-Za-z]+/\d+):[^\d]*').match(line)
1221 if not m:
1222 return None
1223 try:
1224 return datetime.date(*time.strptime(m.group(1), '%d/%b/%Y')[:3])
1225 except ValueError:
1226 return None
1229 def PacificDate(now):
1230 """For a UTC timestamp, return the date in the US/Pacific timezone.
1232 Args:
1233 now: A posix timestamp giving current UTC time.
1235 Returns:
1236 A date object representing what day it is in the US/Pacific timezone.
1239 return datetime.date(*time.gmtime(PacificTime(now))[:3])
1242 def PacificTime(now):
1243 """Helper to return the number of seconds between UTC and Pacific time.
1245 This is needed to compute today's date in Pacific time (more
1246 specifically: Mountain View local time), which is how request logs
1247 are reported. (Google servers always report times in Mountain View
1248 local time, regardless of where they are physically located.)
1250 This takes (post-2006) US DST into account. Pacific time is either
1251 8 hours or 7 hours west of UTC, depending on whether DST is in
1252 effect. Since 2007, US DST starts on the Second Sunday in March
1253 March, and ends on the first Sunday in November. (Reference:
1254 http://aa.usno.navy.mil/faq/docs/daylight_time.php.)
1256 Note that the server doesn't report its local time (the HTTP Date
1257 header uses UTC), and the client's local time is irrelevant.
1259 Args:
1260 now: A posix timestamp giving current UTC time.
1262 Returns:
1263 A pseudo-posix timestamp giving current Pacific time. Passing
1264 this through time.gmtime() will produce a tuple in Pacific local
1265 time.
1267 now -= 8*3600
1268 if IsPacificDST(now):
1269 now += 3600
1270 return now
1273 def IsPacificDST(now):
1274 """Helper for PacificTime to decide whether now is Pacific DST (PDT).
1276 Args:
1277 now: A pseudo-posix timestamp giving current time in PST.
1279 Returns:
1280 True if now falls within the range of DST, False otherwise.
1282 pst = time.gmtime(now)
1283 year = pst[0]
1284 assert year >= 2007
1286 begin = calendar.timegm((year, 3, 8, 2, 0, 0, 0, 0, 0))
1287 while time.gmtime(begin).tm_wday != SUNDAY:
1288 begin += DAY
1290 end = calendar.timegm((year, 11, 1, 2, 0, 0, 0, 0, 0))
1291 while time.gmtime(end).tm_wday != SUNDAY:
1292 end += DAY
1293 return begin <= now < end
1296 def CopyReversedLines(instream, outstream, blocksize=2**16):
1297 r"""Copy lines from input stream to output stream in reverse order.
1299 As a special feature, null bytes in the input are turned into
1300 newlines followed by tabs in the output, but these 'sub-lines'
1301 separated by null bytes are not reversed. E.g. If the input is
1302 'A\0B\nC\0D\n', the output is 'C\n\tD\nA\n\tB\n'.
1304 Args:
1305 instream: A seekable stream open for reading in binary mode.
1306 outstream: A stream open for writing; doesn't have to be seekable or binary.
1307 blocksize: Optional block size for buffering, for unit testing.
1309 Returns:
1310 The number of lines copied.
1312 line_count = 0
1313 instream.seek(0, 2)
1314 last_block = instream.tell() // blocksize
1315 spillover = ''
1316 for iblock in xrange(last_block + 1, -1, -1):
1317 instream.seek(iblock * blocksize)
1318 data = instream.read(blocksize)
1319 lines = data.splitlines(True)
1320 lines[-1:] = ''.join(lines[-1:] + [spillover]).splitlines(True)
1321 if lines and not lines[-1].endswith('\n'):
1323 lines[-1] += '\n'
1324 lines.reverse()
1325 if lines and iblock > 0:
1326 spillover = lines.pop()
1327 if lines:
1328 line_count += len(lines)
1329 data = ''.join(lines).replace('\0', '\n\t')
1330 outstream.write(data)
1331 return line_count
1334 def FindSentinel(filename, blocksize=2**16, error_fh=sys.stderr):
1335 """Return the sentinel line from the output file.
1337 Args:
1338 filename: The filename of the output file. (We'll read this file.)
1339 blocksize: Optional block size for buffering, for unit testing.
1340 error_fh: Where to send status and error messages.
1342 Returns:
1343 The contents of the last line in the file that doesn't start with
1344 a tab, with its trailing newline stripped; or None if the file
1345 couldn't be opened or no such line could be found by inspecting
1346 the last 'blocksize' bytes of the file.
1348 if filename == '-':
1349 StatusUpdate('Can\'t combine --append with output to stdout.',
1350 error_fh)
1351 sys.exit(2)
1352 try:
1353 fp = open(filename, 'rb')
1354 except IOError, err:
1355 StatusUpdate('Append mode disabled: can\'t read %r: %s.' % (filename, err),
1356 error_fh)
1357 return None
1358 try:
1359 fp.seek(0, 2)
1360 fp.seek(max(0, fp.tell() - blocksize))
1361 lines = fp.readlines()
1362 del lines[:1]
1363 sentinel = None
1364 for line in lines:
1365 if not line.startswith('\t'):
1366 sentinel = line
1367 if not sentinel:
1369 StatusUpdate('Append mode disabled: can\'t find sentinel in %r.' %
1370 filename, error_fh)
1371 return None
1372 return sentinel.rstrip('\n')
1373 finally:
1374 fp.close()
1377 class UploadBatcher(object):
1378 """Helper to batch file uploads."""
1380 def __init__(self, what, logging_context):
1381 """Constructor.
1383 Args:
1384 what: Either 'file' or 'blob' or 'errorblob' indicating what kind of
1385 objects this batcher uploads. Used in messages and URLs.
1386 logging_context: The _ClientDeployLoggingContext for this upload.
1388 assert what in ('file', 'blob', 'errorblob'), repr(what)
1389 self.what = what
1390 self.logging_context = logging_context
1391 self.single_url = '/api/appversion/add' + what
1392 self.batch_url = self.single_url + 's'
1393 self.batching = True
1394 self.batch = []
1395 self.batch_size = 0
1397 def SendBatch(self):
1398 """Send the current batch on its way.
1400 If successful, resets self.batch and self.batch_size.
1402 Raises:
1403 HTTPError with code=404 if the server doesn't support batching.
1405 boundary = 'boundary'
1406 parts = []
1407 for path, payload, mime_type in self.batch:
1408 while boundary in payload:
1409 boundary += '%04x' % random.randint(0, 0xffff)
1410 assert len(boundary) < 80, 'Unexpected error, please try again.'
1411 part = '\n'.join(['',
1412 'X-Appcfg-File: %s' % urllib.quote(path),
1413 'X-Appcfg-Hash: %s' % _Hash(payload),
1414 'Content-Type: %s' % mime_type,
1415 'Content-Length: %d' % len(payload),
1416 'Content-Transfer-Encoding: 8bit',
1418 payload,
1420 parts.append(part)
1421 parts.insert(0,
1422 'MIME-Version: 1.0\n'
1423 'Content-Type: multipart/mixed; boundary="%s"\n'
1424 '\n'
1425 'This is a message with multiple parts in MIME format.' %
1426 boundary)
1427 parts.append('--\n')
1428 delimiter = '\n--%s' % boundary
1429 payload = delimiter.join(parts)
1430 logging.info('Uploading batch of %d %ss to %s with boundary="%s".',
1431 len(self.batch), self.what, self.batch_url, boundary)
1432 self.logging_context.Send(self.batch_url,
1433 payload=payload,
1434 content_type='message/rfc822')
1435 self.batch = []
1436 self.batch_size = 0
1438 def SendSingleFile(self, path, payload, mime_type):
1439 """Send a single file on its way."""
1440 logging.info('Uploading %s %s (%s bytes, type=%s) to %s.',
1441 self.what, path, len(payload), mime_type, self.single_url)
1442 self.logging_context.Send(self.single_url,
1443 payload=payload,
1444 content_type=mime_type,
1445 path=path)
1447 def Flush(self):
1448 """Flush the current batch.
1450 This first attempts to send the batch as a single request; if that
1451 fails because the server doesn't support batching, the files are
1452 sent one by one, and self.batching is reset to False.
1454 At the end, self.batch and self.batch_size are reset.
1456 if not self.batch:
1457 return
1458 try:
1459 self.SendBatch()
1460 except urllib2.HTTPError, err:
1461 if err.code != 404:
1462 raise
1465 logging.info('Old server detected; turning off %s batching.', self.what)
1466 self.batching = False
1469 for path, payload, mime_type in self.batch:
1470 self.SendSingleFile(path, payload, mime_type)
1473 self.batch = []
1474 self.batch_size = 0
1476 def AddToBatch(self, path, payload, mime_type):
1477 """Batch a file, possibly flushing first, or perhaps upload it directly.
1479 Args:
1480 path: The name of the file.
1481 payload: The contents of the file.
1482 mime_type: The MIME Content-type of the file, or None.
1484 If mime_type is None, application/octet-stream is substituted.
1486 if not mime_type:
1487 mime_type = 'application/octet-stream'
1488 size = len(payload)
1489 if size <= MAX_BATCH_FILE_SIZE:
1490 if (len(self.batch) >= MAX_BATCH_COUNT or
1491 self.batch_size + size > MAX_BATCH_SIZE):
1492 self.Flush()
1493 if self.batching:
1494 logging.info('Adding %s %s (%s bytes, type=%s) to batch.',
1495 self.what, path, size, mime_type)
1496 self.batch.append((path, payload, mime_type))
1497 self.batch_size += size + BATCH_OVERHEAD
1498 return
1499 self.SendSingleFile(path, payload, mime_type)
1502 def _FormatHash(h):
1503 """Return a string representation of a hash.
1505 The hash is a sha1 hash. It is computed both for files that need to be
1506 pushed to App Engine and for data payloads of requests made to App Engine.
1508 Args:
1509 h: The hash
1511 Returns:
1512 The string representation of the hash.
1514 return '%s_%s_%s_%s_%s' % (h[0:8], h[8:16], h[16:24], h[24:32], h[32:40])
1517 def _Hash(content):
1518 """Compute the sha1 hash of the content.
1520 Args:
1521 content: The data to hash as a string.
1523 Returns:
1524 The string representation of the hash.
1526 h = hashlib.sha1(content).hexdigest()
1527 return _FormatHash(h)
1530 def _HashFromFileHandle(file_handle):
1531 """Compute the hash of the content of the file pointed to by file_handle.
1533 Args:
1534 file_handle: File-like object which provides seek, read and tell.
1536 Returns:
1537 The string representation of the hash.
1546 pos = file_handle.tell()
1547 content_hash = _Hash(file_handle.read())
1548 file_handle.seek(pos, 0)
1549 return content_hash
1552 def EnsureDir(path):
1553 """Makes sure that a directory exists at the given path.
1555 If a directory already exists at that path, nothing is done.
1556 Otherwise, try to create a directory at that path with os.makedirs.
1557 If that fails, propagate the resulting OSError exception.
1559 Args:
1560 path: The path that you want to refer to a directory.
1562 try:
1563 os.makedirs(path)
1564 except OSError, exc:
1567 if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
1568 raise
1571 def DoDownloadApp(rpcserver, out_dir, app_id, module, app_version,
1572 error_fh=sys.stderr):
1573 """Downloads the files associated with a particular app version.
1575 Args:
1576 rpcserver: The RPC server to use to download.
1577 out_dir: The directory the files should be downloaded to.
1578 app_id: The app ID of the app whose files we want to download.
1579 module: The module we want to download from. Can be:
1580 - None: We'll download from the default module.
1581 - <module>: We'll download from the specified module.
1582 app_version: The version number we want to download. Can be:
1583 - None: We'll download the latest default version.
1584 - <major>: We'll download the latest minor version.
1585 - <major>/<minor>: We'll download that exact version.
1586 error_fh: Where to send status and error messages.
1589 StatusUpdate('Fetching file list...', error_fh)
1591 url_args = {'app_id': app_id}
1592 if module:
1593 url_args['module'] = module
1594 if app_version is not None:
1595 url_args['version_match'] = app_version
1597 result = rpcserver.Send('/api/files/list', **url_args)
1599 StatusUpdate('Fetching files...', error_fh)
1601 lines = result.splitlines()
1603 if len(lines) < 1:
1604 logging.error('Invalid response from server: empty')
1605 return
1607 full_version = lines[0]
1608 file_lines = lines[1:]
1610 current_file_number = 0
1611 num_files = len(file_lines)
1613 num_errors = 0
1615 for line in file_lines:
1616 parts = line.split('|', 2)
1617 if len(parts) != 3:
1618 logging.error('Invalid response from server: expecting '
1619 '"<id>|<size>|<path>", found: "%s"\n', line)
1620 return
1622 current_file_number += 1
1624 file_id, size_str, path = parts
1625 try:
1626 size = int(size_str)
1627 except ValueError:
1628 logging.error('Invalid file list entry from server: invalid size: '
1629 '"%s"', size_str)
1630 return
1632 StatusUpdate('[%d/%d] %s' % (current_file_number, num_files, path),
1633 error_fh)
1635 def TryGet():
1636 """A request to /api/files/get which works with the RetryWithBackoff."""
1637 try:
1638 contents = rpcserver.Send('/api/files/get', app_id=app_id,
1639 version=full_version, id=file_id)
1640 return True, contents
1641 except urllib2.HTTPError, exc:
1644 if exc.code == 503:
1645 return False, exc
1646 else:
1647 raise
1649 def PrintRetryMessage(_, delay):
1650 StatusUpdate('Server busy. Will try again in %d seconds.' % delay,
1651 error_fh)
1653 success, contents = RetryWithBackoff(TryGet, PrintRetryMessage)
1654 if not success:
1655 logging.error('Unable to download file "%s".', path)
1656 num_errors += 1
1657 continue
1659 if len(contents) != size:
1660 logging.error('File "%s": server listed as %d bytes but served '
1661 '%d bytes.', path, size, len(contents))
1662 num_errors += 1
1664 full_path = os.path.join(out_dir, path)
1666 if os.path.exists(full_path):
1667 logging.error('Unable to create file "%s": path conflicts with '
1668 'an existing file or directory', path)
1669 num_errors += 1
1670 continue
1672 full_dir = os.path.dirname(full_path)
1673 try:
1674 EnsureDir(full_dir)
1675 except OSError, exc:
1676 logging.error('Couldn\'t create directory "%s": %s', full_dir, exc)
1677 num_errors += 1
1678 continue
1680 try:
1681 out_file = open(full_path, 'wb')
1682 except IOError, exc:
1683 logging.error('Couldn\'t open file "%s": %s', full_path, exc)
1684 num_errors += 1
1685 continue
1687 try:
1688 try:
1689 out_file.write(contents)
1690 except IOError, exc:
1691 logging.error('Couldn\'t write to file "%s": %s', full_path, exc)
1692 num_errors += 1
1693 continue
1694 finally:
1695 out_file.close()
1697 if num_errors > 0:
1698 logging.error('Number of errors: %d. See output for details.', num_errors)
1701 class _ClientDeployLoggingContext(object):
1702 """Context for sending and recording server rpc requests.
1704 Attributes:
1705 rpcserver: The AbstractRpcServer to use for the upload.
1706 requests: A list of client_deployinfo.Request objects to include
1707 with the client deploy log.
1708 time_func: Function to get the current time in milliseconds.
1709 request_params: A dictionary with params to append to requests
1712 def __init__(self,
1713 rpcserver,
1714 request_params,
1715 usage_reporting,
1716 time_func=time.time):
1717 """Creates a new AppVersionUpload.
1719 Args:
1720 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1721 or TestRpcServer.
1722 request_params: A dictionary with params to append to requests
1723 usage_reporting: Whether to actually upload data.
1724 time_func: Function to return the current time in millisecods
1725 (default time.time).
1727 self.rpcserver = rpcserver
1728 self.request_params = request_params
1729 self.usage_reporting = usage_reporting
1730 self.time_func = time_func
1731 self.requests = []
1733 def Send(self, url, payload='', **kwargs):
1734 """Sends a request to the server, with common params."""
1735 start_time_usec = self.GetCurrentTimeUsec()
1736 request_size_bytes = len(payload)
1737 try:
1738 logging.info('Send: %s, params=%s', url, self.request_params)
1740 kwargs.update(self.request_params)
1741 result = self.rpcserver.Send(url, payload=payload, **kwargs)
1742 self._RegisterReqestForLogging(url, 200, start_time_usec,
1743 request_size_bytes)
1744 return result
1745 except urllib2.HTTPError, e:
1746 self._RegisterReqestForLogging(url, e.code, start_time_usec,
1747 request_size_bytes)
1748 raise e
1750 def GetCurrentTimeUsec(self):
1751 """Returns the current time in microseconds."""
1752 return int(round(self.time_func() * 1000 * 1000))
1754 def GetSdkVersion(self):
1755 """Returns the current SDK Version."""
1756 sdk_version = sdk_update_checker.GetVersionObject()
1757 return sdk_version.get('release', '?') if sdk_version else '?'
1759 def _RegisterReqestForLogging(self, path, response_code, start_time_usec,
1760 request_size_bytes):
1761 """Registers a request for client deploy logging purposes."""
1762 end_time_usec = self.GetCurrentTimeUsec()
1763 self.requests.append(client_deployinfo.Request(
1764 path=path,
1765 response_code=response_code,
1766 start_time_usec=start_time_usec,
1767 end_time_usec=end_time_usec,
1768 request_size_bytes=request_size_bytes))
1770 def LogClientDeploy(self, runtime, start_time_usec, success):
1771 """Logs a client deployment attempt.
1773 Args:
1774 runtime: The runtime for the app being deployed.
1775 start_time_usec: The start time of the deployment in micro seconds.
1776 success: True if the deployment succeeded otherwise False.
1778 if not self.usage_reporting:
1779 logging.info('Skipping usage reporting.')
1780 return
1781 end_time_usec = self.GetCurrentTimeUsec()
1782 try:
1783 info = client_deployinfo.ClientDeployInfoExternal(
1784 runtime=runtime,
1785 start_time_usec=start_time_usec,
1786 end_time_usec=end_time_usec,
1787 requests=self.requests,
1788 success=success,
1789 sdk_version=self.GetSdkVersion())
1790 self.Send('/api/logclientdeploy', info.ToYAML())
1791 except BaseException, e:
1792 logging.debug('Exception logging deploy info continuing - %s', e)
1795 class EndpointsState(object):
1796 SERVING = 'serving'
1797 PENDING = 'pending'
1798 FAILED = 'failed'
1799 _STATES = frozenset((SERVING, PENDING, FAILED))
1801 @classmethod
1802 def Parse(cls, value):
1803 state = value.lower()
1804 if state not in cls._STATES:
1805 lst = sorted(cls._STATES)
1806 pretty_states = ', '.join(lst[:-1]) + ', or ' + lst[-1]
1807 raise ValueError('Unexpected Endpoints state "%s"; should be %s.' %
1808 (value, pretty_states))
1809 return state
1812 class AppVersionUpload(object):
1813 """Provides facilities to upload a new appversion to the hosting service.
1815 Attributes:
1816 rpcserver: The AbstractRpcServer to use for the upload.
1817 config: The AppInfoExternal object derived from the app.yaml file.
1818 app_id: The application string from 'config'.
1819 version: The version string from 'config'.
1820 backend: The backend to update, if any.
1821 files: A dictionary of files to upload to the rpcserver, mapping path to
1822 hash of the file contents.
1823 in_transaction: True iff a transaction with the server has started.
1824 An AppVersionUpload can do only one transaction at a time.
1825 deployed: True iff the Deploy method has been called.
1826 started: True iff the StartServing method has been called.
1827 logging_context: The _ClientDeployLoggingContext for this upload.
1830 def __init__(self, rpcserver, config, module_yaml_path='app.yaml',
1831 backend=None,
1832 error_fh=None,
1833 get_version=sdk_update_checker.GetVersionObject,
1834 usage_reporting=False):
1835 """Creates a new AppVersionUpload.
1837 Args:
1838 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1839 or TestRpcServer.
1840 config: An AppInfoExternal object that specifies the configuration for
1841 this application.
1842 module_yaml_path: The (string) path to the yaml file corresponding to
1843 <config>, relative to the bundle directory.
1844 backend: If specified, indicates the update applies to the given backend.
1845 The backend name must match an entry in the backends: stanza.
1846 error_fh: Unexpected HTTPErrors are printed to this file handle.
1847 get_version: Method for determining the current SDK version. The override
1848 is used for testing.
1849 usage_reporting: Whether or not to report usage.
1851 self.rpcserver = rpcserver
1852 self.config = config
1853 self.app_id = self.config.application
1854 self.module = self.config.module
1855 self.backend = backend
1856 self.error_fh = error_fh or sys.stderr
1858 self.version = self.config.version
1860 self.params = {}
1861 if self.app_id:
1862 self.params['app_id'] = self.app_id
1863 if self.module:
1864 self.params['module'] = self.module
1865 if self.backend:
1866 self.params['backend'] = self.backend
1867 elif self.version:
1868 self.params['version'] = self.version
1873 self.files = {}
1876 self.all_files = set()
1878 self.in_transaction = False
1879 self.deployed = False
1880 self.started = False
1881 self.batching = True
1882 self.logging_context = _ClientDeployLoggingContext(rpcserver,
1883 self.params,
1884 usage_reporting)
1885 self.file_batcher = UploadBatcher('file', self.logging_context)
1886 self.blob_batcher = UploadBatcher('blob', self.logging_context)
1887 self.errorblob_batcher = UploadBatcher('errorblob', self.logging_context)
1889 if not self.config.vm_settings:
1890 self.config.vm_settings = appinfo.VmSettings()
1891 self.config.vm_settings['module_yaml_path'] = module_yaml_path
1893 if not self.config.vm_settings.get('image'):
1894 sdk_version = get_version()
1895 if sdk_version and sdk_version.get('release'):
1896 self.config.vm_settings['image'] = sdk_version['release']
1898 if not self.config.auto_id_policy:
1899 self.config.auto_id_policy = appinfo.DATASTORE_ID_POLICY_DEFAULT
1901 def AddFile(self, path, file_handle):
1902 """Adds the provided file to the list to be pushed to the server.
1904 Args:
1905 path: The path the file should be uploaded as.
1906 file_handle: A stream containing data to upload.
1908 assert not self.in_transaction, 'Already in a transaction.'
1909 assert file_handle is not None
1911 reason = appinfo.ValidFilename(path)
1912 if reason:
1913 logging.error(reason)
1914 return
1916 content_hash = _HashFromFileHandle(file_handle)
1918 self.files[path] = content_hash
1919 self.all_files.add(path)
1921 def Describe(self):
1922 """Returns a string describing the object being updated."""
1923 result = 'app: %s' % self.app_id
1924 if self.module is not None and self.module != appinfo.DEFAULT_MODULE:
1925 result += ', module: %s' % self.module
1926 if self.backend:
1927 result += ', backend: %s' % self.backend
1928 elif self.version:
1929 result += ', version: %s' % self.version
1930 return result
1932 @staticmethod
1933 def _ValidateBeginYaml(resp):
1934 """Validates the given /api/appversion/create response string."""
1935 response_dict = yaml.safe_load(resp)
1936 if not response_dict or 'warnings' not in response_dict:
1937 return False
1938 return response_dict
1940 def Begin(self):
1941 """Begins the transaction, returning a list of files that need uploading.
1943 All calls to AddFile must be made before calling Begin().
1945 Returns:
1946 A list of pathnames for files that should be uploaded using UploadFile()
1947 before Commit() can be called.
1949 assert not self.in_transaction, 'Already in a transaction.'
1954 config_copy = copy.deepcopy(self.config)
1955 for url in config_copy.handlers:
1956 handler_type = url.GetHandlerType()
1957 if url.application_readable:
1960 if handler_type == 'static_dir':
1961 url.static_dir = '%s/%s' % (STATIC_FILE_PREFIX, url.static_dir)
1962 elif handler_type == 'static_files':
1963 url.static_files = '%s/%s' % (STATIC_FILE_PREFIX, url.static_files)
1964 url.upload = '%s/%s' % (STATIC_FILE_PREFIX, url.upload)
1966 response = self.logging_context.Send(
1967 '/api/appversion/create',
1968 payload=config_copy.ToYAML())
1970 result = self._ValidateBeginYaml(response)
1971 if result:
1972 warnings = result.get('warnings')
1973 for warning in warnings:
1974 StatusUpdate('WARNING: %s' % warning, self.error_fh)
1976 self.in_transaction = True
1978 files_to_clone = []
1979 blobs_to_clone = []
1980 errorblobs = {}
1981 for path, content_hash in self.files.iteritems():
1982 file_classification = FileClassification(
1983 self.config, path, error_fh=self.error_fh)
1985 if file_classification.IsStaticFile():
1986 upload_path = path
1987 if file_classification.IsApplicationFile():
1988 upload_path = '%s/%s' % (STATIC_FILE_PREFIX, path)
1989 blobs_to_clone.append((path, upload_path, content_hash,
1990 file_classification.StaticMimeType()))
1994 if file_classification.IsErrorFile():
1998 errorblobs[path] = content_hash
2000 if file_classification.IsApplicationFile():
2001 files_to_clone.append((path, path, content_hash))
2003 files_to_upload = {}
2005 def CloneFiles(url, files, file_type):
2006 """Sends files to the given url.
2008 Args:
2009 url: the server URL to use.
2010 files: a list of files
2011 file_type: the type of the files
2013 if not files:
2014 return
2016 StatusUpdate('Cloning %d %s file%s.' %
2017 (len(files), file_type, len(files) != 1 and 's' or ''),
2018 self.error_fh)
2020 max_files = self.resource_limits['max_files_to_clone']
2021 for i in xrange(0, len(files), max_files):
2022 if i > 0 and i % max_files == 0:
2023 StatusUpdate('Cloned %d files.' % i, self.error_fh)
2025 chunk = files[i:min(len(files), i + max_files)]
2026 result = self.logging_context.Send(url,
2027 payload=BuildClonePostBody(chunk))
2028 if result:
2029 to_upload = {}
2030 for f in result.split(LIST_DELIMITER):
2031 for entry in files:
2032 real_path, upload_path = entry[:2]
2033 if f == upload_path:
2034 to_upload[real_path] = self.files[real_path]
2035 break
2036 files_to_upload.update(to_upload)
2038 CloneFiles('/api/appversion/cloneblobs', blobs_to_clone, 'static')
2039 CloneFiles('/api/appversion/clonefiles', files_to_clone, 'application')
2041 logging.debug('Files to upload: %s', files_to_upload)
2043 for (path, content_hash) in errorblobs.iteritems():
2044 files_to_upload[path] = content_hash
2045 self.files = files_to_upload
2046 return sorted(files_to_upload.iterkeys())
2048 def UploadFile(self, path, file_handle):
2049 """Uploads a file to the hosting service.
2051 Must only be called after Begin().
2052 The path provided must be one of those that were returned by Begin().
2054 Args:
2055 path: The path the file is being uploaded as.
2056 file_handle: A file-like object containing the data to upload.
2058 Raises:
2059 KeyError: The provided file is not amongst those to be uploaded.
2061 assert self.in_transaction, 'Begin() must be called before UploadFile().'
2062 if path not in self.files:
2063 raise KeyError('File \'%s\' is not in the list of files to be uploaded.'
2064 % path)
2066 del self.files[path]
2068 file_classification = FileClassification(
2069 self.config, path, error_fh=self.error_fh)
2070 payload = file_handle.read()
2071 if file_classification.IsStaticFile():
2072 upload_path = path
2073 if file_classification.IsApplicationFile():
2074 upload_path = '%s/%s' % (STATIC_FILE_PREFIX, path)
2075 self.blob_batcher.AddToBatch(upload_path, payload,
2076 file_classification.StaticMimeType())
2080 if file_classification.IsErrorFile():
2083 self.errorblob_batcher.AddToBatch(file_classification.ErrorCode(),
2084 payload,
2085 file_classification.ErrorMimeType())
2087 if file_classification.IsApplicationFile():
2089 self.file_batcher.AddToBatch(path, payload, None)
2091 def Precompile(self):
2092 """Handle precompilation."""
2094 StatusUpdate('Compilation starting.', self.error_fh)
2096 files = []
2097 if self.config.GetEffectiveRuntime() == 'go':
2100 for f in self.all_files:
2101 if f.endswith('.go') and not self.config.nobuild_files.match(f):
2102 files.append(f)
2104 while True:
2105 if files:
2106 StatusUpdate('Compilation: %d files left.' % len(files), self.error_fh)
2107 files = self.PrecompileBatch(files)
2108 if not files:
2109 break
2110 StatusUpdate('Compilation completed.', self.error_fh)
2112 def PrecompileBatch(self, files):
2113 """Precompile a batch of files.
2115 Args:
2116 files: Either an empty list (for the initial request) or a list
2117 of files to be precompiled.
2119 Returns:
2120 Either an empty list (if no more files need to be precompiled)
2121 or a list of files to be precompiled subsequently.
2123 payload = LIST_DELIMITER.join(files)
2124 response = self.logging_context.Send('/api/appversion/precompile',
2125 payload=payload)
2126 if not response:
2127 return []
2128 return response.split(LIST_DELIMITER)
2130 def Commit(self):
2131 """Commits the transaction, making the new app version available.
2133 All the files returned by Begin() must have been uploaded with UploadFile()
2134 before Commit() can be called.
2136 This tries the new 'deploy' method; if that fails it uses the old 'commit'.
2138 Returns:
2139 An appinfo.AppInfoSummary if one was returned from the Deploy, None
2140 otherwise.
2142 Raises:
2143 RuntimeError: Some required files were not uploaded.
2144 CannotStartServingError: Another operation is in progress on this version.
2146 assert self.in_transaction, 'Begin() must be called before Commit().'
2147 if self.files:
2148 raise RuntimeError('Not all required files have been uploaded.')
2150 def PrintRetryMessage(_, delay):
2151 StatusUpdate('Will check again in %s seconds.' % delay, self.error_fh)
2153 app_summary = self.Deploy()
2156 success, unused_contents = RetryWithBackoff(
2157 lambda: (self.IsReady(), None), PrintRetryMessage, 1, 2, 60, 20)
2158 if not success:
2160 logging.warning('Version still not ready to serve, aborting.')
2161 raise RuntimeError('Version not ready.')
2163 result = self.StartServing()
2164 if not result:
2167 self.in_transaction = False
2168 else:
2169 if result == '0':
2170 raise CannotStartServingError(
2171 'Another operation on this version is in progress.')
2172 success, response = RetryNoBackoff(self.IsServing, PrintRetryMessage)
2173 if not success:
2175 logging.warning('Version still not serving, aborting.')
2176 raise RuntimeError('Version not ready.')
2180 check_config_updated = response.get('check_endpoints_config')
2181 if check_config_updated:
2182 unused_done, last_state = RetryWithBackoff(
2183 self.IsEndpointsConfigUpdated,
2184 PrintRetryMessage, 1, 2, 60, 20)
2185 if last_state != EndpointsState.SERVING:
2186 error_message = (
2187 'Failed to update Endpoints configuration (last result %s). '
2188 'Check the app\'s AppEngine logs for errors: %s' %
2189 (last_state, self.GetLogUrl()))
2190 StatusUpdate(error_message, self.error_fh)
2191 logging.warning(error_message)
2192 raise RuntimeError(error_message)
2193 self.in_transaction = False
2195 return app_summary
2197 def Deploy(self):
2198 """Deploys the new app version but does not make it default.
2200 All the files returned by Begin() must have been uploaded with UploadFile()
2201 before Deploy() can be called.
2203 Returns:
2204 An appinfo.AppInfoSummary if one was returned from the Deploy, None
2205 otherwise.
2207 Raises:
2208 RuntimeError: Some required files were not uploaded.
2210 assert self.in_transaction, 'Begin() must be called before Deploy().'
2211 if self.files:
2212 raise RuntimeError('Not all required files have been uploaded.')
2214 StatusUpdate('Starting deployment.', self.error_fh)
2215 result = self.logging_context.Send('/api/appversion/deploy')
2216 self.deployed = True
2218 if result:
2219 return yaml_object.BuildSingleObject(appinfo.AppInfoSummary, result)
2220 else:
2221 return None
2223 def IsReady(self):
2224 """Check if the new app version is ready to serve traffic.
2226 Raises:
2227 RuntimeError: Deploy has not yet been called.
2229 Returns:
2230 True if the server returned the app is ready to serve.
2232 assert self.deployed, 'Deploy() must be called before IsReady().'
2234 StatusUpdate('Checking if deployment succeeded.', self.error_fh)
2235 result = self.logging_context.Send('/api/appversion/isready')
2236 return result == '1'
2238 def StartServing(self):
2239 """Start serving with the newly created version.
2241 Raises:
2242 RuntimeError: Deploy has not yet been called.
2244 Returns:
2245 The response body, as a string.
2247 assert self.deployed, 'Deploy() must be called before StartServing().'
2249 StatusUpdate('Deployment successful.', self.error_fh)
2250 self.params['willcheckserving'] = '1'
2251 result = self.logging_context.Send('/api/appversion/startserving')
2252 del self.params['willcheckserving']
2253 self.started = True
2254 return result
2256 @staticmethod
2257 def _ValidateIsServingYaml(resp):
2258 """Validates the given /isserving YAML string.
2260 Args:
2261 resp: the response from an RPC to a URL such as /api/appversion/isserving.
2263 Returns:
2264 The resulting dictionary if the response is valid, or None otherwise.
2266 response_dict = yaml.safe_load(resp)
2267 if 'serving' not in response_dict:
2268 return None
2269 return response_dict
2271 def IsServing(self):
2272 """Check if the new app version is serving.
2274 Raises:
2275 RuntimeError: Deploy has not yet been called.
2276 CannotStartServingError: A bad response was received from the isserving
2277 API call.
2279 Returns:
2280 (serving, response) Where serving is True if the deployed app version is
2281 serving, False otherwise. response is a dict containing the parsed
2282 response from the server, or an empty dict if the server's response was
2283 an old style 0/1 response.
2285 assert self.started, 'StartServing() must be called before IsServing().'
2287 StatusUpdate('Checking if updated app version is serving.', self.error_fh)
2289 self.params['new_serving_resp'] = '1'
2290 result = self.logging_context.Send('/api/appversion/isserving')
2291 del self.params['new_serving_resp']
2292 if result in ['0', '1']:
2293 return result == '1', {}
2294 result = AppVersionUpload._ValidateIsServingYaml(result)
2295 if not result:
2296 raise CannotStartServingError(
2297 'Internal error: Could not parse IsServing response.')
2298 message = result.get('message')
2299 fatal = result.get('fatal')
2300 if message:
2301 StatusUpdate(message, self.error_fh)
2302 if fatal:
2303 raise CannotStartServingError(message or 'Unknown error.')
2304 return result['serving'], result
2306 @staticmethod
2307 def _ValidateIsEndpointsConfigUpdatedYaml(resp):
2308 """Validates the YAML string response from an isconfigupdated request.
2310 Args:
2311 resp: A string containing the response from the server.
2313 Returns:
2314 The dictionary with the parsed response if the response is valid.
2315 Otherwise returns False.
2317 response_dict = yaml.safe_load(resp)
2319 if 'updated' not in response_dict and 'updatedDetail' not in response_dict:
2320 return None
2321 return response_dict
2323 def GetLogUrl(self):
2324 """Get the URL for the app's logs."""
2325 module = '%s:' % self.module if self.module else ''
2326 return ('https://appengine.google.com/logs?' +
2327 urllib.urlencode((('app_id', self.app_id),
2328 ('version_id', module + self.version))))
2330 def IsEndpointsConfigUpdated(self):
2331 """Check if the Endpoints configuration for this app has been updated.
2333 This should only be called if the app has a Google Cloud Endpoints
2334 handler, or if it's removing one. The server performs the check to see
2335 if Endpoints support is added/updated/removed, and the response to the
2336 isserving call indicates whether IsEndpointsConfigUpdated should be called.
2338 Raises:
2339 AssertionError: Deploy has not yet been called.
2340 CannotStartServingError: There was an unexpected error with the server
2341 response.
2343 Returns:
2344 (done, updated_state), where done is False if this function should
2345 be called again to retry, True if not. updated_state is an
2346 EndpointsState value indicating whether the Endpoints configuration has
2347 been updated on the server.
2350 assert self.started, ('StartServing() must be called before '
2351 'IsEndpointsConfigUpdated().')
2353 StatusUpdate('Checking if Endpoints configuration has been updated.',
2354 self.error_fh)
2356 result = self.logging_context.Send('/api/isconfigupdated')
2357 result = AppVersionUpload._ValidateIsEndpointsConfigUpdatedYaml(result)
2358 if result is None:
2359 raise CannotStartServingError(
2360 'Internal error: Could not parse IsEndpointsConfigUpdated response.')
2361 if 'updatedDetail' in result:
2362 updated_state = EndpointsState.Parse(result['updatedDetail'])
2363 else:
2369 updated_state = (EndpointsState.SERVING if result['updated']
2370 else EndpointsState.PENDING)
2371 return updated_state != EndpointsState.PENDING, updated_state
2373 def Rollback(self, force_rollback=False):
2374 """Rolls back the transaction if one is in progress."""
2375 if not self.in_transaction:
2376 return
2377 msg = 'Rolling back the update.'
2378 if self.config.vm and not force_rollback:
2379 msg += (' This can sometimes take a while since a VM version is being '
2380 'rolled back.')
2381 StatusUpdate(msg, self.error_fh)
2382 self.logging_context.Send('/api/appversion/rollback',
2383 force_rollback='1' if force_rollback else '0')
2384 self.in_transaction = False
2385 self.files = {}
2387 def DoUpload(self, paths, openfunc):
2388 """Uploads a new appversion with the given config and files to the server.
2390 Args:
2391 paths: An iterator that yields the relative paths of the files to upload.
2392 openfunc: A function that takes a path and returns a file-like object.
2394 Returns:
2395 An appinfo.AppInfoSummary if one was returned from the server, None
2396 otherwise.
2398 start_time_usec = self.logging_context.GetCurrentTimeUsec()
2399 logging.info('Reading app configuration.')
2401 StatusUpdate('\nStarting update of %s' % self.Describe(), self.error_fh)
2404 path = ''
2405 try:
2406 self.resource_limits = GetResourceLimits(self.logging_context,
2407 self.error_fh)
2408 self._AddFilesThatAreSmallEnough(paths, openfunc)
2409 except KeyboardInterrupt:
2410 logging.info('User interrupted. Aborting.')
2411 raise
2412 except EnvironmentError, e:
2413 if self._IsExceptionClientDeployLoggable(e):
2414 self.logging_context.LogClientDeploy(self.config.runtime,
2415 start_time_usec, False)
2416 logging.error('An error occurred processing file \'%s\': %s. Aborting.',
2417 path, e)
2418 raise
2420 try:
2421 missing_files = self.Begin()
2422 self._UploadMissingFiles(missing_files, openfunc)
2425 if (self.config.derived_file_type and
2426 appinfo.PYTHON_PRECOMPILED in self.config.derived_file_type):
2427 try:
2428 self.Precompile()
2429 except urllib2.HTTPError, e:
2430 ErrorUpdate('Error %d: --- begin server output ---\n'
2431 '%s\n--- end server output ---' %
2432 (e.code, e.read().rstrip('\n')))
2433 if e.code == 422 or self.config.GetEffectiveRuntime() == 'go':
2440 raise
2441 print >>self.error_fh, (
2442 'Precompilation failed. Your app can still serve but may '
2443 'have reduced startup performance. You can retry the update '
2444 'later to retry the precompilation step.')
2447 app_summary = self.Commit()
2448 StatusUpdate('Completed update of %s' % self.Describe(), self.error_fh)
2449 self.logging_context.LogClientDeploy(self.config.runtime, start_time_usec,
2450 True)
2451 except BaseException, e:
2452 try:
2453 self._LogDoUploadException(e)
2454 self.Rollback()
2455 finally:
2456 if self._IsExceptionClientDeployLoggable(e):
2457 self.logging_context.LogClientDeploy(self.config.runtime,
2458 start_time_usec, False)
2460 raise
2462 logging.info('Done!')
2463 return app_summary
2465 def _IsExceptionClientDeployLoggable(self, exception):
2466 """Determines if an exception qualifes for client deploy log reistration.
2468 Args:
2469 exception: The exception to check.
2471 Returns:
2472 True iff exception qualifies for client deploy logging - basically a
2473 system error rather than a user or error or cancellation.
2476 if isinstance(exception, KeyboardInterrupt):
2477 return False
2479 if (isinstance(exception, urllib2.HTTPError)
2480 and 400 <= exception.code <= 499):
2481 return False
2483 return True
2485 def _AddFilesThatAreSmallEnough(self, paths, openfunc):
2486 """Calls self.AddFile on files that are small enough.
2488 By small enough, we mean that their size is within
2489 self.resource_limits['max_file_size'] for application files, and
2490 'max_blob_size' otherwise. Files that are too large are logged as errors,
2491 and dropped (not sure why this isn't handled by raising an exception...).
2493 Args:
2494 paths: List of paths, relative to the app's base path.
2495 openfunc: A function that takes a paths element, and returns a file-like
2496 object.
2498 StatusUpdate('Scanning files on local disk.', self.error_fh)
2499 num_files = 0
2500 for path in paths:
2501 file_handle = openfunc(path)
2502 try:
2503 file_length = GetFileLength(file_handle)
2506 file_classification = FileClassification(
2507 self.config, path, self.error_fh)
2508 if file_classification.IsApplicationFile():
2509 max_size = self.resource_limits['max_file_size']
2510 else:
2511 max_size = self.resource_limits['max_blob_size']
2514 if file_length > max_size:
2515 extra_msg = (' Consider --enable_jar_splitting.'
2516 if JavaSupported() and path.endswith('jar')
2517 else '')
2518 logging.error('Ignoring file \'%s\': Too long '
2519 '(max %d bytes, file is %d bytes).%s',
2520 path, max_size, file_length, extra_msg)
2521 else:
2522 logging.info('Processing file \'%s\'', path)
2523 self.AddFile(path, file_handle)
2524 finally:
2525 file_handle.close()
2528 num_files += 1
2529 if num_files % 500 == 0:
2530 StatusUpdate('Scanned %d files.' % num_files, self.error_fh)
2532 def _UploadMissingFiles(self, missing_files, openfunc):
2533 """DoUpload helper to upload files that need to be uploaded.
2535 Args:
2536 missing_files: List of files that need to be uploaded. Begin returns such
2537 a list. Design note: we don't call Begin here, because we want DoUpload
2538 to call it directly so that Begin/Commit are more clearly paired.
2539 openfunc: Function that takes a path relative to the app's base path, and
2540 returns a file-like object.
2542 if not missing_files:
2543 return
2545 StatusUpdate('Uploading %d files and blobs.' % len(missing_files),
2546 self.error_fh)
2547 num_files = 0
2548 for missing_file in missing_files:
2549 file_handle = openfunc(missing_file)
2550 try:
2551 self.UploadFile(missing_file, file_handle)
2552 finally:
2553 file_handle.close()
2556 num_files += 1
2557 if num_files % 500 == 0:
2558 StatusUpdate('Processed %d out of %s.' %
2559 (num_files, len(missing_files)), self.error_fh)
2562 self.file_batcher.Flush()
2563 self.blob_batcher.Flush()
2564 self.errorblob_batcher.Flush()
2565 StatusUpdate('Uploaded %d files and blobs' % num_files, self.error_fh)
2567 @staticmethod
2568 def _LogDoUploadException(exception):
2569 """Helper that logs exceptions that occurred during DoUpload.
2571 Args:
2572 exception: An exception that was thrown during DoUpload.
2574 def InstanceOf(tipe):
2575 return isinstance(exception, tipe)
2577 if InstanceOf(KeyboardInterrupt):
2578 logging.info('User interrupted. Aborting.')
2579 elif InstanceOf(urllib2.HTTPError):
2580 logging.info('HTTP Error (%s)', exception)
2581 elif InstanceOf(CannotStartServingError):
2582 logging.error(exception.message)
2583 else:
2584 logging.exception('An unexpected error occurred. Aborting.')
2587 class DoLockAction(object):
2588 """Locks/unlocks a particular vm app version and shows state."""
2590 def __init__(
2591 self, url, rpcserver, app_id, version, module, instance, file_handle):
2592 self.url = url
2593 self.rpcserver = rpcserver
2594 self.app_id = app_id
2595 self.version = version
2596 self.module = module
2597 self.instance = instance
2598 self.file_handle = file_handle
2600 def GetState(self):
2601 yaml_data = self.rpcserver.Send('/api/vms/debugstate',
2602 app_id=self.app_id,
2603 version_match=self.version,
2604 module=self.module)
2605 state = yaml.safe_load(yaml_data)
2606 done = state['state'] != 'PENDING'
2607 if done:
2608 print >> self.file_handle, state['message']
2609 return (done, state['message'])
2611 def PrintRetryMessage(self, msg, delay):
2612 StatusUpdate('%s. Will try again in %d seconds.' % (msg, delay),
2613 self.file_handle)
2615 def Do(self):
2616 kwargs = {'app_id': self.app_id,
2617 'version_match': self.version,
2618 'module': self.module}
2619 if self.instance:
2620 kwargs['instance'] = self.instance
2622 response = self.rpcserver.Send(self.url, **kwargs)
2623 print >> self.file_handle, response
2624 RetryWithBackoff(self.GetState, self.PrintRetryMessage, 1, 2, 5, 20)
2627 def FileIterator(base, skip_files, runtime, separator=os.path.sep):
2628 """Walks a directory tree, returning all the files. Follows symlinks.
2630 Args:
2631 base: The base path to search for files under.
2632 skip_files: A regular expression object for files/directories to skip.
2633 runtime: The name of the runtime e.g. "python". If "python27" then .pyc
2634 files with matching .py files will be skipped.
2635 separator: Path separator used by the running system's platform.
2637 Yields:
2638 Paths of files found, relative to base.
2640 dirs = ['']
2641 while dirs:
2642 current_dir = dirs.pop()
2643 entries = set(os.listdir(os.path.join(base, current_dir)))
2644 for entry in sorted(entries):
2645 name = os.path.join(current_dir, entry)
2646 fullname = os.path.join(base, name)
2651 if separator == '\\':
2652 name = name.replace('\\', '/')
2654 if runtime == 'python27' and not skip_files.match(name):
2655 root, extension = os.path.splitext(entry)
2656 if extension == '.pyc' and (root + '.py') in entries:
2657 logging.warning('Ignoring file \'%s\': Cannot upload both '
2658 '<filename>.py and <filename>.pyc', name)
2659 continue
2661 if os.path.isfile(fullname):
2662 if skip_files.match(name):
2663 logging.info('Ignoring file \'%s\': File matches ignore regex.', name)
2664 else:
2665 yield name
2666 elif os.path.isdir(fullname):
2667 if skip_files.match(name):
2668 logging.info(
2669 'Ignoring directory \'%s\': Directory matches ignore regex.',
2670 name)
2671 else:
2672 dirs.append(name)
2675 def GetFileLength(fh):
2676 """Returns the length of the file represented by fh.
2678 This function is capable of finding the length of any seekable stream,
2679 unlike os.fstat, which only works on file streams.
2681 Args:
2682 fh: The stream to get the length of.
2684 Returns:
2685 The length of the stream.
2687 pos = fh.tell()
2689 fh.seek(0, 2)
2690 length = fh.tell()
2691 fh.seek(pos, 0)
2692 return length
2695 def GetUserAgent(get_version=sdk_update_checker.GetVersionObject,
2696 get_platform=appengine_rpc.GetPlatformToken,
2697 sdk_product=SDK_PRODUCT):
2698 """Determines the value of the 'User-agent' header to use for HTTP requests.
2700 If the 'APPCFG_SDK_NAME' environment variable is present, that will be
2701 used as the first product token in the user-agent.
2703 Args:
2704 get_version: Used for testing.
2705 get_platform: Used for testing.
2706 sdk_product: Used as part of sdk/version product token.
2708 Returns:
2709 String containing the 'user-agent' header value, which includes the SDK
2710 version, the platform information, and the version of Python;
2711 e.g., 'appcfg_py/1.0.1 Darwin/9.2.0 Python/2.5.2'.
2713 product_tokens = []
2716 sdk_name = os.environ.get('APPCFG_SDK_NAME')
2717 if sdk_name:
2718 product_tokens.append(sdk_name)
2719 else:
2720 version = get_version()
2721 if version is None:
2722 release = 'unknown'
2723 else:
2724 release = version['release']
2726 product_tokens.append('%s/%s' % (sdk_product, release))
2729 product_tokens.append(get_platform())
2732 python_version = '.'.join(str(i) for i in sys.version_info)
2733 product_tokens.append('Python/%s' % python_version)
2735 return ' '.join(product_tokens)
2738 def GetSourceName(get_version=sdk_update_checker.GetVersionObject):
2739 """Gets the name of this source version."""
2740 version = get_version()
2741 if version is None:
2742 release = 'unknown'
2743 else:
2744 release = version['release']
2745 return 'Google-appcfg-%s' % (release,)
2748 def _ReadUrlContents(url):
2749 """Reads the contents of a URL into a string.
2751 Args:
2752 url: a string that is the URL to read.
2754 Returns:
2755 A string that is the contents read from the URL.
2757 Raises:
2758 urllib2.URLError: If the URL cannot be read.
2760 req = urllib2.Request(url)
2761 return urllib2.urlopen(req).read()
2764 class AppCfgApp(object):
2765 """Singleton class to wrap AppCfg tool functionality.
2767 This class is responsible for parsing the command line and executing
2768 the desired action on behalf of the user. Processing files and
2769 communicating with the server is handled by other classes.
2771 Attributes:
2772 actions: A dictionary mapping action names to Action objects.
2773 action: The Action specified on the command line.
2774 parser: An instance of optparse.OptionParser.
2775 options: The command line options parsed by 'parser'.
2776 argv: The original command line as a list.
2777 args: The positional command line args left over after parsing the options.
2778 raw_input_fn: Function used for getting raw user input, like email.
2779 password_input_fn: Function used for getting user password.
2780 error_fh: Unexpected HTTPErrors are printed to this file handle.
2782 Attributes for testing:
2783 parser_class: The class to use for parsing the command line. Because
2784 OptionsParser will exit the program when there is a parse failure, it
2785 is nice to subclass OptionsParser and catch the error before exiting.
2786 read_url_contents: A function to read the contents of a URL.
2789 def __init__(self, argv, parser_class=optparse.OptionParser,
2790 rpc_server_class=None,
2791 raw_input_fn=raw_input,
2792 password_input_fn=getpass.getpass,
2793 out_fh=sys.stdout,
2794 error_fh=sys.stderr,
2795 update_check_class=sdk_update_checker.SDKUpdateChecker,
2796 throttle_class=None,
2797 opener=open,
2798 file_iterator=FileIterator,
2799 time_func=time.time,
2800 wrap_server_error_message=True,
2801 oauth_client_id=APPCFG_CLIENT_ID,
2802 oauth_client_secret=APPCFG_CLIENT_NOTSOSECRET,
2803 oauth_scopes=APPCFG_SCOPES):
2804 """Initializer. Parses the cmdline and selects the Action to use.
2806 Initializes all of the attributes described in the class docstring.
2807 Prints help or error messages if there is an error parsing the cmdline.
2809 Args:
2810 argv: The list of arguments passed to this program.
2811 parser_class: Options parser to use for this application.
2812 rpc_server_class: RPC server class to use for this application.
2813 raw_input_fn: Function used for getting user email.
2814 password_input_fn: Function used for getting user password.
2815 out_fh: All normal output is printed to this file handle.
2816 error_fh: Unexpected HTTPErrors are printed to this file handle.
2817 update_check_class: sdk_update_checker.SDKUpdateChecker class (can be
2818 replaced for testing).
2819 throttle_class: A class to use instead of ThrottledHttpRpcServer
2820 (only used in the bulkloader).
2821 opener: Function used for opening files.
2822 file_iterator: Callable that takes (basepath, skip_files, file_separator)
2823 and returns a generator that yields all filenames in the file tree
2824 rooted at that path, skipping files that match the skip_files compiled
2825 regular expression.
2826 time_func: A time.time() compatible function, which can be overridden for
2827 testing.
2828 wrap_server_error_message: If true, the error messages from
2829 urllib2.HTTPError exceptions in Run() are wrapped with
2830 '--- begin server output ---' and '--- end server output ---',
2831 otherwise the error message is printed as is.
2832 oauth_client_id: The client ID of the project providing Auth. Defaults to
2833 the SDK default project client ID, the constant APPCFG_CLIENT_ID.
2834 oauth_client_secret: The client secret of the project providing Auth.
2835 Defaults to the SDK default project client secret, the constant
2836 APPCFG_CLIENT_NOTSOSECRET.
2837 oauth_scopes: The scope or set of scopes to be accessed by the OAuth2
2838 token retrieved. Defaults to APPCFG_SCOPES. Can be a string or
2839 iterable of strings, representing the scope(s) to request.
2841 self.parser_class = parser_class
2842 self.argv = argv
2843 self.rpc_server_class = rpc_server_class
2844 self.raw_input_fn = raw_input_fn
2845 self.password_input_fn = password_input_fn
2846 self.out_fh = out_fh
2847 self.error_fh = error_fh
2848 self.update_check_class = update_check_class
2849 self.throttle_class = throttle_class
2850 self.time_func = time_func
2851 self.wrap_server_error_message = wrap_server_error_message
2852 self.oauth_client_id = oauth_client_id
2853 self.oauth_client_secret = oauth_client_secret
2854 self.oauth_scopes = oauth_scopes
2856 self.read_url_contents = _ReadUrlContents
2862 self.parser = self._GetOptionParser()
2863 for action in self.actions.itervalues():
2864 action.options(self, self.parser)
2867 self.options, self.args = self.parser.parse_args(argv[1:])
2869 if len(self.args) < 1:
2870 self._PrintHelpAndExit()
2872 if not self.options.allow_any_runtime:
2873 if self.options.runtime:
2874 if self.options.runtime not in SUPPORTED_RUNTIMES:
2875 _PrintErrorAndExit(self.error_fh,
2876 '"%s" is not a supported runtime\n' %
2877 self.options.runtime)
2878 else:
2879 appinfo.AppInfoExternal.ATTRIBUTES[appinfo.RUNTIME] = (
2880 '|'.join(SUPPORTED_RUNTIMES))
2882 action = self.args.pop(0)
2884 def RaiseParseError(actionname, action):
2887 self.parser, self.options = self._MakeSpecificParser(action)
2888 error_desc = action.error_desc
2889 if not error_desc:
2890 error_desc = "Expected a <directory> argument after '%s'." % (
2891 actionname.split(' ')[0])
2892 self.parser.error(error_desc)
2897 if action == BACKENDS_ACTION:
2898 if len(self.args) < 1:
2899 RaiseParseError(action, self.actions[BACKENDS_ACTION])
2901 backend_action_first = BACKENDS_ACTION + ' ' + self.args[0]
2902 if backend_action_first in self.actions:
2903 self.args.pop(0)
2904 action = backend_action_first
2906 elif len(self.args) > 1:
2907 backend_directory_first = BACKENDS_ACTION + ' ' + self.args[1]
2908 if backend_directory_first in self.actions:
2909 self.args.pop(1)
2910 action = backend_directory_first
2913 if len(self.args) < 1 or action == BACKENDS_ACTION:
2914 RaiseParseError(action, self.actions[action])
2916 if action not in self.actions:
2917 self.parser.error("Unknown action: '%s'\n%s" %
2918 (action, self.parser.get_description()))
2921 self.action = self.actions[action]
2926 if not self.action.uses_basepath or self.options.help:
2927 self.basepath = None
2928 else:
2929 if not self.args:
2930 RaiseParseError(action, self.action)
2931 self.basepath = self.args.pop(0)
2937 self.parser, self.options = self._MakeSpecificParser(self.action)
2941 if self.options.help:
2942 self._PrintHelpAndExit()
2944 if self.options.verbose == 2:
2945 logging.getLogger().setLevel(logging.INFO)
2946 elif self.options.verbose == 3:
2947 logging.getLogger().setLevel(logging.DEBUG)
2952 global verbosity
2953 verbosity = self.options.verbose
2957 if any((self.options.oauth2_refresh_token, self.options.oauth2_access_token,
2958 self.options.authenticate_service_account)):
2959 self.options.oauth2 = True
2962 if self.options.oauth2_client_id:
2963 self.oauth_client_id = self.options.oauth2_client_id
2964 if self.options.oauth2_client_secret:
2965 self.oauth_client_secret = self.options.oauth2_client_secret
2970 self.opener = opener
2971 self.file_iterator = file_iterator
2973 def Run(self):
2974 """Executes the requested action.
2976 Catches any HTTPErrors raised by the action and prints them to stderr.
2978 Returns:
2979 1 on error, 0 if successful.
2981 try:
2982 self.action(self)
2983 except urllib2.HTTPError, e:
2984 body = e.read()
2985 if self.wrap_server_error_message:
2986 error_format = ('Error %d: --- begin server output ---\n'
2987 '%s\n--- end server output ---')
2988 else:
2989 error_format = 'Error %d: %s'
2991 print >>self.error_fh, (error_format % (e.code, body.rstrip('\n')))
2992 return 1
2993 except yaml_errors.EventListenerError, e:
2994 print >>self.error_fh, ('Error parsing yaml file:\n%s' % e)
2995 return 1
2996 except CannotStartServingError:
2997 print >>self.error_fh, 'Could not start serving the given version.'
2998 return 1
2999 return 0
3001 def _GetActionDescriptions(self):
3002 """Returns a formatted string containing the short_descs for all actions."""
3003 action_names = self.actions.keys()
3004 action_names.sort()
3005 desc = ''
3006 for action_name in action_names:
3007 if not self.actions[action_name].hidden:
3008 desc += ' %s: %s\n' % (action_name,
3009 self.actions[action_name].short_desc)
3010 return desc
3012 def _GetOptionParser(self):
3013 """Creates an OptionParser with generic usage and description strings.
3015 Returns:
3016 An OptionParser instance.
3019 class Formatter(optparse.IndentedHelpFormatter):
3020 """Custom help formatter that does not reformat the description."""
3022 def format_description(self, description):
3023 """Very simple formatter."""
3024 return description + '\n'
3026 class AppCfgOption(optparse.Option):
3027 """Custom Option for AppCfg.
3029 Adds an 'update' action for storing key-value pairs as a dict.
3032 _ACTION = 'update'
3033 ACTIONS = optparse.Option.ACTIONS + (_ACTION,)
3034 STORE_ACTIONS = optparse.Option.STORE_ACTIONS + (_ACTION,)
3035 TYPED_ACTIONS = optparse.Option.TYPED_ACTIONS + (_ACTION,)
3036 ALWAYS_TYPED_ACTIONS = optparse.Option.ALWAYS_TYPED_ACTIONS + (_ACTION,)
3038 def take_action(self, action, dest, opt, value, values, parser):
3039 if action != self._ACTION:
3040 return optparse.Option.take_action(
3041 self, action, dest, opt, value, values, parser)
3042 try:
3043 key, value = value.split(':', 1)
3044 except ValueError:
3045 raise optparse.OptionValueError(
3046 'option %s: invalid value: %s (must match NAME:VALUE)' % (
3047 opt, value))
3048 values.ensure_value(dest, {})[key] = value
3050 desc = self._GetActionDescriptions()
3051 desc = ('Action must be one of:\n%s'
3052 'Use \'help <action>\' for a detailed description.') % desc
3056 parser = self.parser_class(usage='%prog [options] <action>',
3057 description=desc,
3058 formatter=Formatter(),
3059 conflict_handler='resolve',
3060 option_class=AppCfgOption)
3065 parser.add_option('-h', '--help', action='store_true',
3066 dest='help', help='Show the help message and exit.')
3067 parser.add_option('-q', '--quiet', action='store_const', const=0,
3068 dest='verbose', help='Print errors only.')
3069 parser.add_option('-v', '--verbose', action='store_const', const=2,
3070 dest='verbose', default=1,
3071 help='Print info level logs.')
3072 parser.add_option('--noisy', action='store_const', const=3,
3073 dest='verbose', help='Print all logs.')
3074 parser.add_option('-s', '--server', action='store', dest='server',
3075 default='appengine.google.com',
3076 metavar='SERVER', help='The App Engine server.')
3077 parser.add_option('--secure', action='store_true', dest='secure',
3078 default=True, help=optparse.SUPPRESS_HELP)
3079 parser.add_option('--ignore_bad_cert', action='store_true',
3080 dest='ignore_certs', default=False,
3081 help=optparse.SUPPRESS_HELP)
3082 parser.add_option('--insecure', action='store_false', dest='secure',
3083 help=optparse.SUPPRESS_HELP)
3084 parser.add_option('-e', '--email', action='store', dest='email',
3085 metavar='EMAIL', default=None,
3086 help='The username to use. Will prompt if omitted.')
3087 parser.add_option('-H', '--host', action='store', dest='host',
3088 metavar='HOST', default=None,
3089 help='Overrides the Host header sent with all RPCs.')
3090 parser.add_option('--no_cookies', action='store_false',
3091 dest='save_cookies', default=True,
3092 help='Do not save authentication cookies to local disk.')
3093 parser.add_option('--skip_sdk_update_check', action='store_true',
3094 dest='skip_sdk_update_check', default=False,
3095 help='Do not check for SDK updates.')
3096 parser.add_option('--passin', action='store_true',
3097 dest='passin', default=False,
3098 help='Read the login password from stdin.')
3099 parser.add_option('-A', '--application', action='store', dest='app_id',
3100 help=('Set the application, overriding the application '
3101 'value from app.yaml file.'))
3102 parser.add_option('-M', '--module', action='store', dest='module',
3103 help=('Set the module, overriding the module value '
3104 'from app.yaml.'))
3105 parser.add_option('-V', '--version', action='store', dest='version',
3106 help=('Set the (major) version, overriding the version '
3107 'value from app.yaml file.'))
3108 parser.add_option('-r', '--runtime', action='store', dest='runtime',
3109 help='Override runtime from app.yaml file.')
3110 parser.add_option('-E', '--env_variable', action='update',
3111 dest='env_variables', metavar='NAME:VALUE',
3112 help=('Set an environment variable, potentially '
3113 'overriding an env_variable value from app.yaml '
3114 'file (flag may be repeated to set multiple '
3115 'variables).'))
3116 parser.add_option('-R', '--allow_any_runtime', action='store_true',
3117 dest='allow_any_runtime', default=False,
3118 help='Do not validate the runtime in app.yaml')
3119 parser.add_option('--oauth2', action='store_true', dest='oauth2',
3120 default=False,
3121 help='Use OAuth2 instead of password auth.')
3122 parser.add_option('--oauth2_refresh_token', action='store',
3123 dest='oauth2_refresh_token', default=None,
3124 help='An existing OAuth2 refresh token to use. Will '
3125 'not attempt interactive OAuth approval.')
3126 parser.add_option('--oauth2_access_token', action='store',
3127 dest='oauth2_access_token', default=None,
3128 help='An existing OAuth2 access token to use. Will '
3129 'not attempt interactive OAuth approval.')
3130 parser.add_option('--oauth2_client_id', action='store',
3131 dest='oauth2_client_id', default=None,
3132 help=optparse.SUPPRESS_HELP)
3133 parser.add_option('--oauth2_client_secret', action='store',
3134 dest='oauth2_client_secret', default=None,
3135 help=optparse.SUPPRESS_HELP)
3136 parser.add_option('--oauth2_credential_file', action='store',
3137 dest='oauth2_credential_file', default=None,
3138 help=optparse.SUPPRESS_HELP)
3139 parser.add_option('--authenticate_service_account', action='store_true',
3140 dest='authenticate_service_account', default=False,
3141 help='Authenticate using the default service account '
3142 'for the Google Compute Engine VM in which appcfg is '
3143 'being called')
3144 parser.add_option('--noauth_local_webserver', action='store_false',
3145 dest='auth_local_webserver', default=True,
3146 help='Do not run a local web server to handle redirects '
3147 'during OAuth authorization.')
3148 parser.add_option('--called_by_gcloud',
3149 action='store_true', default=False,
3150 help=optparse.SUPPRESS_HELP)
3151 return parser
3153 def _MakeSpecificParser(self, action):
3154 """Creates a new parser with documentation specific to 'action'.
3156 Args:
3157 action: An Action instance to be used when initializing the new parser.
3159 Returns:
3160 A tuple containing:
3161 parser: An instance of OptionsParser customized to 'action'.
3162 options: The command line options after re-parsing.
3164 parser = self._GetOptionParser()
3165 parser.set_usage(action.usage)
3166 parser.set_description('%s\n%s' % (action.short_desc, action.long_desc))
3167 action.options(self, parser)
3168 options, unused_args = parser.parse_args(self.argv[1:])
3169 return parser, options
3171 def _PrintHelpAndExit(self, exit_code=2):
3172 """Prints the parser's help message and exits the program.
3174 Args:
3175 exit_code: The integer code to pass to sys.exit().
3177 self.parser.print_help()
3178 sys.exit(exit_code)
3180 def _GetRpcServer(self):
3181 """Returns an instance of an AbstractRpcServer.
3183 Returns:
3184 A new AbstractRpcServer, on which RPC calls can be made.
3186 Raises:
3187 OAuthNotAvailable: OAuth is requested but the dependecies aren't imported.
3188 RuntimeError: The user has request non-interactive authentication but the
3189 environment is not correct for that to work.
3192 def GetUserCredentials():
3193 """Prompts the user for a username and password."""
3194 email = self.options.email
3195 if email is None:
3196 email = self.raw_input_fn('Email: ')
3198 password_prompt = 'Password for %s: ' % email
3201 if self.options.passin:
3202 password = self.raw_input_fn(password_prompt)
3203 else:
3204 password = self.password_input_fn(password_prompt)
3206 return (email, password)
3208 StatusUpdate('Host: %s' % self.options.server, self.error_fh)
3210 source = GetSourceName()
3214 dev_appserver = self.options.host == 'localhost'
3215 if self.options.oauth2 and not dev_appserver:
3216 if not appengine_rpc_httplib2:
3218 raise OAuthNotAvailable()
3219 if not self.rpc_server_class:
3220 self.rpc_server_class = appengine_rpc_httplib2.HttpRpcServerOAuth2
3223 get_user_credentials = (
3224 appengine_rpc_httplib2.HttpRpcServerOAuth2.OAuth2Parameters(
3225 access_token=self.options.oauth2_access_token,
3226 client_id=self.oauth_client_id,
3227 client_secret=self.oauth_client_secret,
3228 scope=self.oauth_scopes,
3229 refresh_token=self.options.oauth2_refresh_token,
3230 credential_file=self.options.oauth2_credential_file,
3231 token_uri=self._GetTokenUri()))
3233 if hasattr(appengine_rpc_httplib2.tools, 'FLAGS'):
3234 appengine_rpc_httplib2.tools.FLAGS.auth_local_webserver = (
3235 self.options.auth_local_webserver)
3236 else:
3237 if not self.rpc_server_class:
3238 self.rpc_server_class = appengine_rpc.HttpRpcServerWithOAuth2Suggestion
3239 if hasattr(self, 'runtime'):
3240 self.rpc_server_class.RUNTIME = self.runtime
3241 get_user_credentials = GetUserCredentials
3244 if dev_appserver:
3245 email = self.options.email
3246 if email is None:
3247 email = 'test@example.com'
3248 logging.info('Using debug user %s. Override with --email', email)
3249 rpcserver = self.rpc_server_class(
3250 self.options.server,
3251 lambda: (email, 'password'),
3252 GetUserAgent(),
3253 source,
3254 host_override=self.options.host,
3255 save_cookies=self.options.save_cookies,
3257 secure=False)
3259 rpcserver.authenticated = True
3260 return rpcserver
3263 if self.options.passin:
3264 auth_tries = 1
3265 else:
3266 auth_tries = 3
3268 return self.rpc_server_class(self.options.server, get_user_credentials,
3269 GetUserAgent(), source,
3270 host_override=self.options.host,
3271 save_cookies=self.options.save_cookies,
3272 auth_tries=auth_tries,
3273 account_type='HOSTED_OR_GOOGLE',
3274 secure=self.options.secure,
3275 ignore_certs=self.options.ignore_certs)
3277 def _GetTokenUri(self):
3278 """Returns the OAuth2 token_uri, or None to use the default URI.
3280 Returns:
3281 A string that is the token_uri, or None.
3283 Raises:
3284 RuntimeError: The user has requested authentication for a service account
3285 but the environment is not correct for that to work.
3287 if self.options.authenticate_service_account:
3291 url = '%s/%s/scopes' % (METADATA_BASE, SERVICE_ACCOUNT_BASE)
3292 try:
3293 vm_scopes_string = self.read_url_contents(url)
3294 except urllib2.URLError, e:
3295 raise RuntimeError('Could not obtain scope list from metadata service: '
3296 '%s: %s. This may be because we are not running in '
3297 'a Google Compute Engine VM.' % (url, e))
3298 vm_scopes = vm_scopes_string.split()
3299 missing = list(set(self.oauth_scopes).difference(vm_scopes))
3300 if missing:
3301 raise RuntimeError('Required scopes %s missing from %s. '
3302 'This VM instance probably needs to be recreated '
3303 'with the missing scopes.' % (missing, vm_scopes))
3304 return '%s/%s/token' % (METADATA_BASE, SERVICE_ACCOUNT_BASE)
3305 else:
3306 return None
3308 def _FindYaml(self, basepath, file_name):
3309 """Find yaml files in application directory.
3311 Args:
3312 basepath: Base application directory.
3313 file_name: Relative file path from basepath, without extension, to search
3314 for.
3316 Returns:
3317 Path to located yaml file if one exists, else None.
3319 if not os.path.isdir(basepath):
3320 self.parser.error('Not a directory: %s' % basepath)
3324 alt_basepath = os.path.join(basepath, 'WEB-INF', 'appengine-generated')
3326 for yaml_basepath in (basepath, alt_basepath):
3327 for yaml_file in (file_name + '.yaml', file_name + '.yml'):
3328 yaml_path = os.path.join(yaml_basepath, yaml_file)
3329 if os.path.isfile(yaml_path):
3330 return yaml_path
3332 return None
3334 def _ParseAppInfoFromYaml(self, basepath, basename='app'):
3335 """Parses the app.yaml file.
3337 Args:
3338 basepath: The directory of the application.
3339 basename: The relative file path, from basepath, to search for.
3341 Returns:
3342 An AppInfoExternal object.
3344 try:
3345 appyaml = self._ParseYamlFile(basepath, basename, appinfo_includes.Parse)
3346 except yaml_errors.EventListenerError, e:
3347 self.parser.error('Error parsing %s.yaml: %s.' % (
3348 os.path.join(basepath, basename), e))
3349 if not appyaml:
3350 if JavaSupported():
3351 if appcfg_java.IsWarFileWithoutYaml(basepath):
3352 java_app_update = appcfg_java.JavaAppUpdate(basepath, self.options)
3353 appyaml_string = java_app_update.GenerateAppYamlString([])
3354 appyaml = appinfo.LoadSingleAppInfo(appyaml_string)
3355 if not appyaml:
3356 self.parser.error('Directory contains neither an %s.yaml '
3357 'configuration file nor a WEB-INF subdirectory '
3358 'with web.xml and appengine-web.xml.' % basename)
3359 else:
3360 self.parser.error('Directory does not contain an %s.yaml configuration '
3361 'file' % basename)
3363 orig_application = appyaml.application
3364 orig_module = appyaml.module
3365 orig_version = appyaml.version
3366 if self.options.app_id:
3367 appyaml.application = self.options.app_id
3368 if self.options.module:
3369 appyaml.module = self.options.module
3370 if self.options.version:
3371 appyaml.version = self.options.version
3372 if self.options.runtime:
3373 appinfo.VmSafeSetRuntime(appyaml, self.options.runtime)
3374 if self.options.env_variables:
3375 if appyaml.env_variables is None:
3376 appyaml.env_variables = appinfo.EnvironmentVariables()
3377 appyaml.env_variables.update(self.options.env_variables)
3379 if not appyaml.application:
3380 self.parser.error('Expected -A app_id when application property in file '
3381 '%s.yaml is not set.' % basename)
3383 msg = 'Application: %s' % appyaml.application
3384 if appyaml.application != orig_application:
3385 msg += ' (was: %s)' % orig_application
3386 if self.action.function is 'Update':
3388 if (appyaml.module is not None and
3389 appyaml.module != appinfo.DEFAULT_MODULE):
3390 msg += '; module: %s' % appyaml.module
3391 if appyaml.module != orig_module:
3392 msg += ' (was: %s)' % orig_module
3393 msg += '; version: %s' % appyaml.version
3394 if appyaml.version != orig_version:
3395 msg += ' (was: %s)' % orig_version
3396 StatusUpdate(msg, self.error_fh)
3397 return appyaml
3399 def _ParseYamlFile(self, basepath, basename, parser):
3400 """Parses a yaml file.
3402 Args:
3403 basepath: The base directory of the application.
3404 basename: The relative file path, from basepath, (with the '.yaml'
3405 stripped off).
3406 parser: the function or method used to parse the file.
3408 Returns:
3409 A single parsed yaml file or None if the file does not exist.
3411 file_name = self._FindYaml(basepath, basename)
3412 if file_name is not None:
3413 fh = self.opener(file_name, 'r')
3414 try:
3415 defns = parser(fh, open_fn=self.opener)
3416 finally:
3417 fh.close()
3418 return defns
3419 return None
3421 def _ParseBackendsYaml(self, basepath):
3422 """Parses the backends.yaml file.
3424 Args:
3425 basepath: the directory of the application.
3427 Returns:
3428 A BackendsInfoExternal object or None if the file does not exist.
3430 return self._ParseYamlFile(basepath, 'backends',
3431 backendinfo.LoadBackendInfo)
3433 def _ParseIndexYaml(self, basepath, appyaml=None):
3434 """Parses the index.yaml file.
3436 Args:
3437 basepath: the directory of the application.
3438 appyaml: The app.yaml, if present.
3439 Returns:
3440 A single parsed yaml file or None if the file does not exist.
3442 index_yaml = self._ParseYamlFile(basepath,
3443 'index',
3444 datastore_index.ParseIndexDefinitions)
3445 if not index_yaml:
3446 return None
3447 self._SetApplication(index_yaml, 'index', appyaml)
3449 return index_yaml
3451 def _SetApplication(self, dest_yaml, basename, appyaml=None):
3452 """Parses and sets the application property onto the dest_yaml parameter.
3454 The order of precendence is:
3455 1. Command line (-A application)
3456 2. Specified dest_yaml file
3457 3. App.yaml file
3459 This exits with a parse error if application is not present in any of these
3460 locations.
3462 Args:
3463 dest_yaml: The yaml object to set 'application' on.
3464 basename: The name of the dest_yaml file for use in errors.
3465 appyaml: The already parsed appyaml, if present. If none, this method will
3466 attempt to parse app.yaml.
3468 if self.options.app_id:
3469 dest_yaml.application = self.options.app_id
3470 if not dest_yaml.application:
3471 if not appyaml:
3472 appyaml = self._ParseYamlFile(self.basepath,
3473 'app',
3474 appinfo_includes.Parse)
3475 if appyaml:
3476 dest_yaml.application = appyaml.application
3477 else:
3478 self.parser.error('Expected -A app_id when %s.yaml.application is not '
3479 'set and app.yaml is not present.' % basename)
3481 def _ParseCronYaml(self, basepath, appyaml=None):
3482 """Parses the cron.yaml file.
3484 Args:
3485 basepath: the directory of the application.
3486 appyaml: The app.yaml, if present.
3488 Returns:
3489 A CronInfoExternal object or None if the file does not exist.
3491 cron_yaml = self._ParseYamlFile(basepath, 'cron', croninfo.LoadSingleCron)
3492 if not cron_yaml:
3493 return None
3494 self._SetApplication(cron_yaml, 'cron', appyaml)
3496 return cron_yaml
3498 def _ParseQueueYaml(self, basepath, appyaml=None):
3499 """Parses the queue.yaml file.
3501 Args:
3502 basepath: the directory of the application.
3503 appyaml: The app.yaml, if present.
3505 Returns:
3506 A QueueInfoExternal object or None if the file does not exist.
3508 queue_yaml = self._ParseYamlFile(basepath,
3509 'queue',
3510 queueinfo.LoadSingleQueue)
3511 if not queue_yaml:
3512 return None
3514 self._SetApplication(queue_yaml, 'queue', appyaml)
3515 return queue_yaml
3517 def _ParseDispatchYaml(self, basepath, appyaml=None):
3518 """Parses the dispatch.yaml file.
3520 Args:
3521 basepath: the directory of the application.
3522 appyaml: The app.yaml, if present.
3524 Returns:
3525 A DispatchInfoExternal object or None if the file does not exist.
3527 dispatch_yaml = self._ParseYamlFile(basepath,
3528 'dispatch',
3529 dispatchinfo.LoadSingleDispatch)
3531 if not dispatch_yaml:
3532 return None
3534 self._SetApplication(dispatch_yaml, 'dispatch', appyaml)
3535 return dispatch_yaml
3537 def _ParseDosYaml(self, basepath, appyaml=None):
3538 """Parses the dos.yaml file.
3540 Args:
3541 basepath: the directory of the application.
3542 appyaml: The app.yaml, if present.
3544 Returns:
3545 A DosInfoExternal object or None if the file does not exist.
3547 dos_yaml = self._ParseYamlFile(basepath, 'dos', dosinfo.LoadSingleDos)
3548 if not dos_yaml:
3549 return None
3551 self._SetApplication(dos_yaml, 'dos', appyaml)
3552 return dos_yaml
3554 def Help(self, action=None):
3555 """Prints help for a specific action.
3557 Args:
3558 action: If provided, print help for the action provided.
3560 Expects self.args[0], or 'action', to contain the name of the action in
3561 question. Exits the program after printing the help message.
3563 if not action:
3564 if len(self.args) > 1:
3565 self.args = [' '.join(self.args)]
3567 if len(self.args) != 1 or self.args[0] not in self.actions:
3568 self.parser.error('Expected a single action argument. '
3569 ' Must be one of:\n' +
3570 self._GetActionDescriptions())
3571 action = self.args[0]
3572 action = self.actions[action]
3573 self.parser, unused_options = self._MakeSpecificParser(action)
3574 self._PrintHelpAndExit(exit_code=0)
3576 def DownloadApp(self):
3577 """Downloads the given app+version."""
3578 if len(self.args) != 1:
3579 self.parser.error('\"download_app\" expects one non-option argument, '
3580 'found ' + str(len(self.args)) + '.')
3582 out_dir = self.args[0]
3584 app_id = self.options.app_id
3585 if app_id is None:
3586 self.parser.error('You must specify an app ID via -A or --application.')
3588 module = self.options.module
3589 app_version = self.options.version
3593 if os.path.exists(out_dir):
3594 if not os.path.isdir(out_dir):
3595 self.parser.error('Cannot download to path "%s": '
3596 'there\'s a file in the way.' % out_dir)
3597 elif os.listdir(out_dir):
3598 self.parser.error('Cannot download to path "%s": directory already '
3599 'exists and it isn\'t empty.' % out_dir)
3601 rpcserver = self._GetRpcServer()
3603 DoDownloadApp(rpcserver, out_dir, app_id, module, app_version)
3605 def UpdateVersion(self, rpcserver, basepath, appyaml, module_yaml_path,
3606 backend=None):
3607 """Updates and deploys a new appversion.
3609 Args:
3610 rpcserver: An AbstractRpcServer instance on which RPC calls can be made.
3611 basepath: The root directory of the version to update.
3612 appyaml: The AppInfoExternal object parsed from an app.yaml-like file.
3613 module_yaml_path: The (string) path to the yaml file, relative to the
3614 bundle directory.
3615 backend: The name of the backend to update, if any.
3617 Returns:
3618 An appinfo.AppInfoSummary if one was returned from the Deploy, None
3619 otherwise.
3621 Raises:
3622 RuntimeError: If go-app-builder fails to generate a mapping from relative
3623 paths to absolute paths, its stderr is raised.
3640 runtime = appyaml.GetEffectiveRuntime()
3641 if appyaml.vm and (self.options.called_by_gcloud or runtime != 'go'):
3642 self.options.precompilation = False
3643 elif runtime == 'dart':
3644 self.options.precompilation = False
3645 elif runtime == 'go' and not self.options.precompilation:
3646 logging.warning('Precompilation is required for Go apps; '
3647 'ignoring --no_precompilation')
3648 self.options.precompilation = True
3649 elif (runtime.startswith('java') and
3650 appinfo.JAVA_PRECOMPILED not in (appyaml.derived_file_type or [])):
3651 self.options.precompilation = False
3653 if self.options.precompilation:
3654 if not appyaml.derived_file_type:
3655 appyaml.derived_file_type = []
3656 if appinfo.PYTHON_PRECOMPILED not in appyaml.derived_file_type:
3657 appyaml.derived_file_type.append(appinfo.PYTHON_PRECOMPILED)
3659 paths = self.file_iterator(basepath, appyaml.skip_files, appyaml.runtime)
3660 openfunc = lambda path: self.opener(os.path.join(basepath, path), 'rb')
3662 if appyaml.GetEffectiveRuntime() == 'go':
3664 sdk_base = os.path.normpath(os.path.join(
3665 google.appengine.__file__, '..', '..', '..'))
3667 gopath = os.environ.get('GOPATH')
3668 if not gopath:
3669 gopath = os.path.join(sdk_base, 'gopath')
3675 goroot = os.path.join(sdk_base, 'goroot')
3676 if not os.path.exists(goroot):
3678 goroot = None
3679 gab = os.path.join(sdk_base, GO_APP_BUILDER)
3680 if os.path.exists(gab):
3681 app_paths = list(paths)
3682 go_files = [f for f in app_paths
3683 if f.endswith('.go') and not appyaml.nobuild_files.match(f)]
3684 if not go_files:
3685 raise RuntimeError('no Go source files to upload '
3686 '(-nobuild_files applied)')
3687 gab_argv = [
3688 gab,
3689 '-app_base', self.basepath,
3690 '-arch', '6',
3691 '-gopath', gopath,
3692 '-print_extras',
3694 if goroot:
3695 gab_argv.extend(['-goroot', goroot])
3696 if appyaml.runtime == 'vm':
3697 gab_argv.append('-vm')
3698 gab_argv.extend(go_files)
3700 env = {
3701 'GOOS': 'linux',
3702 'GOARCH': 'amd64',
3704 logging.info('Invoking go-app-builder: %s', ' '.join(gab_argv))
3705 try:
3706 p = subprocess.Popen(gab_argv, stdout=subprocess.PIPE,
3707 stderr=subprocess.PIPE, env=env)
3708 (stdout, stderr) = p.communicate()
3709 except Exception, e:
3710 raise RuntimeError('failed running go-app-builder', e)
3711 if p.returncode != 0:
3712 raise RuntimeError(stderr)
3717 overlay = dict([l.split('|') for l in stdout.split('\n') if l])
3718 logging.info('GOPATH overlay: %s', overlay)
3720 def Open(path):
3721 if path in overlay:
3722 return self.opener(overlay[path], 'rb')
3723 return self.opener(os.path.join(basepath, path), 'rb')
3724 paths = app_paths + overlay.keys()
3725 openfunc = Open
3727 appversion = AppVersionUpload(rpcserver,
3728 appyaml,
3729 module_yaml_path=module_yaml_path,
3730 backend=backend,
3731 error_fh=self.error_fh,
3732 usage_reporting=self.options.usage_reporting)
3733 return appversion.DoUpload(paths, openfunc)
3735 def UpdateUsingSpecificFiles(self):
3736 """Updates and deploys new app versions based on given config files."""
3737 rpcserver = self._GetRpcServer()
3738 all_files = [self.basepath] + self.args
3739 has_python25_version = False
3741 for yaml_path in all_files:
3742 file_name = os.path.basename(yaml_path)
3743 self.basepath = os.path.dirname(yaml_path)
3744 if not self.basepath:
3745 self.basepath = '.'
3746 module_yaml = self._ParseAppInfoFromYaml(self.basepath,
3747 os.path.splitext(file_name)[0])
3748 if module_yaml.runtime == 'python':
3749 has_python25_version = True
3753 if not module_yaml.module and file_name != 'app.yaml':
3754 ErrorUpdate("Error: 'module' parameter not specified in %s" %
3755 yaml_path)
3756 continue
3757 self.UpdateVersion(rpcserver, self.basepath, module_yaml, file_name)
3758 if has_python25_version:
3759 MigratePython27Notice()
3761 def Update(self):
3762 """Updates and deploys a new appversion and global app configs."""
3763 if not os.path.isdir(self.basepath):
3765 self.UpdateUsingSpecificFiles()
3766 return
3768 if JavaSupported() and appcfg_java.IsWarFileWithoutYaml(self.basepath):
3769 java_app_update = appcfg_java.JavaAppUpdate(self.basepath, self.options)
3770 self.options.compile_jsps = not java_app_update.app_engine_web_xml.vm
3777 sdk_root = os.path.dirname(appcfg_java.__file__)
3778 self.stage_dir = java_app_update.CreateStagingDirectory(sdk_root)
3779 try:
3780 appyaml = self._ParseAppInfoFromYaml(
3781 self.stage_dir,
3782 basename=os.path.splitext(APP_YAML_FILENAME)[0])
3783 self._UpdateWithParsedAppYaml(appyaml, self.stage_dir)
3784 finally:
3785 if self.options.retain_upload_dir:
3786 StatusUpdate(
3787 'Temporary staging directory left in %s' % self.stage_dir,
3788 self.error_fh)
3789 else:
3790 shutil.rmtree(self.stage_dir)
3791 else:
3792 appyaml = self._ParseAppInfoFromYaml(
3793 self.basepath,
3794 basename=os.path.splitext(APP_YAML_FILENAME)[0])
3795 self._UpdateWithParsedAppYaml(appyaml, self.basepath)
3797 def _UpdateWithParsedAppYaml(self, appyaml, basepath):
3798 """Completes update command.
3800 Helper to Update.
3802 Args:
3803 appyaml: AppInfoExternal for the app.
3804 basepath: Path where application's files can be found.
3806 self.runtime = appyaml.runtime
3807 rpcserver = self._GetRpcServer()
3812 if self.options.skip_sdk_update_check:
3813 logging.info('Skipping update check')
3814 else:
3815 updatecheck = self.update_check_class(rpcserver, appyaml)
3816 updatecheck.CheckForUpdates()
3818 def _AbortAppMismatch(yaml_name):
3819 StatusUpdate('Error: Aborting upload because application in %s does not '
3820 'match application in app.yaml' % yaml_name, self.error_fh)
3823 dos_yaml = self._ParseDosYaml(basepath, appyaml)
3824 if dos_yaml and dos_yaml.application != appyaml.application:
3825 _AbortAppMismatch('dos.yaml')
3826 return
3828 queue_yaml = self._ParseQueueYaml(basepath, appyaml)
3829 if queue_yaml and queue_yaml.application != appyaml.application:
3830 _AbortAppMismatch('queue.yaml')
3831 return
3833 cron_yaml = self._ParseCronYaml(basepath, appyaml)
3834 if cron_yaml and cron_yaml.application != appyaml.application:
3835 _AbortAppMismatch('cron.yaml')
3836 return
3838 index_defs = self._ParseIndexYaml(basepath, appyaml)
3839 if index_defs and index_defs.application != appyaml.application:
3840 _AbortAppMismatch('index.yaml')
3841 return
3843 dispatch_yaml = self._ParseDispatchYaml(basepath, appyaml)
3844 if dispatch_yaml and dispatch_yaml.application != appyaml.application:
3845 _AbortAppMismatch('dispatch.yaml')
3846 return
3848 self.UpdateVersion(rpcserver, basepath, appyaml, APP_YAML_FILENAME)
3850 if appyaml.runtime == 'python':
3851 MigratePython27Notice()
3854 if self.options.backends:
3855 self.BackendsUpdate()
3862 if index_defs:
3863 index_upload = IndexDefinitionUpload(rpcserver, index_defs, self.error_fh)
3864 try:
3865 index_upload.DoUpload()
3866 except urllib2.HTTPError, e:
3867 ErrorUpdate('Error %d: --- begin server output ---\n'
3868 '%s\n--- end server output ---' %
3869 (e.code, e.read().rstrip('\n')))
3870 print >> self.error_fh, (
3871 'Your app was updated, but there was an error updating your '
3872 'indexes. Please retry later with appcfg.py update_indexes.')
3875 if cron_yaml:
3876 cron_upload = CronEntryUpload(rpcserver, cron_yaml, self.error_fh)
3877 cron_upload.DoUpload()
3880 if queue_yaml:
3881 queue_upload = QueueEntryUpload(rpcserver, queue_yaml, self.error_fh)
3882 queue_upload.DoUpload()
3885 if dos_yaml:
3886 dos_upload = DosEntryUpload(rpcserver, dos_yaml, self.error_fh)
3887 dos_upload.DoUpload()
3890 if dispatch_yaml:
3891 dispatch_upload = DispatchEntryUpload(rpcserver,
3892 dispatch_yaml,
3893 self.error_fh)
3894 dispatch_upload.DoUpload()
3897 if appyaml:
3898 pagespeed_upload = PagespeedEntryUpload(
3899 rpcserver, appyaml, appyaml.pagespeed, self.error_fh)
3900 try:
3901 pagespeed_upload.DoUpload()
3902 except urllib2.HTTPError, e:
3903 ErrorUpdate('Error %d: --- begin server output ---\n'
3904 '%s\n--- end server output ---' %
3905 (e.code, e.read().rstrip('\n')))
3906 print >> self.error_fh, (
3907 'Your app was updated, but there was an error updating PageSpeed. '
3908 'Please try the update again later.')
3910 def _UpdateOptions(self, parser):
3911 """Adds update-specific options to 'parser'.
3913 Args:
3914 parser: An instance of OptionsParser.
3916 parser.add_option('--no_precompilation', action='store_false',
3917 dest='precompilation', default=True,
3918 help='Disable automatic precompilation '
3919 '(ignored for Go apps).')
3920 parser.add_option('--backends', action='store_true',
3921 dest='backends', default=False,
3922 help='Update backends when performing appcfg update.')
3923 parser.add_option('--no_usage_reporting', action='store_false',
3924 dest='usage_reporting', default=True,
3925 help='Disable usage reporting.')
3926 if JavaSupported():
3927 appcfg_java.AddUpdateOptions(parser)
3929 def VacuumIndexes(self):
3930 """Deletes unused indexes."""
3931 if self.args:
3932 self.parser.error('Expected a single <directory> argument.')
3935 index_defs = self._ParseIndexYaml(self.basepath)
3936 if index_defs is None:
3937 index_defs = datastore_index.IndexDefinitions()
3939 rpcserver = self._GetRpcServer()
3940 vacuum = VacuumIndexesOperation(rpcserver,
3941 self.options.force_delete)
3942 vacuum.DoVacuum(index_defs)
3944 def _VacuumIndexesOptions(self, parser):
3945 """Adds vacuum_indexes-specific options to 'parser'.
3947 Args:
3948 parser: An instance of OptionsParser.
3950 parser.add_option('-f', '--force', action='store_true', dest='force_delete',
3951 default=False,
3952 help='Force deletion without being prompted.')
3954 def UpdateCron(self):
3955 """Updates any new or changed cron definitions."""
3956 if self.args:
3957 self.parser.error('Expected a single <directory> argument.')
3959 rpcserver = self._GetRpcServer()
3962 cron_yaml = self._ParseCronYaml(self.basepath)
3963 if cron_yaml:
3964 cron_upload = CronEntryUpload(rpcserver, cron_yaml, self.error_fh)
3965 cron_upload.DoUpload()
3966 else:
3967 print >>self.error_fh, (
3968 'Could not find cron configuration. No action taken.')
3970 def UpdateIndexes(self):
3971 """Updates indexes."""
3972 if self.args:
3973 self.parser.error('Expected a single <directory> argument.')
3975 rpcserver = self._GetRpcServer()
3978 index_defs = self._ParseIndexYaml(self.basepath)
3979 if index_defs:
3980 index_upload = IndexDefinitionUpload(rpcserver, index_defs, self.error_fh)
3981 index_upload.DoUpload()
3982 else:
3983 print >>self.error_fh, (
3984 'Could not find index configuration. No action taken.')
3986 def UpdateQueues(self):
3987 """Updates any new or changed task queue definitions."""
3988 if self.args:
3989 self.parser.error('Expected a single <directory> argument.')
3990 rpcserver = self._GetRpcServer()
3993 queue_yaml = self._ParseQueueYaml(self.basepath)
3994 if queue_yaml:
3995 queue_upload = QueueEntryUpload(rpcserver, queue_yaml, self.error_fh)
3996 queue_upload.DoUpload()
3997 else:
3998 print >>self.error_fh, (
3999 'Could not find queue configuration. No action taken.')
4001 def UpdateDispatch(self):
4002 """Updates new or changed dispatch definitions."""
4003 if self.args:
4004 self.parser.error('Expected a single <directory> argument.')
4006 rpcserver = self._GetRpcServer()
4009 dispatch_yaml = self._ParseDispatchYaml(self.basepath)
4010 if dispatch_yaml:
4011 dispatch_upload = DispatchEntryUpload(rpcserver,
4012 dispatch_yaml,
4013 self.error_fh)
4014 dispatch_upload.DoUpload()
4015 else:
4016 print >>self.error_fh, ('Could not find dispatch configuration. No action'
4017 ' taken.')
4019 def UpdateDos(self):
4020 """Updates any new or changed dos definitions."""
4021 if self.args:
4022 self.parser.error('Expected a single <directory> argument.')
4023 rpcserver = self._GetRpcServer()
4026 dos_yaml = self._ParseDosYaml(self.basepath)
4027 if dos_yaml:
4028 dos_upload = DosEntryUpload(rpcserver, dos_yaml, self.error_fh)
4029 dos_upload.DoUpload()
4030 else:
4031 print >>self.error_fh, (
4032 'Could not find dos configuration. No action taken.')
4034 def BackendsAction(self):
4035 """Placeholder; we never expect this action to be invoked."""
4036 pass
4038 def BackendsPhpCheck(self, appyaml):
4039 """Don't support backends with the PHP runtime.
4041 This should be used to prevent use of backends update/start/configure
4042 with the PHP runtime. We continue to allow backends
4043 stop/delete/list/rollback just in case there are existing PHP backends.
4045 Args:
4046 appyaml: A parsed app.yaml file.
4048 if appyaml.runtime == 'php':
4049 _PrintErrorAndExit(
4050 self.error_fh,
4051 'Error: Backends are not supported with the PHP runtime. '
4052 'Please use Modules instead.\n')
4054 def BackendsYamlCheck(self, basepath, appyaml, backend=None):
4055 """Check the backends.yaml file is sane and which backends to update."""
4058 if appyaml.backends:
4059 self.parser.error('Backends are not allowed in app.yaml.')
4061 backends_yaml = self._ParseBackendsYaml(basepath)
4062 appyaml.backends = backends_yaml.backends
4064 if not appyaml.backends:
4065 self.parser.error('No backends found in backends.yaml.')
4067 backends = []
4068 for backend_entry in appyaml.backends:
4069 entry = backendinfo.LoadBackendEntry(backend_entry.ToYAML())
4070 if entry.name in backends:
4071 self.parser.error('Duplicate entry for backend: %s.' % entry.name)
4072 else:
4073 backends.append(entry.name)
4075 backends_to_update = []
4077 if backend:
4079 if backend in backends:
4080 backends_to_update = [backend]
4081 else:
4082 self.parser.error("Backend '%s' not found in backends.yaml." %
4083 backend)
4084 else:
4086 backends_to_update = backends
4088 return backends_to_update
4090 def BackendsUpdate(self):
4091 """Updates a backend."""
4092 self.backend = None
4093 if len(self.args) == 1:
4094 self.backend = self.args[0]
4095 elif len(self.args) > 1:
4096 self.parser.error('Expected an optional <backend> argument.')
4097 if JavaSupported() and appcfg_java.IsWarFileWithoutYaml(self.basepath):
4098 java_app_update = appcfg_java.JavaAppUpdate(self.basepath, self.options)
4099 self.options.compile_jsps = True
4100 sdk_root = os.path.dirname(appcfg_java.__file__)
4101 basepath = java_app_update.CreateStagingDirectory(sdk_root)
4102 else:
4103 basepath = self.basepath
4105 yaml_file_basename = 'app'
4106 appyaml = self._ParseAppInfoFromYaml(basepath,
4107 basename=yaml_file_basename)
4108 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4109 self.BackendsPhpCheck(appyaml)
4110 rpcserver = self._GetRpcServer()
4112 backends_to_update = self.BackendsYamlCheck(basepath, appyaml, self.backend)
4113 for backend in backends_to_update:
4114 self.UpdateVersion(rpcserver, basepath, appyaml, yaml_file_basename,
4115 backend=backend)
4117 def BackendsList(self):
4118 """Lists all backends for an app."""
4119 if self.args:
4120 self.parser.error('Expected no arguments.')
4125 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4126 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4127 rpcserver = self._GetRpcServer()
4128 response = rpcserver.Send('/api/backends/list', app_id=appyaml.application)
4129 print >> self.out_fh, response
4131 def BackendsRollback(self):
4132 """Does a rollback of an existing transaction on this backend."""
4133 if len(self.args) != 1:
4134 self.parser.error('Expected a single <backend> argument.')
4136 self._Rollback(self.args[0])
4138 def BackendsStart(self):
4139 """Starts a backend."""
4140 if len(self.args) != 1:
4141 self.parser.error('Expected a single <backend> argument.')
4143 backend = self.args[0]
4144 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4145 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4146 self.BackendsPhpCheck(appyaml)
4147 rpcserver = self._GetRpcServer()
4148 response = rpcserver.Send('/api/backends/start',
4149 app_id=appyaml.application,
4150 backend=backend)
4151 print >> self.out_fh, response
4153 def BackendsStop(self):
4154 """Stops a backend."""
4155 if len(self.args) != 1:
4156 self.parser.error('Expected a single <backend> argument.')
4158 backend = self.args[0]
4159 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4160 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4161 rpcserver = self._GetRpcServer()
4162 response = rpcserver.Send('/api/backends/stop',
4163 app_id=appyaml.application,
4164 backend=backend)
4165 print >> self.out_fh, response
4167 def BackendsDelete(self):
4168 """Deletes a backend."""
4169 if len(self.args) != 1:
4170 self.parser.error('Expected a single <backend> argument.')
4172 backend = self.args[0]
4173 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4174 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4175 rpcserver = self._GetRpcServer()
4176 response = rpcserver.Send('/api/backends/delete',
4177 app_id=appyaml.application,
4178 backend=backend)
4179 print >> self.out_fh, response
4181 def BackendsConfigure(self):
4182 """Changes the configuration of an existing backend."""
4183 if len(self.args) != 1:
4184 self.parser.error('Expected a single <backend> argument.')
4186 backend = self.args[0]
4187 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4188 BackendsStatusUpdate(appyaml.runtime, self.error_fh)
4189 self.BackendsPhpCheck(appyaml)
4190 backends_yaml = self._ParseBackendsYaml(self.basepath)
4191 rpcserver = self._GetRpcServer()
4192 response = rpcserver.Send('/api/backends/configure',
4193 app_id=appyaml.application,
4194 backend=backend,
4195 payload=backends_yaml.ToYAML())
4196 print >> self.out_fh, response
4198 def ListVersions(self):
4199 """Lists all versions for an app."""
4200 if len(self.args) == 0:
4201 if not self.options.app_id:
4202 self.parser.error('Expected <directory> argument or -A <app id>.')
4203 app_id = self.options.app_id
4204 elif len(self.args) == 1:
4205 if self.options.app_id:
4206 self.parser.error('<directory> argument is not needed with -A.')
4207 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4208 app_id = appyaml.application
4209 else:
4210 self.parser.error('Expected 1 argument, not %d.' % len(self.args))
4212 rpcserver = self._GetRpcServer()
4213 response = rpcserver.Send('/api/versions/list', app_id=app_id)
4215 parsed_response = yaml.safe_load(response)
4216 if not parsed_response:
4217 print >> self.out_fh, ('No versions uploaded for app: %s.' % app_id)
4218 else:
4219 print >> self.out_fh, response
4221 def DeleteVersion(self):
4222 """Deletes the specified version for an app."""
4223 if not (self.options.app_id and self.options.version):
4224 self.parser.error('Expected an <app_id> argument, a <version> argument '
4225 'and an optional <module> argument.')
4226 if self.options.module:
4227 module = self.options.module
4228 else:
4229 module = ''
4231 rpcserver = self._GetRpcServer()
4232 response = rpcserver.Send('/api/versions/delete',
4233 app_id=self.options.app_id,
4234 version_match=self.options.version,
4235 module=module)
4237 print >> self.out_fh, response
4239 def _LockingAction(self, url):
4240 """Changes the locking state for a given version."""
4241 if len(self.args) == 1:
4242 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4243 app_id = appyaml.application
4244 module = appyaml.module or ''
4245 version = appyaml.version
4246 elif not self.args:
4247 if not (self.options.app_id and self.options.version):
4248 self.parser.error(
4249 ('Expected a <directory> argument or both --application and '
4250 '--version flags.'))
4251 module = ''
4252 else:
4253 self._PrintHelpAndExit()
4257 if self.options.app_id:
4258 app_id = self.options.app_id
4259 if self.options.module:
4260 module = self.options.module
4261 if self.options.version:
4262 version = self.options.version
4264 rpcserver = self._GetRpcServer()
4265 DoLockAction(
4266 url,
4267 rpcserver,
4268 app_id, version, module,
4269 self.options.instance,
4270 self.out_fh).Do()
4272 def DebugAction(self):
4273 """Sets the specified version and instance for an app to be debuggable."""
4274 self._LockingAction('/api/vms/debug')
4276 def LockAction(self):
4277 """Locks the specified version and instance for an app."""
4278 self._LockingAction('/api/vms/lock')
4280 def _LockActionOptions(self, parser):
4281 """Adds lock/unlock-specific options to 'parser'.
4283 Args:
4284 parser: An instance of OptionsParser.
4286 parser.add_option('-I', '--instance', type='string', dest='instance',
4287 help='Instance to lock/unlock.')
4289 def PrepareVmRuntimeAction(self):
4290 """Prepare the application for vm runtimes and return state."""
4291 if not self.options.app_id:
4292 self.parser.error('Expected an --application argument')
4293 rpcserver = self._GetRpcServer()
4294 response = rpcserver.Send('/api/vms/prepare',
4295 app_id=self.options.app_id)
4296 print >> self.out_fh, response
4298 def _ParseAndValidateModuleYamls(self, yaml_paths):
4299 """Validates given yaml paths and returns the parsed yaml objects.
4301 Args:
4302 yaml_paths: List of paths to AppInfo yaml files.
4304 Returns:
4305 List of parsed AppInfo yamls.
4307 results = []
4308 app_id = None
4309 last_yaml_path = None
4310 for yaml_path in yaml_paths:
4311 if not os.path.isfile(yaml_path):
4312 _PrintErrorAndExit(
4313 self.error_fh,
4314 ("Error: The given path '%s' is not to a YAML configuration "
4315 "file.\n") % yaml_path)
4316 file_name = os.path.basename(yaml_path)
4317 base_path = os.path.dirname(yaml_path)
4318 if not base_path:
4319 base_path = '.'
4320 module_yaml = self._ParseAppInfoFromYaml(base_path,
4321 os.path.splitext(file_name)[0])
4323 if not module_yaml.module and file_name != 'app.yaml':
4324 _PrintErrorAndExit(
4325 self.error_fh,
4326 "Error: 'module' parameter not specified in %s" % yaml_path)
4330 if app_id is not None and module_yaml.application != app_id:
4331 _PrintErrorAndExit(
4332 self.error_fh,
4333 "Error: 'application' value '%s' in %s does not match the value "
4334 "'%s', found in %s" % (module_yaml.application,
4335 yaml_path,
4336 app_id,
4337 last_yaml_path))
4338 app_id = module_yaml.application
4339 last_yaml_path = yaml_path
4340 results.append(module_yaml)
4342 return results
4344 def _ModuleAction(self, action_path):
4345 """Process flags and yaml files and make a call to the given path.
4347 The 'start_module_version' and 'stop_module_version' actions are extremely
4348 similar in how they process input to appcfg.py and only really differ in
4349 what path they hit on the RPCServer.
4351 Args:
4352 action_path: Path on the RPCServer to send the call to.
4355 modules_to_process = []
4356 if not self.args:
4358 if not (self.options.app_id and
4359 self.options.module and
4360 self.options.version):
4361 _PrintErrorAndExit(self.error_fh,
4362 'Expected at least one <file> argument or the '
4363 '--application, --module and --version flags to'
4364 ' be set.')
4365 else:
4366 modules_to_process.append((self.options.app_id,
4367 self.options.module,
4368 self.options.version))
4369 else:
4372 if self.options.module:
4374 _PrintErrorAndExit(self.error_fh,
4375 'You may not specify a <file> argument with the '
4376 '--module flag.')
4378 module_yamls = self._ParseAndValidateModuleYamls(self.args)
4379 for serv_yaml in module_yamls:
4382 app_id = serv_yaml.application
4383 modules_to_process.append((self.options.app_id or serv_yaml.application,
4384 serv_yaml.module or appinfo.DEFAULT_MODULE,
4385 self.options.version or serv_yaml.version))
4387 rpcserver = self._GetRpcServer()
4390 for app_id, module, version in modules_to_process:
4391 response = rpcserver.Send(action_path,
4392 app_id=app_id,
4393 module=module,
4394 version=version)
4395 print >> self.out_fh, response
4397 def StartModuleVersion(self):
4398 """Starts one or more versions."""
4399 self._ModuleAction('/api/modules/start')
4401 def StopModuleVersion(self):
4402 """Stops one or more versions."""
4403 self._ModuleAction('/api/modules/stop')
4405 def Rollback(self):
4406 """Does a rollback of an existing transaction for this app version."""
4407 self._Rollback()
4409 def _RollbackOptions(self, parser):
4410 """Adds rollback-specific options to parser.
4412 Args:
4413 parser: An instance of OptionsParser.
4415 parser.add_option('--force_rollback', action='store_true',
4416 dest='force_rollback', default=False,
4417 help='Force rollback.')
4419 def _Rollback(self, backend=None):
4420 """Does a rollback of an existing transaction.
4422 Args:
4423 backend: name of a backend to rollback, or None
4425 If a backend is specified the rollback will affect only that backend, if no
4426 backend is specified the rollback will affect the current app version.
4428 if os.path.isdir(self.basepath):
4429 module_yaml = self._ParseAppInfoFromYaml(self.basepath)
4430 else:
4432 file_name = os.path.basename(self.basepath)
4433 self.basepath = os.path.dirname(self.basepath)
4434 if not self.basepath:
4435 self.basepath = '.'
4436 module_yaml = self._ParseAppInfoFromYaml(self.basepath,
4437 os.path.splitext(file_name)[0])
4439 appversion = AppVersionUpload(self._GetRpcServer(), module_yaml,
4440 module_yaml_path='app.yaml',
4441 backend=backend)
4443 appversion.in_transaction = True
4448 force_rollback = False
4449 if hasattr(self.options, 'force_rollback'):
4450 force_rollback = self.options.force_rollback
4452 appversion.Rollback(force_rollback)
4454 def SetDefaultVersion(self):
4455 """Sets the default version."""
4456 module = ''
4457 if len(self.args) == 1:
4461 stored_modules = self.options.module
4462 self.options.module = None
4463 try:
4464 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4465 finally:
4466 self.options.module = stored_modules
4468 app_id = appyaml.application
4469 module = appyaml.module or ''
4470 version = appyaml.version
4471 elif not self.args:
4472 if not (self.options.app_id and self.options.version):
4473 self.parser.error(
4474 ('Expected a <directory> argument or both --application and '
4475 '--version flags.'))
4476 else:
4477 self._PrintHelpAndExit()
4480 if self.options.app_id:
4481 app_id = self.options.app_id
4482 if self.options.module:
4483 module = self.options.module
4484 if self.options.version:
4485 version = self.options.version
4487 version_setter = DefaultVersionSet(self._GetRpcServer(),
4488 app_id,
4489 module,
4490 version,
4491 self.error_fh)
4492 version_setter.SetVersion()
4495 def MigrateTraffic(self):
4496 """Migrates traffic."""
4497 if len(self.args) == 1:
4498 appyaml = self._ParseAppInfoFromYaml(self.args[0])
4499 app_id = appyaml.application
4500 version = appyaml.version
4501 elif not self.args:
4502 if not (self.options.app_id and self.options.version):
4503 self.parser.error(
4504 ('Expected a <directory> argument or both --application and '
4505 '--version flags.'))
4506 else:
4507 self._PrintHelpAndExit()
4510 if self.options.app_id:
4511 app_id = self.options.app_id
4512 if self.options.version:
4513 version = self.options.version
4515 traffic_migrator = TrafficMigrator(
4516 self._GetRpcServer(), app_id, version, self.error_fh)
4517 traffic_migrator.MigrateTraffic()
4519 def RequestLogs(self):
4520 """Write request logs to a file."""
4522 args_length = len(self.args)
4523 module = ''
4524 if args_length == 2:
4525 appyaml = self._ParseAppInfoFromYaml(self.args.pop(0))
4526 app_id = appyaml.application
4527 module = appyaml.module or ''
4528 version = appyaml.version
4529 elif args_length == 1:
4530 if not (self.options.app_id and self.options.version):
4531 self.parser.error(
4532 ('Expected the --application and --version flags if <directory> '
4533 'argument is not specified.'))
4534 else:
4535 self._PrintHelpAndExit()
4538 if self.options.app_id:
4539 app_id = self.options.app_id
4540 if self.options.module:
4541 module = self.options.module
4542 if self.options.version:
4543 version = self.options.version
4545 if (self.options.severity is not None and
4546 not 0 <= self.options.severity <= MAX_LOG_LEVEL):
4547 self.parser.error(
4548 'Severity range is 0 (DEBUG) through %s (CRITICAL).' % MAX_LOG_LEVEL)
4550 if self.options.num_days is None:
4551 self.options.num_days = int(not self.options.append)
4553 try:
4554 end_date = self._ParseEndDate(self.options.end_date)
4555 except (TypeError, ValueError):
4556 self.parser.error('End date must be in the format YYYY-MM-DD.')
4558 rpcserver = self._GetRpcServer()
4560 logs_requester = LogsRequester(rpcserver,
4561 app_id,
4562 module,
4563 version,
4564 self.args[0],
4565 self.options.num_days,
4566 self.options.append,
4567 self.options.severity,
4568 end_date,
4569 self.options.vhost,
4570 self.options.include_vhost,
4571 self.options.include_all,
4572 time_func=self.time_func)
4573 logs_requester.DownloadLogs()
4575 @staticmethod
4576 def _ParseEndDate(date, time_func=time.time):
4577 """Translates an ISO 8601 date to a date object.
4579 Args:
4580 date: A date string as YYYY-MM-DD.
4581 time_func: A time.time() compatible function, which can be overridden for
4582 testing.
4584 Returns:
4585 A date object representing the last day of logs to get.
4586 If no date is given, returns today in the US/Pacific timezone.
4588 if not date:
4589 return PacificDate(time_func())
4590 return datetime.date(*[int(i) for i in date.split('-')])
4592 def _RequestLogsOptions(self, parser):
4593 """Adds request_logs-specific options to 'parser'.
4595 Args:
4596 parser: An instance of OptionsParser.
4598 parser.add_option('-n', '--num_days', type='int', dest='num_days',
4599 action='store', default=None,
4600 help='Number of days worth of log data to get. '
4601 'The cut-off point is midnight US/Pacific. '
4602 'Use 0 to get all available logs. '
4603 'Default is 1, unless --append is also given; '
4604 'then the default is 0.')
4605 parser.add_option('-a', '--append', dest='append',
4606 action='store_true', default=False,
4607 help='Append to existing file.')
4608 parser.add_option('--severity', type='int', dest='severity',
4609 action='store', default=None,
4610 help='Severity of app-level log messages to get. '
4611 'The range is 0 (DEBUG) through 4 (CRITICAL). '
4612 'If omitted, only request logs are returned.')
4613 parser.add_option('--vhost', type='string', dest='vhost',
4614 action='store', default=None,
4615 help='The virtual host of log messages to get. '
4616 'If omitted, all log messages are returned.')
4617 parser.add_option('--include_vhost', dest='include_vhost',
4618 action='store_true', default=False,
4619 help='Include virtual host in log messages.')
4620 parser.add_option('--include_all', dest='include_all',
4621 action='store_true', default=None,
4622 help='Include everything in log messages.')
4623 parser.add_option('--end_date', dest='end_date',
4624 action='store', default='',
4625 help='End date (as YYYY-MM-DD) of period for log data. '
4626 'Defaults to today.')
4628 def CronInfo(self, now=None, output=sys.stdout):
4629 """Displays information about cron definitions.
4631 Args:
4632 now: used for testing.
4633 output: Used for testing.
4635 if self.args:
4636 self.parser.error('Expected a single <directory> argument.')
4637 if now is None:
4638 now = datetime.datetime.utcnow()
4640 cron_yaml = self._ParseCronYaml(self.basepath)
4641 if cron_yaml and cron_yaml.cron:
4642 for entry in cron_yaml.cron:
4643 description = entry.description
4644 if not description:
4645 description = '<no description>'
4646 if not entry.timezone:
4647 entry.timezone = 'UTC'
4649 print >>output, '\n%s:\nURL: %s\nSchedule: %s (%s)' % (description,
4650 entry.url,
4651 entry.schedule,
4652 entry.timezone)
4653 if entry.timezone != 'UTC':
4654 print >>output, ('Note: Schedules with timezones won\'t be calculated'
4655 ' correctly here')
4656 schedule = groctimespecification.GrocTimeSpecification(entry.schedule)
4658 matches = schedule.GetMatches(now, self.options.num_runs)
4659 for match in matches:
4660 print >>output, '%s, %s from now' % (
4661 match.strftime('%Y-%m-%d %H:%M:%SZ'), match - now)
4663 def _CronInfoOptions(self, parser):
4664 """Adds cron_info-specific options to 'parser'.
4666 Args:
4667 parser: An instance of OptionsParser.
4669 parser.add_option('-n', '--num_runs', type='int', dest='num_runs',
4670 action='store', default=5,
4671 help='Number of runs of each cron job to display'
4672 'Default is 5')
4674 def _CheckRequiredLoadOptions(self):
4675 """Checks that upload/download options are present."""
4676 for option in ['filename']:
4677 if getattr(self.options, option) is None:
4678 self.parser.error('Option \'%s\' is required.' % option)
4679 if not self.options.url:
4680 self.parser.error('You must have google.appengine.ext.remote_api.handler '
4681 'assigned to an endpoint in app.yaml, or provide '
4682 'the url of the handler via the \'url\' option.')
4684 def InferRemoteApiUrl(self, appyaml):
4685 """Uses app.yaml to determine the remote_api endpoint.
4687 Args:
4688 appyaml: A parsed app.yaml file.
4690 Returns:
4691 The url of the remote_api endpoint as a string, or None
4694 handlers = appyaml.handlers
4695 handler_suffixes = ['remote_api/handler.py',
4696 'remote_api.handler.application']
4697 app_id = appyaml.application
4698 for handler in handlers:
4699 if hasattr(handler, 'script') and handler.script:
4700 if any(handler.script.endswith(suffix) for suffix in handler_suffixes):
4701 server = self.options.server
4702 url = handler.url
4703 if url.endswith('(/.*)?'):
4706 url = url[:-6]
4707 if server == 'appengine.google.com':
4708 return 'http://%s.appspot.com%s' % (app_id, url)
4709 else:
4710 match = re.match(PREFIXED_BY_ADMIN_CONSOLE_RE, server)
4711 if match:
4712 return 'http://%s%s%s' % (app_id, match.group(1), url)
4713 else:
4714 return 'http://%s%s' % (server, url)
4715 return None
4717 def RunBulkloader(self, arg_dict):
4718 """Invokes the bulkloader with the given keyword arguments.
4720 Args:
4721 arg_dict: Dictionary of arguments to pass to bulkloader.Run().
4724 try:
4726 import sqlite3
4727 except ImportError:
4728 logging.error('upload_data action requires SQLite3 and the python '
4729 'sqlite3 module (included in python since 2.5).')
4730 sys.exit(1)
4732 sys.exit(bulkloader.Run(arg_dict))
4734 def _SetupLoad(self):
4735 """Performs common verification and set up for upload and download."""
4737 if len(self.args) != 1 and not self.options.url:
4738 self.parser.error('Expected either --url or a single <directory> '
4739 'argument.')
4741 if len(self.args) == 1:
4742 self.basepath = self.args[0]
4743 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4745 self.options.app_id = appyaml.application
4747 if not self.options.url:
4748 url = self.InferRemoteApiUrl(appyaml)
4749 if url is not None:
4750 self.options.url = url
4752 self._CheckRequiredLoadOptions()
4754 if self.options.batch_size < 1:
4755 self.parser.error('batch_size must be 1 or larger.')
4759 if verbosity == 1:
4760 logging.getLogger().setLevel(logging.INFO)
4761 self.options.debug = False
4762 else:
4763 logging.getLogger().setLevel(logging.DEBUG)
4764 self.options.debug = True
4766 def _MakeLoaderArgs(self):
4767 """Returns a dict made from many attributes of self.options, plus others.
4769 See body for list of self.options attributes included. In addition, result
4770 includes
4771 'application' = self.options.app_id
4772 'throttle_class' = self.throttle_class
4774 Returns:
4775 A dict.
4777 args = dict([(arg_name, getattr(self.options, arg_name, None)) for
4778 arg_name in (
4779 'url',
4780 'filename',
4781 'batch_size',
4782 'kind',
4783 'num_threads',
4784 'bandwidth_limit',
4785 'rps_limit',
4786 'http_limit',
4787 'db_filename',
4788 'config_file',
4789 'auth_domain',
4790 'has_header',
4791 'loader_opts',
4792 'log_file',
4793 'passin',
4794 'email',
4795 'debug',
4796 'exporter_opts',
4797 'mapper_opts',
4798 'result_db_filename',
4799 'mapper_opts',
4800 'dry_run',
4801 'dump',
4802 'restore',
4803 'namespace',
4804 'create_config',
4806 args['application'] = self.options.app_id
4807 args['throttle_class'] = self.throttle_class
4808 return args
4810 def PerformDownload(self, run_fn=None):
4811 """Performs a datastore download via the bulkloader.
4813 Args:
4814 run_fn: Function to invoke the bulkloader, used for testing.
4816 if run_fn is None:
4817 run_fn = self.RunBulkloader
4818 self._SetupLoad()
4820 StatusUpdate('Downloading data records.', self.error_fh)
4822 args = self._MakeLoaderArgs()
4823 args['download'] = bool(args['config_file'])
4824 args['has_header'] = False
4825 args['map'] = False
4826 args['dump'] = not args['config_file']
4827 args['restore'] = False
4828 args['create_config'] = False
4830 run_fn(args)
4832 def PerformUpload(self, run_fn=None):
4833 """Performs a datastore upload via the bulkloader.
4835 Args:
4836 run_fn: Function to invoke the bulkloader, used for testing.
4838 if run_fn is None:
4839 run_fn = self.RunBulkloader
4840 self._SetupLoad()
4842 StatusUpdate('Uploading data records.', self.error_fh)
4844 args = self._MakeLoaderArgs()
4845 args['download'] = False
4846 args['map'] = False
4847 args['dump'] = False
4848 args['restore'] = not args['config_file']
4849 args['create_config'] = False
4851 run_fn(args)
4853 def CreateBulkloadConfig(self, run_fn=None):
4854 """Create a bulkloader config via the bulkloader wizard.
4856 Args:
4857 run_fn: Function to invoke the bulkloader, used for testing.
4859 if run_fn is None:
4860 run_fn = self.RunBulkloader
4861 self._SetupLoad()
4863 StatusUpdate('Creating bulkloader configuration.', self.error_fh)
4865 args = self._MakeLoaderArgs()
4866 args['download'] = False
4867 args['has_header'] = False
4868 args['map'] = False
4869 args['dump'] = False
4870 args['restore'] = False
4871 args['create_config'] = True
4873 run_fn(args)
4875 def _PerformLoadOptions(self, parser):
4876 """Adds options common to 'upload_data' and 'download_data'.
4878 Args:
4879 parser: An instance of OptionsParser.
4881 parser.add_option('--url', type='string', dest='url',
4882 action='store',
4883 help='The location of the remote_api endpoint.')
4884 parser.add_option('--batch_size', type='int', dest='batch_size',
4885 action='store', default=10,
4886 help='Number of records to post in each request.')
4887 parser.add_option('--bandwidth_limit', type='int', dest='bandwidth_limit',
4888 action='store', default=250000,
4889 help='The maximum bytes/second bandwidth for transfers.')
4890 parser.add_option('--rps_limit', type='int', dest='rps_limit',
4891 action='store', default=20,
4892 help='The maximum records/second for transfers.')
4893 parser.add_option('--http_limit', type='int', dest='http_limit',
4894 action='store', default=8,
4895 help='The maximum requests/second for transfers.')
4896 parser.add_option('--db_filename', type='string', dest='db_filename',
4897 action='store',
4898 help='Name of the progress database file.')
4899 parser.add_option('--auth_domain', type='string', dest='auth_domain',
4900 action='store', default='gmail.com',
4901 help='The name of the authorization domain to use.')
4902 parser.add_option('--log_file', type='string', dest='log_file',
4903 help='File to write bulkloader logs. If not supplied '
4904 'then a new log file will be created, named: '
4905 'bulkloader-log-TIMESTAMP.')
4906 parser.add_option('--dry_run', action='store_true',
4907 dest='dry_run', default=False,
4908 help='Do not execute any remote_api calls')
4909 parser.add_option('--namespace', type='string', dest='namespace',
4910 action='store', default='',
4911 help='Namespace to use when accessing datastore.')
4912 parser.add_option('--num_threads', type='int', dest='num_threads',
4913 action='store', default=10,
4914 help='Number of threads to transfer records with.')
4916 def _PerformUploadOptions(self, parser):
4917 """Adds 'upload_data' specific options to the 'parser' passed in.
4919 Args:
4920 parser: An instance of OptionsParser.
4922 self._PerformLoadOptions(parser)
4923 parser.add_option('--filename', type='string', dest='filename',
4924 action='store',
4925 help='The name of the file containing the input data.'
4926 ' (Required)')
4927 parser.add_option('--kind', type='string', dest='kind',
4928 action='store',
4929 help='The kind of the entities to store.')
4930 parser.add_option('--has_header', dest='has_header',
4931 action='store_true', default=False,
4932 help='Whether the first line of the input file should be'
4933 ' skipped')
4934 parser.add_option('--loader_opts', type='string', dest='loader_opts',
4935 help='A string to pass to the Loader.initialize method.')
4936 parser.add_option('--config_file', type='string', dest='config_file',
4937 action='store',
4938 help='Name of the configuration file.')
4940 def _PerformDownloadOptions(self, parser):
4941 """Adds 'download_data' specific options to the 'parser' passed in.
4943 Args:
4944 parser: An instance of OptionsParser.
4946 self._PerformLoadOptions(parser)
4947 parser.add_option('--filename', type='string', dest='filename',
4948 action='store',
4949 help='The name of the file where output data is to be'
4950 ' written. (Required)')
4951 parser.add_option('--kind', type='string', dest='kind',
4952 action='store',
4953 help='The kind of the entities to retrieve.')
4954 parser.add_option('--exporter_opts', type='string', dest='exporter_opts',
4955 help='A string to pass to the Exporter.initialize method.'
4957 parser.add_option('--result_db_filename', type='string',
4958 dest='result_db_filename',
4959 action='store',
4960 help='Database to write entities to for download.')
4961 parser.add_option('--config_file', type='string', dest='config_file',
4962 action='store',
4963 help='Name of the configuration file.')
4965 def _CreateBulkloadConfigOptions(self, parser):
4966 """Adds 'download_data' specific options to the 'parser' passed in.
4968 Args:
4969 parser: An instance of OptionsParser.
4971 self._PerformLoadOptions(parser)
4972 parser.add_option('--filename', type='string', dest='filename',
4973 action='store',
4974 help='The name of the file where the generated template'
4975 ' is to be written. (Required)')
4976 parser.add_option('--result_db_filename', type='string',
4977 dest='result_db_filename',
4978 action='store',
4979 help='Database to write entities to during config '
4980 'generation.')
4982 def ResourceLimitsInfo(self, output=None):
4983 """Outputs the current resource limits.
4985 Args:
4986 output: The file handle to write the output to (used for testing).
4988 rpcserver = self._GetRpcServer()
4989 appyaml = self._ParseAppInfoFromYaml(self.basepath)
4990 request_params = {'app_id': appyaml.application, 'version': appyaml.version}
4991 logging_context = _ClientDeployLoggingContext(rpcserver, request_params,
4992 usage_reporting=False)
4993 resource_limits = GetResourceLimits(logging_context, self.error_fh)
4996 for attr_name in sorted(resource_limits):
4997 print >>output, '%s: %s' % (attr_name, resource_limits[attr_name])
4999 class Action(object):
5000 """Contains information about a command line action.
5002 Attributes:
5003 function: The name of a function defined on AppCfg or its subclasses
5004 that will perform the appropriate action.
5005 usage: A command line usage string.
5006 short_desc: A one-line description of the action.
5007 long_desc: A detailed description of the action. Whitespace and
5008 formatting will be preserved.
5009 error_desc: An error message to display when the incorrect arguments are
5010 given.
5011 options: A function that will add extra options to a given OptionParser
5012 object.
5013 uses_basepath: Does the action use a basepath/app-directory (and hence
5014 app.yaml).
5015 hidden: Should this command be shown in the help listing.
5023 def __init__(self, function, usage, short_desc, long_desc='',
5024 error_desc=None, options=lambda obj, parser: None,
5025 uses_basepath=True, hidden=False):
5026 """Initializer for the class attributes."""
5027 self.function = function
5028 self.usage = usage
5029 self.short_desc = short_desc
5030 self.long_desc = long_desc
5031 self.error_desc = error_desc
5032 self.options = options
5033 self.uses_basepath = uses_basepath
5034 self.hidden = hidden
5036 def __call__(self, appcfg):
5037 """Invoke this Action on the specified AppCfg.
5039 This calls the function of the appropriate name on AppCfg, and
5040 respects polymophic overrides.
5042 Args:
5043 appcfg: The appcfg to use.
5044 Returns:
5045 The result of the function call.
5047 method = getattr(appcfg, self.function)
5048 return method()
5050 actions = {
5052 'help': Action(
5053 function='Help',
5054 usage='%prog help <action>',
5055 short_desc='Print help for a specific action.',
5056 uses_basepath=False),
5058 'update': Action(
5059 function='Update',
5060 usage='%prog [options] update <directory> | [file, ...]',
5061 options=_UpdateOptions,
5062 short_desc='Create or update an app version.',
5063 long_desc="""
5064 Specify a directory that contains all of the files required by
5065 the app, and appcfg.py will create/update the app version referenced
5066 in the app.yaml file at the top level of that directory. appcfg.py
5067 will follow symlinks and recursively upload all files to the server.
5068 Temporary or source control files (e.g. foo~, .svn/*) will be skipped.
5070 If you are using the Modules feature, then you may prefer to pass multiple files
5071 to update, rather than a directory, to specify which modules you would like
5072 updated."""),
5074 'download_app': Action(
5075 function='DownloadApp',
5076 usage='%prog [options] download_app -A app_id [ -V version ]'
5077 ' <out-dir>',
5078 short_desc='Download a previously-uploaded app.',
5079 long_desc="""
5080 Download a previously-uploaded app to the specified directory. The app
5081 ID is specified by the \"-A\" option. The optional version is specified
5082 by the \"-V\" option.""",
5083 uses_basepath=False),
5085 'update_cron': Action(
5086 function='UpdateCron',
5087 usage='%prog [options] update_cron <directory>',
5088 short_desc='Update application cron definitions.',
5089 long_desc="""
5090 The 'update_cron' command will update any new, removed or changed cron
5091 definitions from the optional cron.yaml file."""),
5093 'update_indexes': Action(
5094 function='UpdateIndexes',
5095 usage='%prog [options] update_indexes <directory>',
5096 short_desc='Update application indexes.',
5097 long_desc="""
5098 The 'update_indexes' command will add additional indexes which are not currently
5099 in production as well as restart any indexes that were not completed."""),
5101 'update_queues': Action(
5102 function='UpdateQueues',
5103 usage='%prog [options] update_queues <directory>',
5104 short_desc='Update application task queue definitions.',
5105 long_desc="""
5106 The 'update_queue' command will update any new, removed or changed task queue
5107 definitions from the optional queue.yaml file."""),
5109 'update_dispatch': Action(
5110 function='UpdateDispatch',
5111 usage='%prog [options] update_dispatch <directory>',
5112 short_desc='Update application dispatch definitions.',
5113 long_desc="""
5114 The 'update_dispatch' command will update any new, removed or changed dispatch
5115 definitions from the optional dispatch.yaml file."""),
5117 'update_dos': Action(
5118 function='UpdateDos',
5119 usage='%prog [options] update_dos <directory>',
5120 short_desc='Update application dos definitions.',
5121 long_desc="""
5122 The 'update_dos' command will update any new, removed or changed dos
5123 definitions from the optional dos.yaml file."""),
5125 'backends': Action(
5126 function='BackendsAction',
5127 usage='%prog [options] backends <directory> <action>',
5128 short_desc='Perform a backend action.',
5129 long_desc="""
5130 The 'backends' command will perform a backends action.""",
5131 error_desc="""\
5132 Expected a <directory> and <action> argument."""),
5134 'backends list': Action(
5135 function='BackendsList',
5136 usage='%prog [options] backends <directory> list',
5137 short_desc='List all backends configured for the app.',
5138 long_desc="""
5139 The 'backends list' command will list all backends configured for the app."""),
5141 'backends update': Action(
5142 function='BackendsUpdate',
5143 usage='%prog [options] backends <directory> update [<backend>]',
5144 options=_UpdateOptions,
5145 short_desc='Update one or more backends.',
5146 long_desc="""
5147 The 'backends update' command updates one or more backends. This command
5148 updates backend configuration settings and deploys new code to the server. Any
5149 existing instances will stop and be restarted. Updates all backends, or a
5150 single backend if the <backend> argument is provided."""),
5152 'backends rollback': Action(
5153 function='BackendsRollback',
5154 usage='%prog [options] backends <directory> rollback <backend>',
5155 short_desc='Roll back an update of a backend.',
5156 long_desc="""
5157 The 'backends update' command requires a server-side transaction.
5158 Use 'backends rollback' if you experience an error during 'backends update'
5159 and want to start the update over again."""),
5161 'backends start': Action(
5162 function='BackendsStart',
5163 usage='%prog [options] backends <directory> start <backend>',
5164 short_desc='Start a backend.',
5165 long_desc="""
5166 The 'backends start' command will put a backend into the START state."""),
5168 'backends stop': Action(
5169 function='BackendsStop',
5170 usage='%prog [options] backends <directory> stop <backend>',
5171 short_desc='Stop a backend.',
5172 long_desc="""
5173 The 'backends start' command will put a backend into the STOP state."""),
5175 'backends delete': Action(
5176 function='BackendsDelete',
5177 usage='%prog [options] backends <directory> delete <backend>',
5178 short_desc='Delete a backend.',
5179 long_desc="""
5180 The 'backends delete' command will delete a backend."""),
5182 'backends configure': Action(
5183 function='BackendsConfigure',
5184 usage='%prog [options] backends <directory> configure <backend>',
5185 short_desc='Reconfigure a backend without stopping it.',
5186 long_desc="""
5187 The 'backends configure' command performs an online update of a backend, without
5188 stopping instances that are currently running. No code or handlers are updated,
5189 only certain configuration settings specified in backends.yaml. Valid settings
5190 are: instances, options: public, and options: failfast."""),
5192 'vacuum_indexes': Action(
5193 function='VacuumIndexes',
5194 usage='%prog [options] vacuum_indexes <directory>',
5195 options=_VacuumIndexesOptions,
5196 short_desc='Delete unused indexes from application.',
5197 long_desc="""
5198 The 'vacuum_indexes' command will help clean up indexes which are no longer
5199 in use. It does this by comparing the local index configuration with
5200 indexes that are actually defined on the server. If any indexes on the
5201 server do not exist in the index configuration file, the user is given the
5202 option to delete them."""),
5204 'rollback': Action(
5205 function='Rollback',
5206 usage='%prog [options] rollback <directory> | <file>',
5207 options=_RollbackOptions,
5208 short_desc='Rollback an in-progress update.',
5209 long_desc="""
5210 The 'update' command requires a server-side transaction.
5211 Use 'rollback' if you experience an error during 'update'
5212 and want to begin a new update transaction."""),
5214 'request_logs': Action(
5215 function='RequestLogs',
5216 usage='%prog [options] request_logs [<directory>] <output_file>',
5217 options=_RequestLogsOptions,
5218 uses_basepath=False,
5219 short_desc='Write request logs in Apache common log format.',
5220 long_desc="""
5221 The 'request_logs' command exports the request logs from your application
5222 to a file. It will write Apache common log format records ordered
5223 chronologically. If output file is '-' stdout will be written.""",
5224 error_desc="""\
5225 Expected an optional <directory> and mandatory <output_file> argument."""),
5227 'cron_info': Action(
5228 function='CronInfo',
5229 usage='%prog [options] cron_info <directory>',
5230 options=_CronInfoOptions,
5231 short_desc='Display information about cron jobs.',
5232 long_desc="""
5233 The 'cron_info' command will display the next 'number' runs (default 5) for
5234 each cron job defined in the cron.yaml file."""),
5236 'start_module_version': Action(
5237 function='StartModuleVersion',
5238 uses_basepath=False,
5239 usage='%prog [options] start_module_version [file, ...]',
5240 short_desc='Start a module version.',
5241 long_desc="""
5242 The 'start_module_version' command will put a module version into the START
5243 state."""),
5245 'stop_module_version': Action(
5246 function='StopModuleVersion',
5247 uses_basepath=False,
5248 usage='%prog [options] stop_module_version [file, ...]',
5249 short_desc='Stop a module version.',
5250 long_desc="""
5251 The 'stop_module_version' command will put a module version into the STOP
5252 state."""),
5258 'upload_data': Action(
5259 function='PerformUpload',
5260 usage='%prog [options] upload_data <directory>',
5261 options=_PerformUploadOptions,
5262 short_desc='Upload data records to datastore.',
5263 long_desc="""
5264 The 'upload_data' command translates input records into datastore entities and
5265 uploads them into your application's datastore.""",
5266 uses_basepath=False),
5268 'download_data': Action(
5269 function='PerformDownload',
5270 usage='%prog [options] download_data <directory>',
5271 options=_PerformDownloadOptions,
5272 short_desc='Download entities from datastore.',
5273 long_desc="""
5274 The 'download_data' command downloads datastore entities and writes them to
5275 file as CSV or developer defined format.""",
5276 uses_basepath=False),
5278 'create_bulkloader_config': Action(
5279 function='CreateBulkloadConfig',
5280 usage='%prog [options] create_bulkload_config <directory>',
5281 options=_CreateBulkloadConfigOptions,
5282 short_desc='Create a bulkloader.yaml from a running application.',
5283 long_desc="""
5284 The 'create_bulkloader_config' command creates a bulkloader.yaml configuration
5285 template for use with upload_data or download_data.""",
5286 uses_basepath=False),
5289 'set_default_version': Action(
5290 function='SetDefaultVersion',
5291 usage='%prog [options] set_default_version [directory]',
5292 short_desc='Set the default (serving) version.',
5293 long_desc="""
5294 The 'set_default_version' command sets the default (serving) version of the app.
5295 Defaults to using the application, version and module specified in app.yaml;
5296 use the --application, --version and --module flags to override these values.
5297 The --module flag can also be a comma-delimited string of several modules. (ex.
5298 module1,module2,module3) In this case, the default version of each module will
5299 be changed to the version specified.
5301 The 'migrate_traffic' command can be thought of as a safer version of this
5302 command.""",
5303 uses_basepath=False),
5305 'migrate_traffic': Action(
5306 function='MigrateTraffic',
5307 usage='%prog [options] migrate_traffic [directory]',
5308 short_desc='Migrates traffic to another version.',
5309 long_desc="""
5310 The 'migrate_traffic' command gradually gradually sends an increasing fraction
5311 of traffic your app's traffic from the current default version to another
5312 version. Once all traffic has been migrated, the new version is set as the
5313 default version.
5315 app.yaml specifies the target application, version, and (optionally) module; use
5316 the --application, --version and --module flags to override these values.
5318 Can be thought of as an enhanced version of the 'set_default_version'
5319 command.""",
5321 uses_basepath=False,
5323 hidden=True),
5325 'resource_limits_info': Action(
5326 function='ResourceLimitsInfo',
5327 usage='%prog [options] resource_limits_info <directory>',
5328 short_desc='Get the resource limits.',
5329 long_desc="""
5330 The 'resource_limits_info' command prints the current resource limits that
5331 are enforced."""),
5333 'list_versions': Action(
5334 function='ListVersions',
5335 usage='%prog [options] list_versions [directory]',
5336 short_desc='List all uploaded versions for an app.',
5337 long_desc="""
5338 The 'list_versions' command outputs the uploaded versions for each module of
5339 an application in YAML. The YAML is in formatted as an associative array,
5340 mapping module_ids to the list of versions uploaded for that module. The
5341 default version will be first in the list.""",
5342 uses_basepath=False),
5344 'delete_version': Action(
5345 function='DeleteVersion',
5346 usage='%prog [options] delete_version -A app_id -V version '
5347 '[-M module]',
5348 uses_basepath=False,
5349 short_desc='Delete the specified version for an app.',
5350 long_desc="""
5351 The 'delete_version' command deletes the specified version for the specified
5352 application."""),
5354 'debug': Action(
5355 function='DebugAction',
5356 usage='%prog [options] debug [-A app_id] [-V version]'
5357 ' [-M module] [-I instance] [directory]',
5358 options=_LockActionOptions,
5359 short_desc='Debug a vm runtime application.',
5360 hidden=True,
5361 uses_basepath=False,
5362 long_desc="""
5363 The 'debug' command configures a vm runtime application to be accessable
5364 for debugging."""),
5366 'lock': Action(
5367 function='LockAction',
5368 usage='%prog [options] lock [-A app_id] [-V version]'
5369 ' [-M module] [-I instance] [directory]',
5370 options=_LockActionOptions,
5371 short_desc='Lock a debugged vm runtime application.',
5372 hidden=True,
5373 uses_basepath=False,
5374 long_desc="""
5375 The 'lock' command relocks a debugged vm runtime application."""),
5377 'prepare_vm_runtime': Action(
5378 function='PrepareVmRuntimeAction',
5379 usage='%prog [options] prepare_vm_runtime -A app_id',
5380 short_desc='Prepare an application for the VM runtime.',
5381 hidden=True,
5382 uses_basepath=False,
5383 long_desc="""
5384 The 'prepare_vm_runtime' prepares an application for the VM runtime."""),
5388 def main(argv):
5389 logging.basicConfig(format=('%(asctime)s %(levelname)s %(filename)s:'
5390 '%(lineno)s %(message)s '))
5391 try:
5392 result = AppCfgApp(argv).Run()
5393 if result:
5394 sys.exit(result)
5395 except KeyboardInterrupt:
5396 StatusUpdate('Interrupted.')
5397 sys.exit(1)
5400 if __name__ == '__main__':
5401 main(sys.argv)