3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 """Tool for deploying apps to an app server.
23 Currently, the application only uploads new appversions. To do this, it first
24 walks the directory tree rooted at the path the user specifies, adding all the
25 files it finds to a list. It then uploads the application configuration
26 (app.yaml) to the server using HTTP, followed by uploading each of the files.
27 It then commits the transaction with another request.
29 The bulk of this work is handled by the AppVersionUpload class, which exposes
30 methods to add to the list of files, fetch a list of modified files, upload
31 files, and commit or rollback the transaction.
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 croninfo
64 from google
.appengine
.api
import dispatchinfo
65 from google
.appengine
.api
import dosinfo
66 from google
.appengine
.api
import queueinfo
67 from google
.appengine
.api
import yaml_errors
68 from google
.appengine
.api
import yaml_object
69 from google
.appengine
.datastore
import datastore_index
70 from google
.appengine
.tools
import appengine_rpc
74 from google
.appengine
.tools
import appengine_rpc_httplib2
76 appengine_rpc_httplib2
= None
77 from google
.appengine
.tools
import bulkloader
78 from google
.appengine
.tools
import sdk_update_checker
83 BACKENDS_ACTION
= 'backends'
89 MAX_BATCH_SIZE
= 3200000
91 MAX_BATCH_FILE_SIZE
= 200000
102 PREFIXED_BY_ADMIN_CONSOLE_RE
= '^(?:admin-console)(.*)'
105 SDK_PRODUCT
= 'appcfg_py'
111 SUPPORTED_RUNTIMES
= ('go', 'python', 'python27')
117 MILLION
= 1000 * 1000
118 DEFAULT_RESOURCE_LIMITS
= {
119 'max_file_size': 32 * MILLION
,
120 'max_blob_size': 32 * MILLION
,
121 'max_files_to_clone': 100,
122 'max_total_file_size': 150 * MEGA
,
123 'max_file_count': 10000,
126 # Client ID and secrets are managed in the Google API console.
132 APPCFG_CLIENT_ID
= '550516889912.apps.googleusercontent.com'
133 APPCFG_CLIENT_NOTSOSECRET
= 'ykPq-0UYfKNprLRjVx1hBBar'
134 APPCFG_SCOPES
= ['https://www.googleapis.com/auth/appengine.admin']
137 STATIC_FILE_PREFIX
= '__static__'
140 class Error(Exception):
144 class OAuthNotAvailable(Error
):
145 """The appengine_rpc_httplib2 module could not be imported."""
149 class CannotStartServingError(Error
):
150 """We could not start serving the version being uploaded."""
154 def PrintUpdate(msg
):
155 """Print a message to stderr.
157 If 'verbosity' is greater than 0, print the message.
160 msg: The string to print.
163 timestamp
= datetime
.datetime
.now()
164 print >>sys
.stderr
, '%s' % datetime
.datetime
.now().strftime('%I:%M %p'),
165 print >>sys
.stderr
, msg
168 def StatusUpdate(msg
):
169 """Print a status message to stderr."""
173 def ErrorUpdate(msg
):
174 """Print an error message to stderr."""
178 def _PrintErrorAndExit(stream
, msg
, exit_code
=2):
179 """Prints the given error message and exists the program.
182 stream: The stream (e.g. StringIO or file) to write the message to.
183 msg: The error message to display as a string.
184 exit_code: The integer code to pass to sys.exit().
190 class FileClassification(object):
191 """A class to hold a file's classification.
193 This class both abstracts away the details of how we determine
194 whether a file is a regular, static or error file as well as acting
195 as a container for various metadata about the file.
198 def __init__(self
, config
, filename
):
199 """Initializes a FileClassification instance.
202 config: The app.yaml object to check the filename against.
203 filename: The name of the file.
205 self
.__static
_mime
_type
= self
.__GetMimeTypeIfStaticFile
(config
, filename
)
206 self
.__static
_app
_readable
= self
.__GetAppReadableIfStaticFile
(config
,
208 self
.__error
_mime
_type
, self
.__error
_code
= self
.__LookupErrorBlob
(config
,
212 def __GetMimeTypeIfStaticFile(config
, filename
):
213 """Looks up the mime type for 'filename'.
215 Uses the handlers in 'config' to determine if the file should
216 be treated as a static file.
219 config: The app.yaml object to check the filename against.
220 filename: The name of the file.
223 The mime type string. For example, 'text/plain' or 'image/gif'.
224 None if this is not a static file.
226 for handler
in config
.handlers
:
227 handler_type
= handler
.GetHandlerType()
228 if handler_type
in ('static_dir', 'static_files'):
229 if handler_type
== 'static_dir':
230 regex
= os
.path
.join(re
.escape(handler
.GetHandler()), '.*')
232 regex
= handler
.upload
233 if re
.match(regex
, filename
):
234 return handler
.mime_type
or FileClassification
.__MimeType
(filename
)
238 def __GetAppReadableIfStaticFile(config
, filename
):
239 """Looks up whether a static file is readable by the application.
241 Uses the handlers in 'config' to determine if the file should
242 be treated as a static file and if so, if the file should be readable by the
246 config: The AppInfoExternal object to check the filename against.
247 filename: The name of the file.
250 True if the file is static and marked as app readable, False otherwise.
252 for handler
in config
.handlers
:
253 handler_type
= handler
.GetHandlerType()
254 if handler_type
in ('static_dir', 'static_files'):
255 if handler_type
== 'static_dir':
256 regex
= os
.path
.join(re
.escape(handler
.GetHandler()), '.*')
258 regex
= handler
.upload
259 if re
.match(regex
, filename
):
260 return handler
.application_readable
264 def __LookupErrorBlob(config
, filename
):
265 """Looks up the mime type and error_code for 'filename'.
267 Uses the error handlers in 'config' to determine if the file should
268 be treated as an error blob.
271 config: The app.yaml object to check the filename against.
272 filename: The name of the file.
276 A tuple of (mime_type, error_code), or (None, None) if this is not an
277 error blob. For example, ('text/plain', default) or ('image/gif',
278 timeout) or (None, None).
280 if not config
.error_handlers
:
282 for error_handler
in config
.error_handlers
:
283 if error_handler
.file == filename
:
284 error_code
= error_handler
.error_code
285 error_code
= error_code
or 'default'
286 if error_handler
.mime_type
:
287 return (error_handler
.mime_type
, error_code
)
289 return (FileClassification
.__MimeType
(filename
), error_code
)
293 def __MimeType(filename
, default
='application/octet-stream'):
294 guess
= mimetypes
.guess_type(filename
)[0]
296 print >>sys
.stderr
, ('Could not guess mimetype for %s. Using %s.'
297 % (filename
, default
))
301 def IsApplicationFile(self
):
302 return bool((not self
.IsStaticFile() or self
.__static
_app
_readable
) and
303 not self
.IsErrorFile())
305 def IsStaticFile(self
):
306 return bool(self
.__static
_mime
_type
)
308 def StaticMimeType(self
):
309 return self
.__static
_mime
_type
311 def IsErrorFile(self
):
312 return bool(self
.__error
_mime
_type
)
314 def ErrorMimeType(self
):
315 return self
.__error
_mime
_type
318 return self
.__error
_code
321 def BuildClonePostBody(file_tuples
):
322 """Build the post body for the /api/clone{files,blobs,errorblobs} urls.
325 file_tuples: A list of tuples. Each tuple should contain the entries
326 appropriate for the endpoint in question.
329 A string containing the properly delimited tuples.
332 for tup
in file_tuples
:
335 file_list
.append(TUPLE_DELIMITER
.join([path
] + list(tup
)))
336 return LIST_DELIMITER
.join(file_list
)
339 def GetRemoteResourceLimits(rpcserver
, config
):
340 """Get the resource limit as reported by the admin console.
342 Get the resource limits by querying the admin_console/appserver. The
343 actual limits returned depends on the server we are talking to and
344 could be missing values we expect or include extra values.
347 rpcserver: The RPC server to use.
348 config: The appyaml configuration.
354 StatusUpdate('Getting current resource limits.')
355 yaml_data
= rpcserver
.Send('/api/appversion/getresourcelimits',
356 app_id
=config
.application
,
357 version
=config
.version
)
359 except urllib2
.HTTPError
, err
:
367 return yaml
.safe_load(yaml_data
)
370 def GetResourceLimits(rpcserver
, config
):
371 """Gets the resource limits.
373 Gets the resource limits that should be applied to apps. Any values
374 that the server does not know about will have their default value
375 reported (although it is also possible for the server to report
376 values we don't know about).
379 rpcserver: The RPC server to use.
380 config: The appyaml configuration.
385 resource_limits
= DEFAULT_RESOURCE_LIMITS
.copy()
386 resource_limits
.update(GetRemoteResourceLimits(rpcserver
, config
))
387 logging
.debug('Using resource limits: %s', resource_limits
)
388 return resource_limits
391 def RetryWithBackoff(callable_func
, retry_notify_func
,
392 initial_delay
=1, backoff_factor
=2,
393 max_delay
=60, max_tries
=20):
394 """Calls a function multiple times, backing off more and more each time.
397 callable_func: A function that performs some operation that should be
398 retried a number of times up on failure. Signature: () -> (done, value)
399 If 'done' is True, we'll immediately return (True, value)
400 If 'done' is False, we'll delay a bit and try again, unless we've
401 hit the 'max_tries' limit, in which case we'll return (False, value).
402 retry_notify_func: This function will be called immediately before the
403 next retry delay. Signature: (value, delay) -> None
404 'value' is the value returned by the last call to 'callable_func'
405 'delay' is the retry delay, in seconds
406 initial_delay: Initial delay after first try, in seconds.
407 backoff_factor: Delay will be multiplied by this factor after each try.
408 max_delay: Maximum delay, in seconds.
409 max_tries: Maximum number of tries (the first one counts).
412 What the last call to 'callable_func' returned, which is of the form
413 (done, value). If 'done' is True, you know 'callable_func' returned True
414 before we ran out of retries. If 'done' is False, you know 'callable_func'
415 kept returning False and we ran out of retries.
418 Whatever the function raises--an exception will immediately stop retries.
421 delay
= initial_delay
425 done
, opaque_value
= callable_func()
429 return True, opaque_value
431 if num_tries
>= max_tries
:
432 return False, opaque_value
434 retry_notify_func(opaque_value
, delay
)
436 delay
= min(delay
* backoff_factor
, max_delay
)
439 def MigratePython27Notice():
440 """Tells the user that Python 2.5 runtime is deprecated.
442 Encourages the user to migrate from Python 2.5 to Python 2.7.
444 Prints a message to sys.stdout. The caller should have tested that the user is
445 using Python 2.5, so as not to spuriously display this message.
448 'WARNING: This application is using the Python 2.5 runtime, which is '
449 'deprecated! It should be updated to the Python 2.7 runtime as soon as '
450 'possible, which offers performance improvements and many new features. '
451 'Learn how simple it is to migrate your application to Python 2.7 at '
452 'https://developers.google.com/appengine/docs/python/python25/migrate27.')
455 class IndexDefinitionUpload(object):
456 """Provides facilities to upload index definitions to the hosting service."""
458 def __init__(self
, rpcserver
, config
, definitions
):
459 """Creates a new DatastoreIndexUpload.
462 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
464 config: The AppInfoExternal object derived from the app.yaml file.
465 definitions: An IndexDefinitions object.
467 self
.rpcserver
= rpcserver
469 self
.definitions
= definitions
472 """Uploads the index definitions."""
473 StatusUpdate('Uploading index definitions.')
474 self
.rpcserver
.Send('/api/datastore/index/add',
475 app_id
=self
.config
.application
,
476 version
=self
.config
.version
,
477 payload
=self
.definitions
.ToYAML())
480 class CronEntryUpload(object):
481 """Provides facilities to upload cron entries to the hosting service."""
483 def __init__(self
, rpcserver
, config
, cron
):
484 """Creates a new CronEntryUpload.
487 rpcserver: The RPC server to use. Should be an instance of a subclass of
489 config: The AppInfoExternal object derived from the app.yaml file.
490 cron: The CronInfoExternal object loaded from the cron.yaml file.
492 self
.rpcserver
= rpcserver
497 """Uploads the cron entries."""
498 StatusUpdate('Uploading cron entries.')
499 self
.rpcserver
.Send('/api/cron/update',
500 app_id
=self
.config
.application
,
501 version
=self
.config
.version
,
502 payload
=self
.cron
.ToYAML())
505 class QueueEntryUpload(object):
506 """Provides facilities to upload task queue entries to the hosting service."""
508 def __init__(self
, rpcserver
, config
, queue
):
509 """Creates a new QueueEntryUpload.
512 rpcserver: The RPC server to use. Should be an instance of a subclass of
514 config: The AppInfoExternal object derived from the app.yaml file.
515 queue: The QueueInfoExternal object loaded from the queue.yaml file.
517 self
.rpcserver
= rpcserver
522 """Uploads the task queue entries."""
523 StatusUpdate('Uploading task queue entries.')
524 self
.rpcserver
.Send('/api/queue/update',
525 app_id
=self
.config
.application
,
526 version
=self
.config
.version
,
527 payload
=self
.queue
.ToYAML())
530 class DosEntryUpload(object):
531 """Provides facilities to upload dos entries to the hosting service."""
533 def __init__(self
, rpcserver
, config
, dos
):
534 """Creates a new DosEntryUpload.
537 rpcserver: The RPC server to use. Should be an instance of a subclass of
539 config: The AppInfoExternal object derived from the app.yaml file.
540 dos: The DosInfoExternal object loaded from the dos.yaml file.
542 self
.rpcserver
= rpcserver
547 """Uploads the dos entries."""
548 StatusUpdate('Uploading DOS entries.')
549 self
.rpcserver
.Send('/api/dos/update',
550 app_id
=self
.config
.application
,
551 version
=self
.config
.version
,
552 payload
=self
.dos
.ToYAML())
555 class PagespeedEntryUpload(object):
556 """Provides facilities to upload pagespeed configs to the hosting service."""
558 def __init__(self
, rpcserver
, config
, pagespeed
):
559 """Creates a new PagespeedEntryUpload.
562 rpcserver: The RPC server to use. Should be an instance of a subclass of
564 config: The AppInfoExternal object derived from the app.yaml file.
565 pagespeed: The PagespeedEntry object from config.
567 self
.rpcserver
= rpcserver
569 self
.pagespeed
= pagespeed
572 """Uploads the pagespeed entries."""
576 StatusUpdate('Uploading PageSpeed configuration.')
577 pagespeed_yaml
= self
.pagespeed
.ToYAML()
579 self
.rpcserver
.Send('/api/appversion/updatepagespeed',
580 app_id
=self
.config
.application
,
581 version
=self
.config
.version
,
582 payload
=pagespeed_yaml
)
583 except urllib2
.HTTPError
, err
:
593 if err
.code
!= 404 or self
.pagespeed
is not None:
597 class DefaultVersionSet(object):
598 """Provides facilities to set the default (serving) version."""
600 def __init__(self
, rpcserver
, app_id
, server
, version
):
601 """Creates a new DefaultVersionSet.
604 rpcserver: The RPC server to use. Should be an instance of a subclass of
606 app_id: The application to make the change to.
607 server: The server to set the default version of (if any).
608 version: The version to set as the default.
610 self
.rpcserver
= rpcserver
613 self
.version
= version
615 def SetVersion(self
):
616 """Sets the default version."""
618 StatusUpdate('Setting default version of server %s of application %s '
619 'to %s.' % (self
.app_id
, self
.server
, self
.version
))
621 StatusUpdate('Setting default version of application %s to %s.'
622 % (self
.app_id
, self
.version
))
623 self
.rpcserver
.Send('/api/appversion/setdefault',
626 version
=self
.version
)
629 class IndexOperation(object):
630 """Provide facilities for writing Index operation commands."""
632 def __init__(self
, rpcserver
, config
):
633 """Creates a new IndexOperation.
636 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
638 config: appinfo.AppInfoExternal configuration object.
640 self
.rpcserver
= rpcserver
643 def DoDiff(self
, definitions
):
644 """Retrieve diff file from the server.
647 definitions: datastore_index.IndexDefinitions as loaded from users
651 A pair of datastore_index.IndexDefinitions objects. The first record
652 is the set of indexes that are present in the index.yaml file but missing
653 from the server. The second record is the set of indexes that are
654 present on the server but missing from the index.yaml file (indicating
655 that these indexes should probably be vacuumed).
657 StatusUpdate('Fetching index definitions diff.')
658 response
= self
.rpcserver
.Send('/api/datastore/index/diff',
659 app_id
=self
.config
.application
,
660 payload
=definitions
.ToYAML())
661 return datastore_index
.ParseMultipleIndexDefinitions(response
)
663 def DoDelete(self
, definitions
):
664 """Delete indexes from the server.
667 definitions: Index definitions to delete from datastore.
670 A single datstore_index.IndexDefinitions containing indexes that were
671 not deleted, probably because they were already removed. This may
672 be normal behavior as there is a potential race condition between fetching
673 the index-diff and sending deletion confirmation through.
675 StatusUpdate('Deleting selected index definitions.')
676 response
= self
.rpcserver
.Send('/api/datastore/index/delete',
677 app_id
=self
.config
.application
,
678 payload
=definitions
.ToYAML())
679 return datastore_index
.ParseIndexDefinitions(response
)
682 class VacuumIndexesOperation(IndexOperation
):
683 """Provide facilities to request the deletion of datastore indexes."""
685 def __init__(self
, rpcserver
, config
, force
,
686 confirmation_fn
=raw_input):
687 """Creates a new VacuumIndexesOperation.
690 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
692 config: appinfo.AppInfoExternal configuration object.
693 force: True to force deletion of indexes, else False.
694 confirmation_fn: Function used for getting input form user.
696 super(VacuumIndexesOperation
, self
).__init
__(rpcserver
, config
)
698 self
.confirmation_fn
= confirmation_fn
700 def GetConfirmation(self
, index
):
701 """Get confirmation from user to delete an index.
703 This method will enter an input loop until the user provides a
704 response it is expecting. Valid input is one of three responses:
706 y: Confirm deletion of index.
707 n: Do not delete index.
708 a: Delete all indexes without asking for further confirmation.
710 If the user enters nothing at all, the default action is to skip
711 that index and do not delete.
713 If the user selects 'a', as a side effect, the 'force' flag is set.
716 index: Index to confirm.
719 True if user enters 'y' or 'a'. False if user enter 'n'.
723 print 'This index is no longer defined in your index.yaml file.'
729 confirmation
= self
.confirmation_fn(
730 'Are you sure you want to delete this index? (N/y/a): ')
731 confirmation
= confirmation
.strip().lower()
734 if confirmation
== 'y':
736 elif confirmation
== 'n' or not confirmation
:
738 elif confirmation
== 'a':
742 print 'Did not understand your response.'
744 def DoVacuum(self
, definitions
):
745 """Vacuum indexes in datastore.
747 This method will query the server to determine which indexes are not
748 being used according to the user's local index.yaml file. Once it has
749 made this determination, it confirms with the user which unused indexes
750 should be deleted. Once confirmation for each index is receives, it
751 deletes those indexes.
753 Because another user may in theory delete the same indexes at the same
754 time as the user, there is a potential race condition. In this rare cases,
755 some of the indexes previously confirmed for deletion will not be found.
756 The user is notified which indexes these were.
759 definitions: datastore_index.IndexDefinitions as loaded from users
763 unused_new_indexes
, notused_indexes
= self
.DoDiff(definitions
)
766 deletions
= datastore_index
.IndexDefinitions(indexes
=[])
767 if notused_indexes
.indexes
is not None:
768 for index
in notused_indexes
.indexes
:
769 if self
.force
or self
.GetConfirmation(index
):
770 deletions
.indexes
.append(index
)
773 if deletions
.indexes
:
774 not_deleted
= self
.DoDelete(deletions
)
777 if not_deleted
.indexes
:
778 not_deleted_count
= len(not_deleted
.indexes
)
779 if not_deleted_count
== 1:
780 warning_message
= ('An index was not deleted. Most likely this is '
781 'because it no longer exists.\n\n')
783 warning_message
= ('%d indexes were not deleted. Most likely this '
784 'is because they no longer exist.\n\n'
786 for index
in not_deleted
.indexes
:
787 warning_message
+= index
.ToYAML()
788 logging
.warning(warning_message
)
791 class LogsRequester(object):
792 """Provide facilities to export request logs."""
807 time_func
=time
.time
):
811 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
813 app_id: The application to fetch logs from.
814 server: The server of the app to fetch logs from, optional.
815 version_id: The version of the app to fetch logs for.
816 output_file: Output file name.
817 num_days: Number of days worth of logs to export; 0 for all available.
818 append: True if appending to an existing file.
819 severity: App log severity to request (0-4); None for no app logs.
820 end: date object representing last day of logs to return.
821 vhost: The virtual host of log messages to get. None for all hosts.
822 include_vhost: If true, the virtual host is included in log messages.
823 include_all: If true, we add to the log message everything we know
825 time_func: Method that return a timestamp representing now (for testing).
828 self
.rpcserver
= rpcserver
830 self
.output_file
= output_file
832 self
.num_days
= num_days
833 self
.severity
= severity
835 self
.include_vhost
= include_vhost
836 self
.include_all
= include_all
839 self
.version_id
= version_id
841 self
.write_mode
= 'w'
843 self
.sentinel
= FindSentinel(self
.output_file
)
844 self
.write_mode
= 'a'
847 self
.skip_until
= False
848 now
= PacificDate(time_func())
850 self
.skip_until
= end
855 self
.valid_dates
= None
857 start
= end
- datetime
.timedelta(self
.num_days
- 1)
858 self
.valid_dates
= (start
, end
)
860 def DownloadLogs(self
):
861 """Download the requested logs.
863 This will write the logs to the file designated by
864 self.output_file, or to stdout if the filename is '-'.
865 Multiple roundtrips to the server may be made.
868 StatusUpdate('Downloading request logs for app %s server %s version %s.' %
869 (self
.app_id
, self
.server
, self
.version_id
))
871 StatusUpdate('Downloading request logs for app %s version %s.' %
872 (self
.app_id
, self
.version_id
))
878 tf
= tempfile
.TemporaryFile()
883 new_offset
= self
.RequestLogLines(tf
, last_offset
)
884 if not new_offset
or new_offset
== last_offset
:
886 last_offset
= new_offset
887 except KeyboardInterrupt:
888 StatusUpdate('Keyboard interrupt; saving data downloaded so far.')
890 StatusUpdate('Copying request logs to %r.' % self
.output_file
)
891 if self
.output_file
== '-':
895 of
= open(self
.output_file
, self
.write_mode
)
897 StatusUpdate('Can\'t write %r: %s.' % (self
.output_file
, err
))
900 line_count
= CopyReversedLines(tf
, of
)
903 if of
is not sys
.stdout
:
907 StatusUpdate('Copied %d records.' % line_count
)
909 def RequestLogLines(self
, tf
, offset
):
910 """Make a single roundtrip to the server.
913 tf: Writable binary stream to which the log lines returned by
914 the server are written, stripped of headers, and excluding
915 lines skipped due to self.sentinel or self.valid_dates filtering.
916 offset: Offset string for a continued request; None for the first.
919 The offset string to be used for the next request, if another
920 request should be issued; or None, if not.
922 logging
.info('Request with offset %r.', offset
)
923 kwds
= {'app_id': self
.app_id
,
924 'version': self
.version_id
,
928 kwds
['server'] = self
.server
930 kwds
['offset'] = offset
931 if self
.severity
is not None:
932 kwds
['severity'] = str(self
.severity
)
933 if self
.vhost
is not None:
934 kwds
['vhost'] = str(self
.vhost
)
935 if self
.include_vhost
is not None:
936 kwds
['include_vhost'] = str(self
.include_vhost
)
937 if self
.include_all
is not None:
938 kwds
['include_all'] = str(self
.include_all
)
939 response
= self
.rpcserver
.Send('/api/request_logs', payload
=None, **kwds
)
940 response
= response
.replace('\r', '\0')
941 lines
= response
.splitlines()
942 logging
.info('Received %d bytes, %d records.', len(response
), len(lines
))
944 if lines
and lines
[0].startswith('#'):
945 match
= re
.match(r
'^#\s*next_offset=(\S+)\s*$', lines
[0])
948 offset
= match
.group(1)
949 if lines
and lines
[-1].startswith('#'):
952 valid_dates
= self
.valid_dates
953 sentinel
= self
.sentinel
954 skip_until
= self
.skip_until
957 len_sentinel
= len(sentinel
)
960 line
.startswith(sentinel
) and
961 line
[len_sentinel
: len_sentinel
+1] in ('', '\0')):
964 linedate
= DateOfLogLine(line
)
970 if linedate
> skip_until
:
974 self
.skip_until
= skip_until
= False
976 if valid_dates
and not valid_dates
[0] <= linedate
<= valid_dates
[1]:
978 tf
.write(line
+ '\n')
984 def DateOfLogLine(line
):
985 """Returns a date object representing the log line's timestamp.
988 line: a log line string.
990 A date object representing the timestamp or None if parsing fails.
992 m
= re
.compile(r
'[^[]+\[(\d+/[A-Za-z]+/\d+):[^\d]*').match(line
)
996 return datetime
.date(*time
.strptime(m
.group(1), '%d/%b/%Y')[:3])
1001 def PacificDate(now
):
1002 """For a UTC timestamp, return the date in the US/Pacific timezone.
1005 now: A posix timestamp giving current UTC time.
1008 A date object representing what day it is in the US/Pacific timezone.
1011 return datetime
.date(*time
.gmtime(PacificTime(now
))[:3])
1014 def PacificTime(now
):
1015 """Helper to return the number of seconds between UTC and Pacific time.
1017 This is needed to compute today's date in Pacific time (more
1018 specifically: Mountain View local time), which is how request logs
1019 are reported. (Google servers always report times in Mountain View
1020 local time, regardless of where they are physically located.)
1022 This takes (post-2006) US DST into account. Pacific time is either
1023 8 hours or 7 hours west of UTC, depending on whether DST is in
1024 effect. Since 2007, US DST starts on the Second Sunday in March
1025 March, and ends on the first Sunday in November. (Reference:
1026 http://aa.usno.navy.mil/faq/docs/daylight_time.php.)
1028 Note that the server doesn't report its local time (the HTTP Date
1029 header uses UTC), and the client's local time is irrelevant.
1032 now: A posix timestamp giving current UTC time.
1035 A pseudo-posix timestamp giving current Pacific time. Passing
1036 this through time.gmtime() will produce a tuple in Pacific local
1040 if IsPacificDST(now
):
1045 def IsPacificDST(now
):
1046 """Helper for PacificTime to decide whether now is Pacific DST (PDT).
1049 now: A pseudo-posix timestamp giving current time in PST.
1052 True if now falls within the range of DST, False otherwise.
1054 pst
= time
.gmtime(now
)
1058 begin
= calendar
.timegm((year
, 3, 8, 2, 0, 0, 0, 0, 0))
1059 while time
.gmtime(begin
).tm_wday
!= SUNDAY
:
1062 end
= calendar
.timegm((year
, 11, 1, 2, 0, 0, 0, 0, 0))
1063 while time
.gmtime(end
).tm_wday
!= SUNDAY
:
1065 return begin
<= now
< end
1068 def CopyReversedLines(instream
, outstream
, blocksize
=2**16):
1069 r
"""Copy lines from input stream to output stream in reverse order.
1071 As a special feature, null bytes in the input are turned into
1072 newlines followed by tabs in the output, but these 'sub-lines'
1073 separated by null bytes are not reversed. E.g. If the input is
1074 'A\0B\nC\0D\n', the output is 'C\n\tD\nA\n\tB\n'.
1077 instream: A seekable stream open for reading in binary mode.
1078 outstream: A stream open for writing; doesn't have to be seekable or binary.
1079 blocksize: Optional block size for buffering, for unit testing.
1082 The number of lines copied.
1086 last_block
= instream
.tell() // blocksize
1088 for iblock
in xrange(last_block
+ 1, -1, -1):
1089 instream
.seek(iblock
* blocksize
)
1090 data
= instream
.read(blocksize
)
1091 lines
= data
.splitlines(True)
1092 lines
[-1:] = ''.join(lines
[-1:] + [spillover
]).splitlines(True)
1093 if lines
and not lines
[-1].endswith('\n'):
1097 if lines
and iblock
> 0:
1098 spillover
= lines
.pop()
1100 line_count
+= len(lines
)
1101 data
= ''.join(lines
).replace('\0', '\n\t')
1102 outstream
.write(data
)
1106 def FindSentinel(filename
, blocksize
=2**16):
1107 """Return the sentinel line from the output file.
1110 filename: The filename of the output file. (We'll read this file.)
1111 blocksize: Optional block size for buffering, for unit testing.
1114 The contents of the last line in the file that doesn't start with
1115 a tab, with its trailing newline stripped; or None if the file
1116 couldn't be opened or no such line could be found by inspecting
1117 the last 'blocksize' bytes of the file.
1120 StatusUpdate('Can\'t combine --append with output to stdout.')
1123 fp
= open(filename
, 'rb')
1124 except IOError, err
:
1125 StatusUpdate('Append mode disabled: can\'t read %r: %s.' % (filename
, err
))
1129 fp
.seek(max(0, fp
.tell() - blocksize
))
1130 lines
= fp
.readlines()
1134 if not line
.startswith('\t'):
1138 StatusUpdate('Append mode disabled: can\'t find sentinel in %r.' %
1141 return sentinel
.rstrip('\n')
1146 class UploadBatcher(object):
1147 """Helper to batch file uploads."""
1149 def __init__(self
, what
, rpcserver
, params
):
1153 what: Either 'file' or 'blob' or 'errorblob' indicating what kind of
1154 objects this batcher uploads. Used in messages and URLs.
1155 rpcserver: The RPC server.
1156 params: A dictionary object containing URL params to add to HTTP requests.
1158 assert what
in ('file', 'blob', 'errorblob'), repr(what
)
1160 self
.params
= params
1161 self
.rpcserver
= rpcserver
1162 self
.single_url
= '/api/appversion/add' + what
1163 self
.batch_url
= self
.single_url
+ 's'
1164 self
.batching
= True
1168 def SendBatch(self
):
1169 """Send the current batch on its way.
1171 If successful, resets self.batch and self.batch_size.
1174 HTTPError with code=404 if the server doesn't support batching.
1176 boundary
= 'boundary'
1178 for path
, payload
, mime_type
in self
.batch
:
1179 while boundary
in payload
:
1180 boundary
+= '%04x' % random
.randint(0, 0xffff)
1181 assert len(boundary
) < 80, 'Unexpected error, please try again.'
1182 part
= '\n'.join(['',
1183 'X-Appcfg-File: %s' % urllib
.quote(path
),
1184 'X-Appcfg-Hash: %s' % _Hash(payload
),
1185 'Content-Type: %s' % mime_type
,
1186 'Content-Length: %d' % len(payload
),
1187 'Content-Transfer-Encoding: 8bit',
1193 'MIME-Version: 1.0\n'
1194 'Content-Type: multipart/mixed; boundary="%s"\n'
1196 'This is a message with multiple parts in MIME format.' %
1198 parts
.append('--\n')
1199 delimiter
= '\n--%s' % boundary
1200 payload
= delimiter
.join(parts
)
1201 logging
.info('Uploading batch of %d %ss to %s with boundary="%s".',
1202 len(self
.batch
), self
.what
, self
.batch_url
, boundary
)
1203 self
.rpcserver
.Send(self
.batch_url
,
1205 content_type
='message/rfc822',
1210 def SendSingleFile(self
, path
, payload
, mime_type
):
1211 """Send a single file on its way."""
1212 logging
.info('Uploading %s %s (%s bytes, type=%s) to %s.',
1213 self
.what
, path
, len(payload
), mime_type
, self
.single_url
)
1214 self
.rpcserver
.Send(self
.single_url
,
1216 content_type
=mime_type
,
1221 """Flush the current batch.
1223 This first attempts to send the batch as a single request; if that
1224 fails because the server doesn't support batching, the files are
1225 sent one by one, and self.batching is reset to False.
1227 At the end, self.batch and self.batch_size are reset.
1233 except urllib2
.HTTPError
, err
:
1238 logging
.info('Old server detected; turning off %s batching.', self
.what
)
1239 self
.batching
= False
1242 for path
, payload
, mime_type
in self
.batch
:
1243 self
.SendSingleFile(path
, payload
, mime_type
)
1249 def AddToBatch(self
, path
, payload
, mime_type
):
1250 """Batch a file, possibly flushing first, or perhaps upload it directly.
1253 path: The name of the file.
1254 payload: The contents of the file.
1255 mime_type: The MIME Content-type of the file, or None.
1257 If mime_type is None, application/octet-stream is substituted.
1260 mime_type
= 'application/octet-stream'
1262 if size
<= MAX_BATCH_FILE_SIZE
:
1263 if (len(self
.batch
) >= MAX_BATCH_COUNT
or
1264 self
.batch_size
+ size
> MAX_BATCH_SIZE
):
1267 logging
.info('Adding %s %s (%s bytes, type=%s) to batch.',
1268 self
.what
, path
, size
, mime_type
)
1269 self
.batch
.append((path
, payload
, mime_type
))
1270 self
.batch_size
+= size
+ BATCH_OVERHEAD
1272 self
.SendSingleFile(path
, payload
, mime_type
)
1276 """Return a string representation of a hash.
1278 The hash is a sha1 hash. It is computed both for files that need to be
1279 pushed to App Engine and for data payloads of requests made to App Engine.
1285 The string representation of the hash.
1287 return '%s_%s_%s_%s_%s' % (h
[0:8], h
[8:16], h
[16:24], h
[24:32], h
[32:40])
1291 """Compute the sha1 hash of the content.
1294 content: The data to hash as a string.
1297 The string representation of the hash.
1299 h
= hashlib
.sha1(content
).hexdigest()
1300 return _FormatHash(h
)
1303 def _HashFromFileHandle(file_handle
):
1304 """Compute the hash of the content of the file pointed to by file_handle.
1307 file_handle: File-like object which provides seek, read and tell.
1310 The string representation of the hash.
1319 pos
= file_handle
.tell()
1320 content_hash
= _Hash(file_handle
.read())
1321 file_handle
.seek(pos
, 0)
1325 def EnsureDir(path
):
1326 """Makes sure that a directory exists at the given path.
1328 If a directory already exists at that path, nothing is done.
1329 Otherwise, try to create a directory at that path with os.makedirs.
1330 If that fails, propagate the resulting OSError exception.
1333 path: The path that you want to refer to a directory.
1337 except OSError, exc
:
1340 if not (exc
.errno
== errno
.EEXIST
and os
.path
.isdir(path
)):
1344 def DoDownloadApp(rpcserver
, out_dir
, app_id
, server
, app_version
):
1345 """Downloads the files associated with a particular app version.
1348 rpcserver: The RPC server to use to download.
1349 out_dir: The directory the files should be downloaded to.
1350 app_id: The app ID of the app whose files we want to download.
1351 server: The server we want to download from. Can be:
1352 - None: We'll download from the default server.
1353 - <server>: We'll download from the specified server.
1354 app_version: The version number we want to download. Can be:
1355 - None: We'll download the latest default version.
1356 - <major>: We'll download the latest minor version.
1357 - <major>/<minor>: We'll download that exact version.
1360 StatusUpdate('Fetching file list...')
1362 url_args
= {'app_id': app_id
}
1364 url_args
['server'] = server
1365 if app_version
is not None:
1366 url_args
['version_match'] = app_version
1368 result
= rpcserver
.Send('/api/files/list', **url_args
)
1370 StatusUpdate('Fetching files...')
1372 lines
= result
.splitlines()
1375 logging
.error('Invalid response from server: empty')
1378 full_version
= lines
[0]
1379 file_lines
= lines
[1:]
1381 current_file_number
= 0
1382 num_files
= len(file_lines
)
1386 for line
in file_lines
:
1387 parts
= line
.split('|', 2)
1389 logging
.error('Invalid response from server: expecting '
1390 '"<id>|<size>|<path>", found: "%s"\n', line
)
1393 current_file_number
+= 1
1395 file_id
, size_str
, path
= parts
1397 size
= int(size_str
)
1399 logging
.error('Invalid file list entry from server: invalid size: '
1403 StatusUpdate('[%d/%d] %s' % (current_file_number
, num_files
, path
))
1406 """A request to /api/files/get which works with the RetryWithBackoff."""
1408 contents
= rpcserver
.Send('/api/files/get', app_id
=app_id
,
1409 version
=full_version
, id=file_id
)
1410 return True, contents
1411 except urllib2
.HTTPError
, exc
:
1419 def PrintRetryMessage(_
, delay
):
1420 StatusUpdate('Server busy. Will try again in %d seconds.' % delay
)
1422 success
, contents
= RetryWithBackoff(TryGet
, PrintRetryMessage
)
1424 logging
.error('Unable to download file "%s".', path
)
1428 if len(contents
) != size
:
1429 logging
.error('File "%s": server listed as %d bytes but served '
1430 '%d bytes.', path
, size
, len(contents
))
1433 full_path
= os
.path
.join(out_dir
, path
)
1435 if os
.path
.exists(full_path
):
1436 logging
.error('Unable to create file "%s": path conflicts with '
1437 'an existing file or directory', path
)
1441 full_dir
= os
.path
.dirname(full_path
)
1444 except OSError, exc
:
1445 logging
.error('Couldn\'t create directory "%s": %s', full_dir
, exc
)
1450 out_file
= open(full_path
, 'wb')
1451 except IOError, exc
:
1452 logging
.error('Couldn\'t open file "%s": %s', full_path
, exc
)
1458 out_file
.write(contents
)
1459 except IOError, exc
:
1460 logging
.error('Couldn\'t write to file "%s": %s', full_path
, exc
)
1467 logging
.error('Number of errors: %d. See output for details.', num_errors
)
1470 class AppVersionUpload(object):
1471 """Provides facilities to upload a new appversion to the hosting service.
1474 rpcserver: The AbstractRpcServer to use for the upload.
1475 config: The AppInfoExternal object derived from the app.yaml file.
1476 app_id: The application string from 'config'.
1477 version: The version string from 'config'.
1478 backend: The backend to update, if any.
1479 files: A dictionary of files to upload to the rpcserver, mapping path to
1480 hash of the file contents.
1481 in_transaction: True iff a transaction with the server has started.
1482 An AppVersionUpload can do only one transaction at a time.
1483 deployed: True iff the Deploy method has been called.
1484 started: True iff the StartServing method has been called.
1487 def __init__(self
, rpcserver
, config
, server_yaml_path
='app.yaml',
1490 """Creates a new AppVersionUpload.
1493 rpcserver: The RPC server to use. Should be an instance of HttpRpcServer
1495 config: An AppInfoExternal object that specifies the configuration for
1497 server_yaml_path: The (string) path to the yaml file corresponding to
1498 <config>, relative to the bundle directory.
1499 backend: If specified, indicates the update applies to the given backend.
1500 The backend name must match an entry in the backends: stanza.
1501 error_fh: Unexpected HTTPErrors are printed to this file handle.
1503 self
.rpcserver
= rpcserver
1504 self
.config
= config
1505 self
.app_id
= self
.config
.application
1506 self
.server
= self
.config
.server
1507 self
.backend
= backend
1508 self
.error_fh
= error_fh
or sys
.stderr
1510 self
.version
= self
.config
.version
1514 self
.params
['app_id'] = self
.app_id
1516 self
.params
['server'] = self
.server
1518 self
.params
['backend'] = self
.backend
1520 self
.params
['version'] = self
.version
1528 self
.all_files
= set()
1530 self
.in_transaction
= False
1531 self
.deployed
= False
1532 self
.started
= False
1533 self
.batching
= True
1534 self
.file_batcher
= UploadBatcher('file', self
.rpcserver
, self
.params
)
1535 self
.blob_batcher
= UploadBatcher('blob', self
.rpcserver
, self
.params
)
1536 self
.errorblob_batcher
= UploadBatcher('errorblob', self
.rpcserver
,
1539 if not self
.config
.vm_settings
:
1540 self
.config
.vm_settings
= appinfo
.VmSettings()
1541 self
.config
.vm_settings
['server_yaml_path'] = server_yaml_path
1543 def Send(self
, url
, payload
=''):
1544 """Sends a request to the server, with common params."""
1545 logging
.info('Send: %s, params=%s', url
, self
.params
)
1546 return self
.rpcserver
.Send(url
, payload
=payload
, **self
.params
)
1548 def AddFile(self
, path
, file_handle
):
1549 """Adds the provided file to the list to be pushed to the server.
1552 path: The path the file should be uploaded as.
1553 file_handle: A stream containing data to upload.
1555 assert not self
.in_transaction
, 'Already in a transaction.'
1556 assert file_handle
is not None
1558 reason
= appinfo
.ValidFilename(path
)
1560 logging
.error(reason
)
1563 content_hash
= _HashFromFileHandle(file_handle
)
1565 self
.files
[path
] = content_hash
1566 self
.all_files
.add(path
)
1569 """Returns a string describing the object being updated."""
1570 result
= 'app: %s' % self
.app_id
1571 if self
.server
is not None and self
.server
!= appinfo
.DEFAULT_SERVER
:
1572 result
+= ', server: %s' % self
.server
1574 result
+= ', backend: %s' % self
.backend
1576 result
+= ', version: %s' % self
.version
1580 def _ValidateBeginYaml(resp
):
1581 """Validates the given /api/appversion/create response string."""
1582 response_dict
= yaml
.safe_load(resp
)
1583 if not response_dict
or 'warnings' not in response_dict
:
1585 return response_dict
1588 """Begins the transaction, returning a list of files that need uploading.
1590 All calls to AddFile must be made before calling Begin().
1593 A list of pathnames for files that should be uploaded using UploadFile()
1594 before Commit() can be called.
1596 assert not self
.in_transaction
, 'Already in a transaction.'
1601 config_copy
= copy
.deepcopy(self
.config
)
1602 for url
in config_copy
.handlers
:
1603 handler_type
= url
.GetHandlerType()
1604 if url
.application_readable
:
1605 if handler_type
== 'static_dir':
1606 url
.static_dir
= os
.path
.join(STATIC_FILE_PREFIX
, url
.static_dir
)
1607 elif handler_type
== 'static_files':
1608 url
.static_files
= os
.path
.join(STATIC_FILE_PREFIX
, url
.static_files
)
1609 url
.upload
= os
.path
.join(STATIC_FILE_PREFIX
, url
.upload
)
1611 response
= self
.Send(
1612 '/api/appversion/create',
1613 payload
=config_copy
.ToYAML())
1615 result
= self
._ValidateBeginYaml
(response
)
1617 warnings
= result
.get('warnings')
1618 for warning
in warnings
:
1619 StatusUpdate('WARNING: %s' % warning
)
1621 self
.in_transaction
= True
1626 for path
, content_hash
in self
.files
.iteritems():
1627 file_classification
= FileClassification(self
.config
, path
)
1629 if file_classification
.IsStaticFile():
1631 if file_classification
.IsApplicationFile():
1632 upload_path
= os
.path
.join(STATIC_FILE_PREFIX
, path
)
1633 blobs_to_clone
.append((path
, upload_path
, content_hash
,
1634 file_classification
.StaticMimeType()))
1638 if file_classification
.IsErrorFile():
1642 errorblobs
[path
] = content_hash
1644 if file_classification
.IsApplicationFile():
1645 files_to_clone
.append((path
, path
, content_hash
))
1647 files_to_upload
= {}
1649 def CloneFiles(url
, files
, file_type
):
1650 """Sends files to the given url.
1653 url: the server URL to use.
1654 files: a list of files
1655 file_type: the type of the files
1660 StatusUpdate('Cloning %d %s file%s.' %
1661 (len(files
), file_type
, len(files
) != 1 and 's' or ''))
1663 max_files
= self
.resource_limits
['max_files_to_clone']
1664 for i
in xrange(0, len(files
), max_files
):
1665 if i
> 0 and i
% max_files
== 0:
1666 StatusUpdate('Cloned %d files.' % i
)
1668 chunk
= files
[i
:min(len(files
), i
+ max_files
)]
1669 result
= self
.Send(url
, payload
=BuildClonePostBody(chunk
))
1672 for f
in result
.split(LIST_DELIMITER
):
1674 real_path
, upload_path
= entry
[:2]
1675 if f
== upload_path
:
1676 to_upload
[real_path
] = self
.files
[real_path
]
1678 files_to_upload
.update(to_upload
)
1680 CloneFiles('/api/appversion/cloneblobs', blobs_to_clone
, 'static')
1681 CloneFiles('/api/appversion/clonefiles', files_to_clone
, 'application')
1683 logging
.debug('Files to upload: %s', files_to_upload
)
1685 for (path
, content_hash
) in errorblobs
.iteritems():
1686 files_to_upload
[path
] = content_hash
1687 self
.files
= files_to_upload
1688 return sorted(files_to_upload
.iterkeys())
1690 def UploadFile(self
, path
, file_handle
):
1691 """Uploads a file to the hosting service.
1693 Must only be called after Begin().
1694 The path provided must be one of those that were returned by Begin().
1697 path: The path the file is being uploaded as.
1698 file_handle: A file-like object containing the data to upload.
1701 KeyError: The provided file is not amongst those to be uploaded.
1703 assert self
.in_transaction
, 'Begin() must be called before UploadFile().'
1704 if path
not in self
.files
:
1705 raise KeyError('File \'%s\' is not in the list of files to be uploaded.'
1708 del self
.files
[path
]
1710 file_classification
= FileClassification(self
.config
, path
)
1711 payload
= file_handle
.read()
1712 if file_classification
.IsStaticFile():
1714 if file_classification
.IsApplicationFile():
1715 upload_path
= os
.path
.join(STATIC_FILE_PREFIX
, path
)
1716 self
.blob_batcher
.AddToBatch(upload_path
, payload
,
1717 file_classification
.StaticMimeType())
1721 if file_classification
.IsErrorFile():
1724 self
.errorblob_batcher
.AddToBatch(file_classification
.ErrorCode(),
1726 file_classification
.ErrorMimeType())
1728 if file_classification
.IsApplicationFile():
1730 self
.file_batcher
.AddToBatch(path
, payload
, None)
1732 def Precompile(self
):
1733 """Handle bytecode precompilation."""
1735 StatusUpdate('Compilation starting.')
1738 if self
.config
.runtime
== 'go':
1741 for f
in self
.all_files
:
1742 if f
.endswith('.go') and not self
.config
.nobuild_files
.match(f
):
1747 StatusUpdate('Compilation: %d files left.' % len(files
))
1748 files
= self
.PrecompileBatch(files
)
1751 StatusUpdate('Compilation completed.')
1753 def PrecompileBatch(self
, files
):
1754 """Precompile a batch of files.
1757 files: Either an empty list (for the initial request) or a list
1758 of files to be precompiled.
1761 Either an empty list (if no more files need to be precompiled)
1762 or a list of files to be precompiled subsequently.
1764 payload
= LIST_DELIMITER
.join(files
)
1765 response
= self
.Send('/api/appversion/precompile', payload
=payload
)
1768 return response
.split(LIST_DELIMITER
)
1771 """Commits the transaction, making the new app version available.
1773 All the files returned by Begin() must have been uploaded with UploadFile()
1774 before Commit() can be called.
1776 This tries the new 'deploy' method; if that fails it uses the old 'commit'.
1779 An appinfo.AppInfoSummary if one was returned from the Deploy, None
1783 Exception: Some required files were not uploaded.
1785 assert self
.in_transaction
, 'Begin() must be called before Commit().'
1787 raise Exception('Not all required files have been uploaded.')
1789 def PrintRetryMessage(_
, delay
):
1790 StatusUpdate('Will check again in %s seconds.' % delay
)
1794 app_summary
= self
.Deploy()
1797 success
, unused_contents
= RetryWithBackoff(
1798 lambda: (self
.IsReady(), None), PrintRetryMessage
, 1, 2, 60, 20)
1801 logging
.warning('Version still not ready to serve, aborting.')
1802 raise Exception('Version not ready.')
1804 result
= self
.StartServing()
1808 self
.in_transaction
= False
1811 raise CannotStartServingError(
1812 'Another operation on this version is in progress.')
1813 success
, unused_contents
= RetryWithBackoff(
1814 lambda: (self
.IsServing(), None), PrintRetryMessage
, 1, 2, 60, 20)
1817 logging
.warning('Version still not serving, aborting.')
1818 raise Exception('Version not ready.')
1819 self
.in_transaction
= False
1824 """Deploys the new app version but does not make it default.
1826 All the files returned by Begin() must have been uploaded with UploadFile()
1827 before Deploy() can be called.
1830 An appinfo.AppInfoSummary if one was returned from the Deploy, None
1834 Exception: Some required files were not uploaded.
1836 assert self
.in_transaction
, 'Begin() must be called before Deploy().'
1838 raise Exception('Not all required files have been uploaded.')
1840 StatusUpdate('Starting deployment.')
1841 result
= self
.Send('/api/appversion/deploy')
1842 self
.deployed
= True
1845 return yaml_object
.BuildSingleObject(appinfo
.AppInfoSummary
, result
)
1850 """Check if the new app version is ready to serve traffic.
1853 Exception: Deploy has not yet been called.
1856 True if the server returned the app is ready to serve.
1858 assert self
.deployed
, 'Deploy() must be called before IsReady().'
1860 StatusUpdate('Checking if deployment succeeded.')
1861 result
= self
.Send('/api/appversion/isready')
1862 return result
== '1'
1864 def StartServing(self
):
1865 """Start serving with the newly created version.
1868 Exception: Deploy has not yet been called.
1871 The response body, as a string.
1873 assert self
.deployed
, 'Deploy() must be called before StartServing().'
1875 StatusUpdate('Deployment successful.')
1876 self
.params
['willcheckserving'] = '1'
1877 result
= self
.Send('/api/appversion/startserving')
1878 del self
.params
['willcheckserving']
1883 def _ValidateIsServingYaml(resp
):
1884 """Validates the given /isserving YAML string.
1886 Returns the resulting dictionary if the response is valid.
1888 response_dict
= yaml
.safe_load(resp
)
1889 if 'serving' not in response_dict
:
1891 return response_dict
1893 def IsServing(self
):
1894 """Check if the new app version is serving.
1897 Exception: Deploy has not yet been called.
1900 True if the deployed app version is serving.
1902 assert self
.started
, 'StartServing() must be called before IsServing().'
1904 StatusUpdate('Checking if updated app version is serving.')
1906 self
.params
['new_serving_resp'] = '1'
1907 result
= self
.Send('/api/appversion/isserving')
1908 del self
.params
['new_serving_resp']
1909 if result
in ['0', '1']:
1910 return result
== '1'
1911 result
= AppVersionUpload
._ValidateIsServingYaml
(result
)
1913 raise CannotStartServingError(
1914 'Internal error: Could not parse IsServing response.')
1915 message
= result
.get('message')
1916 fatal
= result
.get('fatal')
1918 StatusUpdate(message
)
1920 raise CannotStartServingError(fatal
)
1921 return result
['serving']
1924 """Rolls back the transaction if one is in progress."""
1925 if not self
.in_transaction
:
1927 StatusUpdate('Rolling back the update.')
1928 self
.Send('/api/appversion/rollback')
1929 self
.in_transaction
= False
1932 def DoUpload(self
, paths
, openfunc
):
1933 """Uploads a new appversion with the given config and files to the server.
1936 paths: An iterator that yields the relative paths of the files to upload.
1937 openfunc: A function that takes a path and returns a file-like object.
1940 An appinfo.AppInfoSummary if one was returned from the server, None
1943 logging
.info('Reading app configuration.')
1945 StatusUpdate('\nStarting update of %s' % self
.Describe())
1950 self
.resource_limits
= GetResourceLimits(self
.rpcserver
, self
.config
)
1952 StatusUpdate('Scanning files on local disk.')
1955 file_handle
= openfunc(path
)
1956 file_classification
= FileClassification(self
.config
, path
)
1958 file_length
= GetFileLength(file_handle
)
1959 if file_classification
.IsApplicationFile():
1960 max_size
= self
.resource_limits
['max_file_size']
1962 max_size
= self
.resource_limits
['max_blob_size']
1963 if file_length
> max_size
:
1964 logging
.error('Ignoring file \'%s\': Too long '
1965 '(max %d bytes, file is %d bytes)',
1966 path
, max_size
, file_length
)
1968 logging
.info('Processing file \'%s\'', path
)
1969 self
.AddFile(path
, file_handle
)
1973 if num_files
% 500 == 0:
1974 StatusUpdate('Scanned %d files.' % num_files
)
1975 except KeyboardInterrupt:
1976 logging
.info('User interrupted. Aborting.')
1978 except EnvironmentError, e
:
1979 logging
.error('An error occurred processing file \'%s\': %s. Aborting.',
1986 missing_files
= self
.Begin()
1988 StatusUpdate('Uploading %d files and blobs.' % len(missing_files
))
1990 for missing_file
in missing_files
:
1991 file_handle
= openfunc(missing_file
)
1993 self
.UploadFile(missing_file
, file_handle
)
1997 if num_files
% 500 == 0:
1998 StatusUpdate('Processed %d out of %s.' %
1999 (num_files
, len(missing_files
)))
2001 self
.file_batcher
.Flush()
2002 self
.blob_batcher
.Flush()
2003 self
.errorblob_batcher
.Flush()
2004 StatusUpdate('Uploaded %d files and blobs' % num_files
)
2007 if (self
.config
.derived_file_type
and
2008 appinfo
.PYTHON_PRECOMPILED
in self
.config
.derived_file_type
):
2011 except urllib2
.HTTPError
, e
:
2013 ErrorUpdate('Error %d: --- begin server output ---\n'
2014 '%s\n--- end server output ---' %
2015 (e
.code
, e
.read().rstrip('\n')))
2016 if e
.code
== 422 or self
.config
.runtime
== 'go':
2024 print >>self
.error_fh
, (
2025 'Precompilation failed. Your app can still serve but may '
2026 'have reduced startup performance. You can retry the update '
2027 'later to retry the precompilation step.')
2030 app_summary
= self
.Commit()
2031 StatusUpdate('Completed update of %s' % self
.Describe())
2033 except KeyboardInterrupt:
2035 logging
.info('User interrupted. Aborting.')
2038 except urllib2
.HTTPError
, err
:
2040 logging
.info('HTTP Error (%s)', err
)
2043 except CannotStartServingError
, err
:
2045 logging
.error(err
.message
)
2049 logging
.exception('An unexpected error occurred. Aborting.')
2053 logging
.info('Done!')
2057 def FileIterator(base
, skip_files
, runtime
, separator
=os
.path
.sep
):
2058 """Walks a directory tree, returning all the files. Follows symlinks.
2061 base: The base path to search for files under.
2062 skip_files: A regular expression object for files/directories to skip.
2063 runtime: The name of the runtime e.g. "python". If "python27" then .pyc
2064 files with matching .py files will be skipped.
2065 separator: Path separator used by the running system's platform.
2068 Paths of files found, relative to base.
2072 current_dir
= dirs
.pop()
2073 entries
= set(os
.listdir(os
.path
.join(base
, current_dir
)))
2074 for entry
in sorted(entries
):
2075 name
= os
.path
.join(current_dir
, entry
)
2076 fullname
= os
.path
.join(base
, name
)
2081 if separator
== '\\':
2082 name
= name
.replace('\\', '/')
2084 if runtime
== 'python27' and not skip_files
.match(name
):
2085 root
, extension
= os
.path
.splitext(entry
)
2086 if extension
== '.pyc' and (root
+ '.py') in entries
:
2087 logging
.warning('Ignoring file \'%s\': Cannot upload both '
2088 '<filename>.py and <filename>.pyc', name
)
2091 if os
.path
.isfile(fullname
):
2092 if skip_files
.match(name
):
2093 logging
.info('Ignoring file \'%s\': File matches ignore regex.', name
)
2096 elif os
.path
.isdir(fullname
):
2097 if skip_files
.match(name
):
2099 'Ignoring directory \'%s\': Directory matches ignore regex.',
2105 def GetFileLength(fh
):
2106 """Returns the length of the file represented by fh.
2108 This function is capable of finding the length of any seekable stream,
2109 unlike os.fstat, which only works on file streams.
2112 fh: The stream to get the length of.
2115 The length of the stream.
2125 def GetUserAgent(get_version
=sdk_update_checker
.GetVersionObject
,
2126 get_platform
=appengine_rpc
.GetPlatformToken
,
2127 sdk_product
=SDK_PRODUCT
):
2128 """Determines the value of the 'User-agent' header to use for HTTP requests.
2130 If the 'APPCFG_SDK_NAME' environment variable is present, that will be
2131 used as the first product token in the user-agent.
2134 get_version: Used for testing.
2135 get_platform: Used for testing.
2136 sdk_product: Used as part of sdk/version product token.
2139 String containing the 'user-agent' header value, which includes the SDK
2140 version, the platform information, and the version of Python;
2141 e.g., 'appcfg_py/1.0.1 Darwin/9.2.0 Python/2.5.2'.
2146 sdk_name
= os
.environ
.get('APPCFG_SDK_NAME')
2148 product_tokens
.append(sdk_name
)
2150 version
= get_version()
2154 release
= version
['release']
2156 product_tokens
.append('%s/%s' % (sdk_product
, release
))
2159 product_tokens
.append(get_platform())
2162 python_version
= '.'.join(str(i
) for i
in sys
.version_info
)
2163 product_tokens
.append('Python/%s' % python_version
)
2165 return ' '.join(product_tokens
)
2168 def GetSourceName(get_version
=sdk_update_checker
.GetVersionObject
):
2169 """Gets the name of this source version."""
2170 version
= get_version()
2174 release
= version
['release']
2175 return 'Google-appcfg-%s' % (release
,)
2178 class AppCfgApp(object):
2179 """Singleton class to wrap AppCfg tool functionality.
2181 This class is responsible for parsing the command line and executing
2182 the desired action on behalf of the user. Processing files and
2183 communicating with the server is handled by other classes.
2186 actions: A dictionary mapping action names to Action objects.
2187 action: The Action specified on the command line.
2188 parser: An instance of optparse.OptionParser.
2189 options: The command line options parsed by 'parser'.
2190 argv: The original command line as a list.
2191 args: The positional command line args left over after parsing the options.
2192 raw_input_fn: Function used for getting raw user input, like email.
2193 password_input_fn: Function used for getting user password.
2194 error_fh: Unexpected HTTPErrors are printed to this file handle.
2196 Attributes for testing:
2197 parser_class: The class to use for parsing the command line. Because
2198 OptionsParser will exit the program when there is a parse failure, it
2199 is nice to subclass OptionsParser and catch the error before exiting.
2202 def __init__(self
, argv
, parser_class
=optparse
.OptionParser
,
2203 rpc_server_class
=None,
2204 raw_input_fn
=raw_input,
2205 password_input_fn
=getpass
.getpass
,
2207 error_fh
=sys
.stderr
,
2208 update_check_class
=sdk_update_checker
.SDKUpdateChecker
,
2209 throttle_class
=None,
2211 file_iterator
=FileIterator
,
2212 time_func
=time
.time
,
2213 wrap_server_error_message
=True,
2214 oauth_client_id
=APPCFG_CLIENT_ID
,
2215 oauth_client_secret
=APPCFG_CLIENT_NOTSOSECRET
,
2216 oauth_scopes
=APPCFG_SCOPES
):
2217 """Initializer. Parses the cmdline and selects the Action to use.
2219 Initializes all of the attributes described in the class docstring.
2220 Prints help or error messages if there is an error parsing the cmdline.
2223 argv: The list of arguments passed to this program.
2224 parser_class: Options parser to use for this application.
2225 rpc_server_class: RPC server class to use for this application.
2226 raw_input_fn: Function used for getting user email.
2227 password_input_fn: Function used for getting user password.
2228 out_fh: All normal output is printed to this file handle.
2229 error_fh: Unexpected HTTPErrors are printed to this file handle.
2230 update_check_class: sdk_update_checker.SDKUpdateChecker class (can be
2231 replaced for testing).
2232 throttle_class: A class to use instead of ThrottledHttpRpcServer
2233 (only used in the bulkloader).
2234 opener: Function used for opening files.
2235 file_iterator: Callable that takes (basepath, skip_files, file_separator)
2236 and returns a generator that yields all filenames in the file tree
2237 rooted at that path, skipping files that match the skip_files compiled
2239 time_func: Function which provides the current time (can be replaced for
2241 wrap_server_error_message: If true, the error messages from
2242 urllib2.HTTPError exceptions in Run() are wrapped with
2243 '--- begin server output ---' and '--- end server output ---',
2244 otherwise the error message is printed as is.
2245 oauth_client_id: The client ID of the project providing Auth. Defaults to
2246 the SDK default project client ID, the constant APPCFG_CLIENT_ID.
2247 oauth_client_secret: The client secret of the project providing Auth.
2248 Defaults to the SDK default project client secret, the constant
2249 APPCFG_CLIENT_NOTSOSECRET.
2250 oauth_scopes: The scope or set of scopes to be accessed by the OAuth2
2251 token retrieved. Defaults to APPCFG_SCOPES. Can be a string or list of
2252 strings, representing the scope(s) to request.
2254 self
.parser_class
= parser_class
2256 self
.rpc_server_class
= rpc_server_class
2257 self
.raw_input_fn
= raw_input_fn
2258 self
.password_input_fn
= password_input_fn
2259 self
.out_fh
= out_fh
2260 self
.error_fh
= error_fh
2261 self
.update_check_class
= update_check_class
2262 self
.throttle_class
= throttle_class
2263 self
.time_func
= time_func
2264 self
.wrap_server_error_message
= wrap_server_error_message
2265 self
.oauth_client_id
= oauth_client_id
2266 self
.oauth_client_secret
= oauth_client_secret
2267 self
.oauth_scopes
= oauth_scopes
2273 self
.parser
= self
._GetOptionParser
()
2274 for action
in self
.actions
.itervalues():
2275 action
.options(self
, self
.parser
)
2278 self
.options
, self
.args
= self
.parser
.parse_args(argv
[1:])
2280 if len(self
.args
) < 1:
2281 self
._PrintHelpAndExit
()
2283 if self
.options
.allow_any_runtime
:
2287 appinfo
.AppInfoExternal
._skip
_runtime
_checks
= True
2289 if self
.options
.runtime
:
2290 if self
.options
.runtime
not in SUPPORTED_RUNTIMES
:
2291 _PrintErrorAndExit(self
.error_fh
,
2292 '"%s" is not a supported runtime\n' %
2293 self
.options
.runtime
)
2295 appinfo
.AppInfoExternal
.ATTRIBUTES
[appinfo
.RUNTIME
] = (
2296 '|'.join(SUPPORTED_RUNTIMES
))
2298 action
= self
.args
.pop(0)
2300 def RaiseParseError(actionname
, action
):
2303 self
.parser
, self
.options
= self
._MakeSpecificParser
(action
)
2304 error_desc
= action
.error_desc
2306 error_desc
= "Expected a <directory> argument after '%s'." % (
2307 actionname
.split(' ')[0])
2308 self
.parser
.error(error_desc
)
2313 if action
== BACKENDS_ACTION
:
2314 if len(self
.args
) < 1:
2315 RaiseParseError(action
, self
.actions
[BACKENDS_ACTION
])
2317 backend_action_first
= BACKENDS_ACTION
+ ' ' + self
.args
[0]
2318 if backend_action_first
in self
.actions
:
2320 action
= backend_action_first
2322 elif len(self
.args
) > 1:
2323 backend_directory_first
= BACKENDS_ACTION
+ ' ' + self
.args
[1]
2324 if backend_directory_first
in self
.actions
:
2326 action
= backend_directory_first
2329 if len(self
.args
) < 1 or action
== BACKENDS_ACTION
:
2330 RaiseParseError(action
, self
.actions
[action
])
2332 if action
not in self
.actions
:
2333 self
.parser
.error("Unknown action: '%s'\n%s" %
2334 (action
, self
.parser
.get_description()))
2337 self
.action
= self
.actions
[action
]
2342 if not self
.action
.uses_basepath
or self
.options
.help:
2343 self
.basepath
= None
2346 RaiseParseError(action
, self
.action
)
2347 self
.basepath
= self
.args
.pop(0)
2353 self
.parser
, self
.options
= self
._MakeSpecificParser
(self
.action
)
2357 if self
.options
.help:
2358 self
._PrintHelpAndExit
()
2360 if self
.options
.verbose
== 2:
2361 logging
.getLogger().setLevel(logging
.INFO
)
2362 elif self
.options
.verbose
== 3:
2363 logging
.getLogger().setLevel(logging
.DEBUG
)
2369 verbosity
= self
.options
.verbose
2372 if self
.options
.oauth2_refresh_token
:
2373 self
.options
.oauth2
= True
2376 if self
.options
.oauth2_client_id
:
2377 self
.oauth_client_id
= self
.options
.oauth2_client_id
2378 if self
.options
.oauth2_client_secret
:
2379 self
.oauth_client_secret
= self
.options
.oauth2_client_secret
2384 self
.opener
= opener
2385 self
.file_iterator
= file_iterator
2388 """Executes the requested action.
2390 Catches any HTTPErrors raised by the action and prints them to stderr.
2393 1 on error, 0 if successful.
2397 except urllib2
.HTTPError
, e
:
2399 if self
.wrap_server_error_message
:
2400 error_format
= ('Error %d: --- begin server output ---\n'
2401 '%s\n--- end server output ---')
2403 error_format
= 'Error %d: %s'
2405 print >>self
.error_fh
, (error_format
% (e
.code
, body
.rstrip('\n')))
2407 except yaml_errors
.EventListenerError
, e
:
2408 print >>self
.error_fh
, ('Error parsing yaml file:\n%s' % e
)
2410 except CannotStartServingError
:
2411 print >>self
.error_fh
, 'Could not start serving the given version.'
2415 def _GetActionDescriptions(self
):
2416 """Returns a formatted string containing the short_descs for all actions."""
2417 action_names
= self
.actions
.keys()
2420 for action_name
in action_names
:
2421 if not self
.actions
[action_name
].hidden
:
2422 desc
+= ' %s: %s\n' % (action_name
,
2423 self
.actions
[action_name
].short_desc
)
2426 def _GetOptionParser(self
):
2427 """Creates an OptionParser with generic usage and description strings.
2430 An OptionParser instance.
2433 class Formatter(optparse
.IndentedHelpFormatter
):
2434 """Custom help formatter that does not reformat the description."""
2436 def format_description(self
, description
):
2437 """Very simple formatter."""
2438 return description
+ '\n'
2440 desc
= self
._GetActionDescriptions
()
2441 desc
= ('Action must be one of:\n%s'
2442 'Use \'help <action>\' for a detailed description.') % desc
2446 parser
= self
.parser_class(usage
='%prog [options] <action>',
2448 formatter
=Formatter(),
2449 conflict_handler
='resolve')
2454 parser
.add_option('-h', '--help', action
='store_true',
2455 dest
='help', help='Show the help message and exit.')
2456 parser
.add_option('-q', '--quiet', action
='store_const', const
=0,
2457 dest
='verbose', help='Print errors only.')
2458 parser
.add_option('-v', '--verbose', action
='store_const', const
=2,
2459 dest
='verbose', default
=1,
2460 help='Print info level logs.')
2461 parser
.add_option('--noisy', action
='store_const', const
=3,
2462 dest
='verbose', help='Print all logs.')
2463 parser
.add_option('-s', '--server', action
='store', dest
='server',
2464 default
='appengine.google.com',
2465 metavar
='SERVER', help='The App Engine server.')
2466 parser
.add_option('--secure', action
='store_true', dest
='secure',
2467 default
=True, help=optparse
.SUPPRESS_HELP
)
2468 parser
.add_option('--ignore_bad_cert', action
='store_true',
2469 dest
='ignore_certs', default
=False,
2470 help=optparse
.SUPPRESS_HELP
)
2471 parser
.add_option('--insecure', action
='store_false', dest
='secure',
2472 help=optparse
.SUPPRESS_HELP
)
2473 parser
.add_option('-e', '--email', action
='store', dest
='email',
2474 metavar
='EMAIL', default
=None,
2475 help='The username to use. Will prompt if omitted.')
2476 parser
.add_option('-H', '--host', action
='store', dest
='host',
2477 metavar
='HOST', default
=None,
2478 help='Overrides the Host header sent with all RPCs.')
2479 parser
.add_option('--no_cookies', action
='store_false',
2480 dest
='save_cookies', default
=True,
2481 help='Do not save authentication cookies to local disk.')
2482 parser
.add_option('--skip_sdk_update_check', action
='store_true',
2483 dest
='skip_sdk_update_check', default
=False,
2484 help='Do not check for SDK updates.')
2485 parser
.add_option('--passin', action
='store_true',
2486 dest
='passin', default
=False,
2487 help='Read the login password from stdin.')
2488 parser
.add_option('-A', '--application', action
='store', dest
='app_id',
2489 help=('Set the application, overriding the application '
2490 'value from app.yaml file.'))
2491 parser
.add_option('-S', '--server_id', action
='store', dest
='server_id',
2492 help=optparse
.SUPPRESS_HELP
)
2493 parser
.add_option('-V', '--version', action
='store', dest
='version',
2494 help=('Set the (major) version, overriding the version '
2495 'value from app.yaml file.'))
2496 parser
.add_option('-r', '--runtime', action
='store', dest
='runtime',
2497 help='Override runtime from app.yaml file.')
2498 parser
.add_option('-R', '--allow_any_runtime', action
='store_true',
2499 dest
='allow_any_runtime', default
=False,
2500 help='Do not validate the runtime in app.yaml')
2501 parser
.add_option('--oauth2', action
='store_true', dest
='oauth2',
2503 help='Use OAuth2 instead of password auth.')
2504 parser
.add_option('--oauth2_refresh_token', action
='store',
2505 dest
='oauth2_refresh_token', default
=None,
2506 help='An existing OAuth2 refresh token to use. Will '
2507 'not attempt interactive OAuth approval.')
2508 parser
.add_option('--oauth2_client_id', action
='store',
2509 dest
='oauth2_client_id', default
=None,
2510 help=optparse
.SUPPRESS_HELP
)
2511 parser
.add_option('--oauth2_client_secret', action
='store',
2512 dest
='oauth2_client_secret', default
=None,
2513 help=optparse
.SUPPRESS_HELP
)
2514 parser
.add_option('--oauth2_credential_file', action
='store',
2515 dest
='oauth2_credential_file', default
=None,
2516 help=optparse
.SUPPRESS_HELP
)
2517 parser
.add_option('--noauth_local_webserver', action
='store_false',
2518 dest
='auth_local_webserver', default
=True,
2519 help='Do not run a local web server to handle redirects '
2520 'during OAuth authorization.')
2523 def _MakeSpecificParser(self
, action
):
2524 """Creates a new parser with documentation specific to 'action'.
2527 action: An Action instance to be used when initializing the new parser.
2531 parser: An instance of OptionsParser customized to 'action'.
2532 options: The command line options after re-parsing.
2534 parser
= self
._GetOptionParser
()
2535 parser
.set_usage(action
.usage
)
2536 parser
.set_description('%s\n%s' % (action
.short_desc
, action
.long_desc
))
2537 action
.options(self
, parser
)
2538 options
, unused_args
= parser
.parse_args(self
.argv
[1:])
2539 return parser
, options
2541 def _PrintHelpAndExit(self
, exit_code
=2):
2542 """Prints the parser's help message and exits the program.
2545 exit_code: The integer code to pass to sys.exit().
2547 self
.parser
.print_help()
2550 def _GetRpcServer(self
):
2551 """Returns an instance of an AbstractRpcServer.
2554 A new AbstractRpcServer, on which RPC calls can be made.
2557 OAuthNotAvailable: Oauth is requested but the dependecies aren't imported.
2560 def GetUserCredentials():
2561 """Prompts the user for a username and password."""
2562 email
= self
.options
.email
2564 email
= self
.raw_input_fn('Email: ')
2566 password_prompt
= 'Password for %s: ' % email
2569 if self
.options
.passin
:
2570 password
= self
.raw_input_fn(password_prompt
)
2572 password
= self
.password_input_fn(password_prompt
)
2574 return (email
, password
)
2576 StatusUpdate('Host: %s' % self
.options
.server
)
2580 dev_appserver
= self
.options
.host
== 'localhost'
2581 if self
.options
.oauth2
and not dev_appserver
:
2582 if not appengine_rpc_httplib2
:
2584 raise OAuthNotAvailable()
2585 if not self
.rpc_server_class
:
2586 self
.rpc_server_class
= appengine_rpc_httplib2
.HttpRpcServerOauth2
2588 get_user_credentials
= self
.options
.oauth2_refresh_token
2590 source
= (self
.oauth_client_id
,
2591 self
.oauth_client_secret
,
2593 self
.options
.oauth2_credential_file
)
2595 appengine_rpc_httplib2
.tools
.FLAGS
.auth_local_webserver
= (
2596 self
.options
.auth_local_webserver
)
2598 if not self
.rpc_server_class
:
2599 self
.rpc_server_class
= appengine_rpc
.HttpRpcServerWithOAuth2Suggestion
2600 get_user_credentials
= GetUserCredentials
2601 source
= GetSourceName()
2605 email
= self
.options
.email
2607 email
= 'test@example.com'
2608 logging
.info('Using debug user %s. Override with --email', email
)
2609 rpcserver
= self
.rpc_server_class(
2610 self
.options
.server
,
2611 lambda: (email
, 'password'),
2614 host_override
=self
.options
.host
,
2615 save_cookies
=self
.options
.save_cookies
,
2619 rpcserver
.authenticated
= True
2623 if self
.options
.passin
:
2628 return self
.rpc_server_class(self
.options
.server
, get_user_credentials
,
2629 GetUserAgent(), source
,
2630 host_override
=self
.options
.host
,
2631 save_cookies
=self
.options
.save_cookies
,
2632 auth_tries
=auth_tries
,
2633 account_type
='HOSTED_OR_GOOGLE',
2634 secure
=self
.options
.secure
,
2635 ignore_certs
=self
.options
.ignore_certs
)
2637 def _FindYaml(self
, basepath
, file_name
):
2638 """Find yaml files in application directory.
2641 basepath: Base application directory.
2642 file_name: Relative file path from basepath, without extension, to search
2646 Path to located yaml file if one exists, else None.
2648 if not os
.path
.isdir(basepath
):
2649 self
.parser
.error('Not a directory: %s' % basepath
)
2653 alt_basepath
= os
.path
.join(basepath
, 'WEB-INF', 'appengine-generated')
2655 for yaml_basepath
in (basepath
, alt_basepath
):
2656 for yaml_file
in (file_name
+ '.yaml', file_name
+ '.yml'):
2657 yaml_path
= os
.path
.join(yaml_basepath
, yaml_file
)
2658 if os
.path
.isfile(yaml_path
):
2663 def _ParseAppInfoFromYaml(self
, basepath
, basename
='app'):
2664 """Parses the app.yaml file.
2667 basepath: The directory of the application.
2668 basename: The relative file path, from basepath, to search for.
2671 An AppInfoExternal object.
2673 appyaml
= self
._ParseYamlFile
(basepath
, basename
, appinfo_includes
.Parse
)
2675 self
.parser
.error('Directory does not contain an %s.yaml '
2676 'configuration file.' % basename
)
2678 orig_application
= appyaml
.application
2679 orig_server
= appyaml
.server
2680 orig_version
= appyaml
.version
2681 if self
.options
.app_id
:
2682 appyaml
.application
= self
.options
.app_id
2683 if self
.options
.server_id
:
2684 appyaml
.server
= self
.options
.server_id
2685 if self
.options
.version
:
2686 appyaml
.version
= self
.options
.version
2687 if self
.options
.runtime
:
2688 appyaml
.runtime
= self
.options
.runtime
2690 msg
= 'Application: %s' % appyaml
.application
2691 if appyaml
.application
!= orig_application
:
2692 msg
+= ' (was: %s)' % orig_application
2693 if self
.action
.function
is 'Update':
2695 if (appyaml
.server
is not None and
2696 appyaml
.server
!= appinfo
.DEFAULT_SERVER
):
2697 msg
+= '; server: %s' % appyaml
.server
2698 if appyaml
.server
!= orig_server
:
2699 msg
+= ' (was: %s)' % orig_server
2701 msg
+= '; version: %s' % appyaml
.version
2702 if appyaml
.version
!= orig_version
:
2703 msg
+= ' (was: %s)' % orig_version
2707 def _ParseYamlFile(self
, basepath
, basename
, parser
):
2708 """Parses a yaml file.
2711 basepath: The base directory of the application.
2712 basename: The relative file path, from basepath, (with the '.yaml'
2714 parser: the function or method used to parse the file.
2717 A single parsed yaml file or None if the file does not exist.
2719 file_name
= self
._FindYaml
(basepath
, basename
)
2720 if file_name
is not None:
2721 fh
= self
.opener(file_name
, 'r')
2723 defns
= parser(fh
, open_fn
=self
.opener
)
2729 def _ParseBackendsYaml(self
, basepath
):
2730 """Parses the backends.yaml file.
2733 basepath: the directory of the application.
2736 A BackendsInfoExternal object or None if the file does not exist.
2738 return self
._ParseYamlFile
(basepath
, 'backends',
2739 backendinfo
.LoadBackendInfo
)
2741 def _ParseIndexYaml(self
, basepath
):
2742 """Parses the index.yaml file.
2745 basepath: the directory of the application.
2748 A single parsed yaml file or None if the file does not exist.
2750 return self
._ParseYamlFile
(basepath
, 'index',
2751 datastore_index
.ParseIndexDefinitions
)
2753 def _ParseCronYaml(self
, basepath
):
2754 """Parses the cron.yaml file.
2757 basepath: the directory of the application.
2760 A CronInfoExternal object or None if the file does not exist.
2762 return self
._ParseYamlFile
(basepath
, 'cron', croninfo
.LoadSingleCron
)
2764 def _ParseQueueYaml(self
, basepath
):
2765 """Parses the queue.yaml file.
2768 basepath: the directory of the application.
2771 A QueueInfoExternal object or None if the file does not exist.
2773 return self
._ParseYamlFile
(basepath
, 'queue', queueinfo
.LoadSingleQueue
)
2775 def _ParseDispatchYaml(self
, basepath
):
2776 """Parses the dispatch.yaml file.
2779 basepath: the directory of the application.
2782 A DispatchInfoExternal object or None if the file does not exist.
2784 return self
._ParseYamlFile
(basepath
, 'dispatch',
2785 dispatchinfo
.LoadSingleDispatch
)
2787 def _ParseDosYaml(self
, basepath
):
2788 """Parses the dos.yaml file.
2791 basepath: the directory of the application.
2794 A DosInfoExternal object or None if the file does not exist.
2796 return self
._ParseYamlFile
(basepath
, 'dos', dosinfo
.LoadSingleDos
)
2798 def Help(self
, action
=None):
2799 """Prints help for a specific action.
2802 action: If provided, print help for the action provided.
2804 Expects self.args[0], or 'action', to contain the name of the action in
2805 question. Exits the program after printing the help message.
2808 if len(self
.args
) > 1:
2809 self
.args
= [' '.join(self
.args
)]
2811 if len(self
.args
) != 1 or self
.args
[0] not in self
.actions
:
2812 self
.parser
.error('Expected a single action argument. '
2813 ' Must be one of:\n' +
2814 self
._GetActionDescriptions
())
2815 action
= self
.args
[0]
2816 action
= self
.actions
[action
]
2817 self
.parser
, unused_options
= self
._MakeSpecificParser
(action
)
2818 self
._PrintHelpAndExit
(exit_code
=0)
2820 def DownloadApp(self
):
2821 """Downloads the given app+version."""
2822 if len(self
.args
) != 1:
2823 self
.parser
.error('\"download_app\" expects one non-option argument, '
2824 'found ' + str(len(self
.args
)) + '.')
2826 out_dir
= self
.args
[0]
2828 app_id
= self
.options
.app_id
2830 self
.parser
.error('You must specify an app ID via -A or --application.')
2832 server
= self
.options
.server_id
2833 app_version
= self
.options
.version
2837 if os
.path
.exists(out_dir
):
2838 if not os
.path
.isdir(out_dir
):
2839 self
.parser
.error('Cannot download to path "%s": '
2840 'there\'s a file in the way.' % out_dir
)
2841 elif os
.listdir(out_dir
):
2842 self
.parser
.error('Cannot download to path "%s": directory already '
2843 'exists and it isn\'t empty.' % out_dir
)
2845 rpcserver
= self
._GetRpcServer
()
2847 DoDownloadApp(rpcserver
, out_dir
, app_id
, server
, app_version
)
2849 def UpdateVersion(self
, rpcserver
, basepath
, appyaml
, server_yaml_path
,
2851 """Updates and deploys a new appversion.
2854 rpcserver: An AbstractRpcServer instance on which RPC calls can be made.
2855 basepath: The root directory of the version to update.
2856 appyaml: The AppInfoExternal object parsed from an app.yaml-like file.
2857 server_yaml_path: The (string) path to the yaml file, relative to the
2859 backend: The name of the backend to update, if any.
2862 An appinfo.AppInfoSummary if one was returned from the Deploy, None
2866 if self
.options
.precompilation
:
2867 if not appyaml
.derived_file_type
:
2868 appyaml
.derived_file_type
= []
2869 if appinfo
.PYTHON_PRECOMPILED
not in appyaml
.derived_file_type
:
2870 appyaml
.derived_file_type
.append(appinfo
.PYTHON_PRECOMPILED
)
2872 paths
= self
.file_iterator(basepath
, appyaml
.skip_files
, appyaml
.runtime
)
2873 openfunc
= lambda path
: self
.opener(os
.path
.join(basepath
, path
), 'rb')
2875 if appyaml
.runtime
== 'go':
2878 goroot
= os
.path
.join(os
.path
.dirname(google
.appengine
.__file
__),
2880 gopath
= os
.environ
.get('GOPATH')
2881 if os
.path
.isdir(goroot
) and gopath
:
2882 app_paths
= list(paths
)
2883 go_files
= [f
for f
in app_paths
2884 if f
.endswith('.go') and not appyaml
.nobuild_files
.match(f
)]
2886 raise Exception('no Go source files to upload '
2887 '(-nobuild_files applied)')
2889 os
.path
.join(goroot
, 'bin', 'go-app-builder'),
2890 '-app_base', self
.basepath
,
2897 p
= subprocess
.Popen(gab_argv
, stdout
=subprocess
.PIPE
,
2898 stderr
=subprocess
.PIPE
, env
={})
2900 except Exception, e
:
2901 raise Exception('failed running go-app-builder', e
)
2903 raise Exception(p
.stderr
.read())
2908 overlay
= dict([l
.split('|') for l
in p
.stdout
.read().split('\n') if l
])
2909 logging
.info('GOPATH overlay: %s', overlay
)
2913 return self
.opener(overlay
[path
], 'rb')
2914 return self
.opener(os
.path
.join(basepath
, path
), 'rb')
2915 paths
= app_paths
+ overlay
.keys()
2918 appversion
= AppVersionUpload(rpcserver
,
2920 server_yaml_path
=server_yaml_path
,
2922 error_fh
=self
.error_fh
)
2923 return appversion
.DoUpload(paths
, openfunc
)
2925 def UpdateUsingSpecificFiles(self
):
2926 """Updates and deploys new app versions based on given config files."""
2927 rpcserver
= self
._GetRpcServer
()
2928 all_files
= [self
.basepath
] + self
.args
2929 has_python25_version
= False
2931 for yaml_path
in all_files
:
2932 file_name
= os
.path
.basename(yaml_path
)
2933 self
.basepath
= os
.path
.dirname(yaml_path
)
2934 if not self
.basepath
:
2936 server_yaml
= self
._ParseAppInfoFromYaml
(self
.basepath
,
2937 os
.path
.splitext(file_name
)[0])
2938 if server_yaml
.runtime
== 'python':
2939 has_python25_version
= True
2943 if not server_yaml
.server
and file_name
!= 'app.yaml':
2944 ErrorUpdate("Error: 'server' parameter not specified in %s" %
2947 self
.UpdateVersion(rpcserver
, self
.basepath
, server_yaml
, file_name
)
2948 if has_python25_version
:
2949 MigratePython27Notice()
2952 """Updates and deploys a new appversion and global app configs."""
2954 rpcserver
= self
._GetRpcServer
()
2955 if not os
.path
.isdir(self
.basepath
):
2957 self
.UpdateUsingSpecificFiles()
2961 yaml_file_basename
= 'app.yaml'
2962 appyaml
= self
._ParseAppInfoFromYaml
(
2964 basename
=os
.path
.splitext(yaml_file_basename
)[0])
2969 if self
.options
.skip_sdk_update_check
:
2970 logging
.info('Skipping update check')
2972 updatecheck
= self
.update_check_class(rpcserver
, appyaml
)
2973 updatecheck
.CheckForUpdates()
2974 self
.UpdateVersion(rpcserver
, self
.basepath
, appyaml
, yaml_file_basename
)
2976 if appyaml
.runtime
== 'python':
2977 MigratePython27Notice()
2980 if self
.options
.backends
:
2981 self
.BackendsUpdate()
2988 index_defs
= self
._ParseIndexYaml
(self
.basepath
)
2990 index_upload
= IndexDefinitionUpload(rpcserver
, appyaml
, index_defs
)
2992 index_upload
.DoUpload()
2993 except urllib2
.HTTPError
, e
:
2994 ErrorUpdate('Error %d: --- begin server output ---\n'
2995 '%s\n--- end server output ---' %
2996 (e
.code
, e
.read().rstrip('\n')))
2997 print >> self
.error_fh
, (
2998 'Your app was updated, but there was an error updating your '
2999 'indexes. Please retry later with appcfg.py update_indexes.')
3002 cron_yaml
= self
._ParseCronYaml
(self
.basepath
)
3004 cron_upload
= CronEntryUpload(rpcserver
, appyaml
, cron_yaml
)
3005 cron_upload
.DoUpload()
3008 queue_yaml
= self
._ParseQueueYaml
(self
.basepath
)
3010 queue_upload
= QueueEntryUpload(rpcserver
, appyaml
, queue_yaml
)
3011 queue_upload
.DoUpload()
3014 dos_yaml
= self
._ParseDosYaml
(self
.basepath
)
3016 dos_upload
= DosEntryUpload(rpcserver
, appyaml
, dos_yaml
)
3017 dos_upload
.DoUpload()
3021 pagespeed_upload
= PagespeedEntryUpload(
3022 rpcserver
, appyaml
, appyaml
.pagespeed
)
3024 pagespeed_upload
.DoUpload()
3025 except urllib2
.HTTPError
, e
:
3026 ErrorUpdate('Error %d: --- begin server output ---\n'
3027 '%s\n--- end server output ---' %
3028 (e
.code
, e
.read().rstrip('\n')))
3029 print >> self
.error_fh
, (
3030 'Your app was updated, but there was an error updating PageSpeed. '
3031 'Please try the update again later.')
3033 def _UpdateOptions(self
, parser
):
3034 """Adds update-specific options to 'parser'.
3037 parser: An instance of OptionsParser.
3039 parser
.add_option('--no_precompilation', action
='store_false',
3040 dest
='precompilation', default
=True,
3041 help='Disable automatic Python precompilation.')
3042 parser
.add_option('--backends', action
='store_true',
3043 dest
='backends', default
=False,
3044 help='Update backends when performing appcfg update.')
3046 def VacuumIndexes(self
):
3047 """Deletes unused indexes."""
3049 self
.parser
.error('Expected a single <directory> argument.')
3051 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3054 index_defs
= self
._ParseIndexYaml
(self
.basepath
)
3055 if index_defs
is None:
3056 index_defs
= datastore_index
.IndexDefinitions()
3058 rpcserver
= self
._GetRpcServer
()
3059 vacuum
= VacuumIndexesOperation(rpcserver
,
3061 self
.options
.force_delete
)
3062 vacuum
.DoVacuum(index_defs
)
3064 def _VacuumIndexesOptions(self
, parser
):
3065 """Adds vacuum_indexes-specific options to 'parser'.
3068 parser: An instance of OptionsParser.
3070 parser
.add_option('-f', '--force', action
='store_true', dest
='force_delete',
3072 help='Force deletion without being prompted.')
3074 def UpdateCron(self
):
3075 """Updates any new or changed cron definitions."""
3077 self
.parser
.error('Expected a single <directory> argument.')
3079 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3080 rpcserver
= self
._GetRpcServer
()
3083 cron_yaml
= self
._ParseCronYaml
(self
.basepath
)
3085 cron_upload
= CronEntryUpload(rpcserver
, appyaml
, cron_yaml
)
3086 cron_upload
.DoUpload()
3088 print >>sys
.stderr
, 'Could not find cron configuration. No action taken.'
3090 def UpdateIndexes(self
):
3091 """Updates indexes."""
3093 self
.parser
.error('Expected a single <directory> argument.')
3096 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3097 rpcserver
= self
._GetRpcServer
()
3100 index_defs
= self
._ParseIndexYaml
(self
.basepath
)
3102 index_upload
= IndexDefinitionUpload(rpcserver
, appyaml
, index_defs
)
3103 index_upload
.DoUpload()
3105 print >>sys
.stderr
, 'Could not find index configuration. No action taken.'
3107 def UpdateQueues(self
):
3108 """Updates any new or changed task queue definitions."""
3110 self
.parser
.error('Expected a single <directory> argument.')
3112 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3113 rpcserver
= self
._GetRpcServer
()
3116 queue_yaml
= self
._ParseQueueYaml
(self
.basepath
)
3118 queue_upload
= QueueEntryUpload(rpcserver
, appyaml
, queue_yaml
)
3119 queue_upload
.DoUpload()
3121 print >>sys
.stderr
, 'Could not find queue configuration. No action taken.'
3123 def UpdateDispatch(self
):
3124 """Updates new or changed dispatch definitions."""
3126 self
.parser
.error('Expected a single <directory> argument.')
3128 rpcserver
= self
._GetRpcServer
()
3131 dispatch_yaml
= self
._ParseDispatchYaml
(self
.basepath
)
3133 if self
.options
.app_id
:
3134 dispatch_yaml
.application
= self
.options
.app_id
3135 if not dispatch_yaml
.application
:
3136 self
.parser
.error('Expected -A app_id when dispatch.yaml.application'
3138 StatusUpdate('Uploading dispatch entries.')
3139 rpcserver
.Send('/api/dispatch/update',
3140 app_id
=dispatch_yaml
.application
,
3141 payload
=dispatch_yaml
.ToYAML())
3143 print >>sys
.stderr
, ('Could not find dispatch configuration. No action'
3146 def UpdateDos(self
):
3147 """Updates any new or changed dos definitions."""
3149 self
.parser
.error('Expected a single <directory> argument.')
3151 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3152 rpcserver
= self
._GetRpcServer
()
3155 dos_yaml
= self
._ParseDosYaml
(self
.basepath
)
3157 dos_upload
= DosEntryUpload(rpcserver
, appyaml
, dos_yaml
)
3158 dos_upload
.DoUpload()
3160 print >>sys
.stderr
, 'Could not find dos configuration. No action taken.'
3162 def BackendsAction(self
):
3163 """Placeholder; we never expect this action to be invoked."""
3166 def BackendsYamlCheck(self
, appyaml
, backend
=None):
3167 """Check the backends.yaml file is sane and which backends to update."""
3170 if appyaml
.backends
:
3171 self
.parser
.error('Backends are not allowed in app.yaml.')
3173 backends_yaml
= self
._ParseBackendsYaml
(self
.basepath
)
3174 appyaml
.backends
= backends_yaml
.backends
3176 if not appyaml
.backends
:
3177 self
.parser
.error('No backends found in backends.yaml.')
3180 for backend_entry
in appyaml
.backends
:
3181 entry
= backendinfo
.LoadBackendEntry(backend_entry
.ToYAML())
3182 if entry
.name
in backends
:
3183 self
.parser
.error('Duplicate entry for backend: %s.' % entry
.name
)
3185 backends
.append(entry
.name
)
3187 backends_to_update
= []
3191 if backend
in backends
:
3192 backends_to_update
= [backend
]
3194 self
.parser
.error("Backend '%s' not found in backends.yaml." %
3198 backends_to_update
= backends
3200 return backends_to_update
3202 def BackendsUpdate(self
):
3203 """Updates a backend."""
3205 if len(self
.args
) == 1:
3206 self
.backend
= self
.args
[0]
3207 elif len(self
.args
) > 1:
3208 self
.parser
.error('Expected an optional <backend> argument.')
3210 yaml_file_basename
= 'app'
3211 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
,
3212 basename
=yaml_file_basename
)
3213 rpcserver
= self
._GetRpcServer
()
3215 backends_to_update
= self
.BackendsYamlCheck(appyaml
, self
.backend
)
3216 for backend
in backends_to_update
:
3217 self
.UpdateVersion(rpcserver
, self
.basepath
, appyaml
, yaml_file_basename
,
3220 def BackendsList(self
):
3221 """Lists all backends for an app."""
3223 self
.parser
.error('Expected no arguments.')
3228 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3229 rpcserver
= self
._GetRpcServer
()
3230 response
= rpcserver
.Send('/api/backends/list', app_id
=appyaml
.application
)
3231 print >> self
.out_fh
, response
3233 def BackendsRollback(self
):
3234 """Does a rollback of an existing transaction on this backend."""
3235 if len(self
.args
) != 1:
3236 self
.parser
.error('Expected a single <backend> argument.')
3238 self
._Rollback
(self
.args
[0])
3240 def BackendsStart(self
):
3241 """Starts a backend."""
3242 if len(self
.args
) != 1:
3243 self
.parser
.error('Expected a single <backend> argument.')
3245 backend
= self
.args
[0]
3246 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3247 rpcserver
= self
._GetRpcServer
()
3248 response
= rpcserver
.Send('/api/backends/start',
3249 app_id
=appyaml
.application
,
3251 print >> self
.out_fh
, response
3253 def BackendsStop(self
):
3254 """Stops a backend."""
3255 if len(self
.args
) != 1:
3256 self
.parser
.error('Expected a single <backend> argument.')
3258 backend
= self
.args
[0]
3259 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3260 rpcserver
= self
._GetRpcServer
()
3261 response
= rpcserver
.Send('/api/backends/stop',
3262 app_id
=appyaml
.application
,
3264 print >> self
.out_fh
, response
3266 def BackendsDelete(self
):
3267 """Deletes a backend."""
3268 if len(self
.args
) != 1:
3269 self
.parser
.error('Expected a single <backend> argument.')
3271 backend
= self
.args
[0]
3272 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3273 rpcserver
= self
._GetRpcServer
()
3274 response
= rpcserver
.Send('/api/backends/delete',
3275 app_id
=appyaml
.application
,
3277 print >> self
.out_fh
, response
3279 def BackendsConfigure(self
):
3280 """Changes the configuration of an existing backend."""
3281 if len(self
.args
) != 1:
3282 self
.parser
.error('Expected a single <backend> argument.')
3284 backend
= self
.args
[0]
3285 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3286 backends_yaml
= self
._ParseBackendsYaml
(self
.basepath
)
3287 rpcserver
= self
._GetRpcServer
()
3288 response
= rpcserver
.Send('/api/backends/configure',
3289 app_id
=appyaml
.application
,
3291 payload
=backends_yaml
.ToYAML())
3292 print >> self
.out_fh
, response
3294 def _ParseAndValidateServerYamls(self
, yaml_paths
):
3295 """Validates given yaml paths and returns the parsed yaml objects.
3298 yaml_paths: List of paths to AppInfo yaml files.
3301 List of parsed AppInfo yamls.
3305 last_yaml_path
= None
3306 for yaml_path
in yaml_paths
:
3307 if not os
.path
.isfile(yaml_path
):
3310 ("Error: The given path '%s' is not to a YAML configuration "
3311 "file.\n") % yaml_path
)
3312 file_name
= os
.path
.basename(yaml_path
)
3313 base_path
= os
.path
.dirname(yaml_path
)
3316 server_yaml
= self
._ParseAppInfoFromYaml
(base_path
,
3317 os
.path
.splitext(file_name
)[0])
3319 if not server_yaml
.server
and file_name
!= 'app.yaml':
3322 "Error: 'server' parameter not specified in %s" % yaml_path
)
3326 if app_id
is not None and server_yaml
.application
!= app_id
:
3329 "Error: 'application' value '%s' in %s does not match the value "
3330 "'%s', found in %s" % (server_yaml
.application
,
3334 app_id
= server_yaml
.application
3335 last_yaml_path
= yaml_path
3336 results
.append(server_yaml
)
3340 def _ServerAction(self
, action_path
):
3341 """Process flags and yaml files and make a call to the given path.
3343 The 'start' and 'stop' actions are extremely similar in how they process
3344 input to appcfg.py and only really differ in what path they hit on the
3348 action_path: Path on the RPCServer to send the call to.
3351 servers_to_process
= []
3352 if len(self
.args
) == 0:
3354 if not (self
.options
.app_id
and
3355 self
.options
.server_id
and
3356 self
.options
.version
):
3357 _PrintErrorAndExit(self
.error_fh
,
3358 'Expected at least one <file> argument or the '
3359 '--application, --server_id and --version flags to'
3362 servers_to_process
.append((self
.options
.app_id
,
3363 self
.options
.server_id
,
3364 self
.options
.version
))
3368 if self
.options
.server_id
:
3370 _PrintErrorAndExit(self
.error_fh
,
3371 'You may not specify a <file> argument with the '
3372 '--server_id flag.')
3374 server_yamls
= self
._ParseAndValidateServerYamls
(self
.args
)
3375 for serv_yaml
in server_yamls
:
3378 app_id
= serv_yaml
.application
3379 servers_to_process
.append((self
.options
.app_id
or serv_yaml
.application
,
3380 serv_yaml
.server
or appinfo
.DEFAULT_SERVER
,
3381 self
.options
.version
or serv_yaml
.version
))
3383 rpcserver
= self
._GetRpcServer
()
3386 for app_id
, server
, version
in servers_to_process
:
3387 response
= rpcserver
.Send(action_path
,
3391 print >> self
.out_fh
, response
3394 """Starts one or more servers."""
3395 self
._ServerAction
('/api/servers/start')
3398 """Stops one or more servers."""
3399 self
._ServerAction
('/api/servers/stop')
3402 """Does a rollback of an existing transaction for this app version."""
3404 self
.parser
.error('Expected a single <directory> or <file> argument.')
3407 def _Rollback(self
, backend
=None):
3408 """Does a rollback of an existing transaction.
3411 backend: name of a backend to rollback, or None
3413 If a backend is specified the rollback will affect only that backend, if no
3414 backend is specified the rollback will affect the current app version.
3416 if os
.path
.isdir(self
.basepath
):
3417 server_yaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3420 file_name
= os
.path
.basename(self
.basepath
)
3421 self
.basepath
= os
.path
.dirname(self
.basepath
)
3422 if not self
.basepath
:
3424 server_yaml
= self
._ParseAppInfoFromYaml
(self
.basepath
,
3425 os
.path
.splitext(file_name
)[0])
3427 appversion
= AppVersionUpload(self
._GetRpcServer
(), server_yaml
,
3428 server_yaml_path
='app.yaml',
3431 appversion
.in_transaction
= True
3432 appversion
.Rollback()
3434 def SetDefaultVersion(self
):
3435 """Sets the default version."""
3437 if len(self
.args
) == 1:
3438 appyaml
= self
._ParseAppInfoFromYaml
(self
.args
[0])
3439 app_id
= appyaml
.application
3440 server
= appyaml
.server
or ''
3441 version
= appyaml
.version
3442 elif len(self
.args
) == 0:
3443 if not (self
.options
.app_id
and self
.options
.version
):
3445 ('Expected a <directory> argument or both --application and '
3446 '--version flags.'))
3448 self
._PrintHelpAndExit
()
3451 if self
.options
.app_id
:
3452 app_id
= self
.options
.app_id
3453 if self
.options
.server_id
:
3454 server
= self
.options
.server_id
3455 if self
.options
.version
:
3456 version
= self
.options
.version
3458 version_setter
= DefaultVersionSet(self
._GetRpcServer
(),
3462 version_setter
.SetVersion()
3464 def RequestLogs(self
):
3465 """Write request logs to a file."""
3467 args_length
= len(self
.args
)
3469 if args_length
== 2:
3470 appyaml
= self
._ParseAppInfoFromYaml
(self
.args
.pop(0))
3471 app_id
= appyaml
.application
3472 server
= appyaml
.server
or ''
3473 version
= appyaml
.version
3474 elif args_length
== 1:
3475 if not (self
.options
.app_id
and self
.options
.version
):
3477 ('Expected the --application and --version flags if <directory> '
3478 'argument is not specified.'))
3480 self
._PrintHelpAndExit
()
3483 if self
.options
.app_id
:
3484 app_id
= self
.options
.app_id
3485 if self
.options
.server_id
:
3486 server
= self
.options
.server_id
3487 if self
.options
.version
:
3488 version
= self
.options
.version
3490 if (self
.options
.severity
is not None and
3491 not 0 <= self
.options
.severity
<= MAX_LOG_LEVEL
):
3493 'Severity range is 0 (DEBUG) through %s (CRITICAL).' % MAX_LOG_LEVEL
)
3495 if self
.options
.num_days
is None:
3496 self
.options
.num_days
= int(not self
.options
.append
)
3499 end_date
= self
._ParseEndDate
(self
.options
.end_date
)
3500 except (TypeError, ValueError):
3501 self
.parser
.error('End date must be in the format YYYY-MM-DD.')
3503 rpcserver
= self
._GetRpcServer
()
3505 logs_requester
= LogsRequester(rpcserver
,
3510 self
.options
.num_days
,
3511 self
.options
.append
,
3512 self
.options
.severity
,
3515 self
.options
.include_vhost
,
3516 self
.options
.include_all
,
3517 time_func
=self
.time_func
)
3518 logs_requester
.DownloadLogs()
3521 def _ParseEndDate(date
, time_func
=time
.time
):
3522 """Translates an ISO 8601 date to a date object.
3525 date: A date string as YYYY-MM-DD.
3526 time_func: time.time() function for testing.
3529 A date object representing the last day of logs to get.
3530 If no date is given, returns today in the US/Pacific timezone.
3533 return PacificDate(time_func())
3534 return datetime
.date(*[int(i
) for i
in date
.split('-')])
3536 def _RequestLogsOptions(self
, parser
):
3537 """Adds request_logs-specific options to 'parser'.
3540 parser: An instance of OptionsParser.
3542 parser
.add_option('-n', '--num_days', type='int', dest
='num_days',
3543 action
='store', default
=None,
3544 help='Number of days worth of log data to get. '
3545 'The cut-off point is midnight US/Pacific. '
3546 'Use 0 to get all available logs. '
3547 'Default is 1, unless --append is also given; '
3548 'then the default is 0.')
3549 parser
.add_option('-a', '--append', dest
='append',
3550 action
='store_true', default
=False,
3551 help='Append to existing file.')
3552 parser
.add_option('--severity', type='int', dest
='severity',
3553 action
='store', default
=None,
3554 help='Severity of app-level log messages to get. '
3555 'The range is 0 (DEBUG) through 4 (CRITICAL). '
3556 'If omitted, only request logs are returned.')
3557 parser
.add_option('--vhost', type='string', dest
='vhost',
3558 action
='store', default
=None,
3559 help='The virtual host of log messages to get. '
3560 'If omitted, all log messages are returned.')
3561 parser
.add_option('--include_vhost', dest
='include_vhost',
3562 action
='store_true', default
=False,
3563 help='Include virtual host in log messages.')
3564 parser
.add_option('--include_all', dest
='include_all',
3565 action
='store_true', default
=None,
3566 help='Include everything in log messages.')
3567 parser
.add_option('--end_date', dest
='end_date',
3568 action
='store', default
='',
3569 help='End date (as YYYY-MM-DD) of period for log data. '
3570 'Defaults to today.')
3572 def CronInfo(self
, now
=None, output
=sys
.stdout
):
3573 """Displays information about cron definitions.
3576 now: used for testing.
3577 output: Used for testing.
3580 self
.parser
.error('Expected a single <directory> argument.')
3582 now
= datetime
.datetime
.utcnow()
3584 cron_yaml
= self
._ParseCronYaml
(self
.basepath
)
3585 if cron_yaml
and cron_yaml
.cron
:
3586 for entry
in cron_yaml
.cron
:
3587 description
= entry
.description
3589 description
= '<no description>'
3590 if not entry
.timezone
:
3591 entry
.timezone
= 'UTC'
3593 print >>output
, '\n%s:\nURL: %s\nSchedule: %s (%s)' % (description
,
3597 if entry
.timezone
!= 'UTC':
3598 print >>output
, ('Note: Schedules with timezones won\'t be calculated'
3600 schedule
= groctimespecification
.GrocTimeSpecification(entry
.schedule
)
3602 matches
= schedule
.GetMatches(now
, self
.options
.num_runs
)
3603 for match
in matches
:
3604 print >>output
, '%s, %s from now' % (
3605 match
.strftime('%Y-%m-%d %H:%M:%SZ'), match
- now
)
3607 def _CronInfoOptions(self
, parser
):
3608 """Adds cron_info-specific options to 'parser'.
3611 parser: An instance of OptionsParser.
3613 parser
.add_option('-n', '--num_runs', type='int', dest
='num_runs',
3614 action
='store', default
=5,
3615 help='Number of runs of each cron job to display'
3618 def _CheckRequiredLoadOptions(self
):
3619 """Checks that upload/download options are present."""
3620 for option
in ['filename']:
3621 if getattr(self
.options
, option
) is None:
3622 self
.parser
.error('Option \'%s\' is required.' % option
)
3623 if not self
.options
.url
:
3624 self
.parser
.error('You must have google.appengine.ext.remote_api.handler '
3625 'assigned to an endpoint in app.yaml, or provide '
3626 'the url of the handler via the \'url\' option.')
3628 def InferRemoteApiUrl(self
, appyaml
):
3629 """Uses app.yaml to determine the remote_api endpoint.
3632 appyaml: A parsed app.yaml file.
3635 The url of the remote_api endpoint as a string, or None
3638 handlers
= appyaml
.handlers
3639 handler_suffixes
= ['remote_api/handler.py',
3640 'remote_api.handler.application']
3641 app_id
= appyaml
.application
3642 for handler
in handlers
:
3643 if hasattr(handler
, 'script') and handler
.script
:
3644 if any(handler
.script
.endswith(suffix
) for suffix
in handler_suffixes
):
3645 server
= self
.options
.server
3647 if url
.endswith('(/.*)?'):
3651 if server
== 'appengine.google.com':
3652 return 'http://%s.appspot.com%s' % (app_id
, url
)
3654 match
= re
.match(PREFIXED_BY_ADMIN_CONSOLE_RE
, server
)
3656 return 'http://%s%s%s' % (app_id
, match
.group(1), url
)
3658 return 'http://%s%s' % (server
, url
)
3661 def RunBulkloader(self
, arg_dict
):
3662 """Invokes the bulkloader with the given keyword arguments.
3665 arg_dict: Dictionary of arguments to pass to bulkloader.Run().
3672 logging
.error('upload_data action requires SQLite3 and the python '
3673 'sqlite3 module (included in python since 2.5).')
3676 sys
.exit(bulkloader
.Run(arg_dict
))
3678 def _SetupLoad(self
):
3679 """Performs common verification and set up for upload and download."""
3681 if len(self
.args
) != 1 and not self
.options
.url
:
3682 self
.parser
.error('Expected either --url or a single <directory> '
3685 if len(self
.args
) == 1:
3686 self
.basepath
= self
.args
[0]
3687 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3689 self
.options
.app_id
= appyaml
.application
3691 if not self
.options
.url
:
3692 url
= self
.InferRemoteApiUrl(appyaml
)
3694 self
.options
.url
= url
3696 self
._CheckRequiredLoadOptions
()
3698 if self
.options
.batch_size
< 1:
3699 self
.parser
.error('batch_size must be 1 or larger.')
3704 logging
.getLogger().setLevel(logging
.INFO
)
3705 self
.options
.debug
= False
3707 logging
.getLogger().setLevel(logging
.DEBUG
)
3708 self
.options
.debug
= True
3710 def _MakeLoaderArgs(self
):
3711 args
= dict([(arg_name
, getattr(self
.options
, arg_name
, None)) for
3732 'result_db_filename',
3740 args
['application'] = self
.options
.app_id
3741 args
['throttle_class'] = self
.throttle_class
3744 def PerformDownload(self
, run_fn
=None):
3745 """Performs a datastore download via the bulkloader.
3748 run_fn: Function to invoke the bulkloader, used for testing.
3751 run_fn
= self
.RunBulkloader
3754 StatusUpdate('Downloading data records.')
3756 args
= self
._MakeLoaderArgs
()
3757 args
['download'] = bool(args
['config_file'])
3758 args
['has_header'] = False
3760 args
['dump'] = not args
['config_file']
3761 args
['restore'] = False
3762 args
['create_config'] = False
3766 def PerformUpload(self
, run_fn
=None):
3767 """Performs a datastore upload via the bulkloader.
3770 run_fn: Function to invoke the bulkloader, used for testing.
3773 run_fn
= self
.RunBulkloader
3776 StatusUpdate('Uploading data records.')
3778 args
= self
._MakeLoaderArgs
()
3779 args
['download'] = False
3781 args
['dump'] = False
3782 args
['restore'] = not args
['config_file']
3783 args
['create_config'] = False
3787 def CreateBulkloadConfig(self
, run_fn
=None):
3788 """Create a bulkloader config via the bulkloader wizard.
3791 run_fn: Function to invoke the bulkloader, used for testing.
3794 run_fn
= self
.RunBulkloader
3797 StatusUpdate('Creating bulkloader configuration.')
3799 args
= self
._MakeLoaderArgs
()
3800 args
['download'] = False
3801 args
['has_header'] = False
3803 args
['dump'] = False
3804 args
['restore'] = False
3805 args
['create_config'] = True
3809 def _PerformLoadOptions(self
, parser
):
3810 """Adds options common to 'upload_data' and 'download_data'.
3813 parser: An instance of OptionsParser.
3815 parser
.add_option('--url', type='string', dest
='url',
3817 help='The location of the remote_api endpoint.')
3818 parser
.add_option('--batch_size', type='int', dest
='batch_size',
3819 action
='store', default
=10,
3820 help='Number of records to post in each request.')
3821 parser
.add_option('--bandwidth_limit', type='int', dest
='bandwidth_limit',
3822 action
='store', default
=250000,
3823 help='The maximum bytes/second bandwidth for transfers.')
3824 parser
.add_option('--rps_limit', type='int', dest
='rps_limit',
3825 action
='store', default
=20,
3826 help='The maximum records/second for transfers.')
3827 parser
.add_option('--http_limit', type='int', dest
='http_limit',
3828 action
='store', default
=8,
3829 help='The maximum requests/second for transfers.')
3830 parser
.add_option('--db_filename', type='string', dest
='db_filename',
3832 help='Name of the progress database file.')
3833 parser
.add_option('--auth_domain', type='string', dest
='auth_domain',
3834 action
='store', default
='gmail.com',
3835 help='The name of the authorization domain to use.')
3836 parser
.add_option('--log_file', type='string', dest
='log_file',
3837 help='File to write bulkloader logs. If not supplied '
3838 'then a new log file will be created, named: '
3839 'bulkloader-log-TIMESTAMP.')
3840 parser
.add_option('--dry_run', action
='store_true',
3841 dest
='dry_run', default
=False,
3842 help='Do not execute any remote_api calls')
3843 parser
.add_option('--namespace', type='string', dest
='namespace',
3844 action
='store', default
='',
3845 help='Namespace to use when accessing datastore.')
3846 parser
.add_option('--num_threads', type='int', dest
='num_threads',
3847 action
='store', default
=10,
3848 help='Number of threads to transfer records with.')
3850 def _PerformUploadOptions(self
, parser
):
3851 """Adds 'upload_data' specific options to the 'parser' passed in.
3854 parser: An instance of OptionsParser.
3856 self
._PerformLoadOptions
(parser
)
3857 parser
.add_option('--filename', type='string', dest
='filename',
3859 help='The name of the file containing the input data.'
3861 parser
.add_option('--kind', type='string', dest
='kind',
3863 help='The kind of the entities to store.')
3864 parser
.add_option('--has_header', dest
='has_header',
3865 action
='store_true', default
=False,
3866 help='Whether the first line of the input file should be'
3868 parser
.add_option('--loader_opts', type='string', dest
='loader_opts',
3869 help='A string to pass to the Loader.initialize method.')
3870 parser
.add_option('--config_file', type='string', dest
='config_file',
3872 help='Name of the configuration file.')
3874 def _PerformDownloadOptions(self
, parser
):
3875 """Adds 'download_data' specific options to the 'parser' passed in.
3878 parser: An instance of OptionsParser.
3880 self
._PerformLoadOptions
(parser
)
3881 parser
.add_option('--filename', type='string', dest
='filename',
3883 help='The name of the file where output data is to be'
3884 ' written. (Required)')
3885 parser
.add_option('--kind', type='string', dest
='kind',
3887 help='The kind of the entities to retrieve.')
3888 parser
.add_option('--exporter_opts', type='string', dest
='exporter_opts',
3889 help='A string to pass to the Exporter.initialize method.'
3891 parser
.add_option('--result_db_filename', type='string',
3892 dest
='result_db_filename',
3894 help='Database to write entities to for download.')
3895 parser
.add_option('--config_file', type='string', dest
='config_file',
3897 help='Name of the configuration file.')
3899 def _CreateBulkloadConfigOptions(self
, parser
):
3900 """Adds 'download_data' specific options to the 'parser' passed in.
3903 parser: An instance of OptionsParser.
3905 self
._PerformLoadOptions
(parser
)
3906 parser
.add_option('--filename', type='string', dest
='filename',
3908 help='The name of the file where the generated template'
3909 ' is to be written. (Required)')
3911 def ResourceLimitsInfo(self
, output
=None):
3912 """Outputs the current resource limits.
3915 output: The file handle to write the output to (used for testing).
3917 appyaml
= self
._ParseAppInfoFromYaml
(self
.basepath
)
3918 resource_limits
= GetResourceLimits(self
._GetRpcServer
(), appyaml
)
3921 for attr_name
in sorted(resource_limits
):
3922 print >>output
, '%s: %s' % (attr_name
, resource_limits
[attr_name
])
3924 class Action(object):
3925 """Contains information about a command line action.
3928 function: The name of a function defined on AppCfg or its subclasses
3929 that will perform the appropriate action.
3930 usage: A command line usage string.
3931 short_desc: A one-line description of the action.
3932 long_desc: A detailed description of the action. Whitespace and
3933 formatting will be preserved.
3934 error_desc: An error message to display when the incorrect arguments are
3936 options: A function that will add extra options to a given OptionParser
3938 uses_basepath: Does the action use a basepath/app-directory (and hence
3940 hidden: Should this command be shown in the help listing.
3948 def __init__(self
, function
, usage
, short_desc
, long_desc
='',
3949 error_desc
=None, options
=lambda obj
, parser
: None,
3950 uses_basepath
=True, hidden
=False):
3951 """Initializer for the class attributes."""
3952 self
.function
= function
3954 self
.short_desc
= short_desc
3955 self
.long_desc
= long_desc
3956 self
.error_desc
= error_desc
3957 self
.options
= options
3958 self
.uses_basepath
= uses_basepath
3959 self
.hidden
= hidden
3961 def __call__(self
, appcfg
):
3962 """Invoke this Action on the specified AppCfg.
3964 This calls the function of the appropriate name on AppCfg, and
3965 respects polymophic overrides.
3968 appcfg: The appcfg to use.
3970 The result of the function call.
3972 method
= getattr(appcfg
, self
.function
)
3979 usage
='%prog help <action>',
3980 short_desc
='Print help for a specific action.',
3981 uses_basepath
=False),
3985 usage
='%prog [options] update <directory>',
3986 options
=_UpdateOptions
,
3987 short_desc
='Create or update an app version.',
3989 Specify a directory that contains all of the files required by
3990 the app, and appcfg.py will create/update the app version referenced
3991 in the app.yaml file at the top level of that directory. appcfg.py
3992 will follow symlinks and recursively upload all files to the server.
3993 Temporary or source control files (e.g. foo~, .svn/*) will be skipped."""),
3995 'download_app': Action(
3996 function
='DownloadApp',
3997 usage
='%prog [options] download_app -A app_id [ -V version ]'
3999 short_desc
='Download a previously-uploaded app.',
4001 Download a previously-uploaded app to the specified directory. The app
4002 ID is specified by the \"-A\" option. The optional version is specified
4003 by the \"-V\" option.""",
4004 uses_basepath
=False),
4006 'update_cron': Action(
4007 function
='UpdateCron',
4008 usage
='%prog [options] update_cron <directory>',
4009 short_desc
='Update application cron definitions.',
4011 The 'update_cron' command will update any new, removed or changed cron
4012 definitions from the optional cron.yaml file."""),
4014 'update_indexes': Action(
4015 function
='UpdateIndexes',
4016 usage
='%prog [options] update_indexes <directory>',
4017 short_desc
='Update application indexes.',
4019 The 'update_indexes' command will add additional indexes which are not currently
4020 in production as well as restart any indexes that were not completed."""),
4022 'update_queues': Action(
4023 function
='UpdateQueues',
4024 usage
='%prog [options] update_queues <directory>',
4025 short_desc
='Update application task queue definitions.',
4027 The 'update_queue' command will update any new, removed or changed task queue
4028 definitions from the optional queue.yaml file."""),
4030 'update_dispatch': Action(
4031 function
='UpdateDispatch',
4033 usage
='%prog [options] update_dispatch <directory>',
4034 short_desc
='Update application dispatch definitions.',
4036 The 'update_dispatch' command will update any new, removed or changed dispatch
4037 definitions from the optional dispatch.yaml file."""),
4039 'update_dos': Action(
4040 function
='UpdateDos',
4041 usage
='%prog [options] update_dos <directory>',
4042 short_desc
='Update application dos definitions.',
4044 The 'update_dos' command will update any new, removed or changed dos
4045 definitions from the optional dos.yaml file."""),
4048 function
='BackendsAction',
4049 usage
='%prog [options] backends <directory> <action>',
4050 short_desc
='Perform a backend action.',
4052 The 'backends' command will perform a backends action.""",
4054 Expected a <directory> and <action> argument."""),
4056 'backends list': Action(
4057 function
='BackendsList',
4058 usage
='%prog [options] backends <directory> list',
4059 short_desc
='List all backends configured for the app.',
4061 The 'backends list' command will list all backends configured for the app."""),
4063 'backends update': Action(
4064 function
='BackendsUpdate',
4065 usage
='%prog [options] backends <directory> update [<backend>]',
4066 options
=_UpdateOptions
,
4067 short_desc
='Update one or more backends.',
4069 The 'backends update' command updates one or more backends. This command
4070 updates backend configuration settings and deploys new code to the server. Any
4071 existing instances will stop and be restarted. Updates all backends, or a
4072 single backend if the <backend> argument is provided."""),
4074 'backends rollback': Action(
4075 function
='BackendsRollback',
4076 usage
='%prog [options] backends <directory> rollback <backend>',
4077 short_desc
='Roll back an update of a backend.',
4079 The 'backends update' command requires a server-side transaction.
4080 Use 'backends rollback' if you experience an error during 'backends update'
4081 and want to start the update over again."""),
4083 'backends start': Action(
4084 function
='BackendsStart',
4085 usage
='%prog [options] backends <directory> start <backend>',
4086 short_desc
='Start a backend.',
4088 The 'backends start' command will put a backend into the START state."""),
4090 'backends stop': Action(
4091 function
='BackendsStop',
4092 usage
='%prog [options] backends <directory> stop <backend>',
4093 short_desc
='Stop a backend.',
4095 The 'backends start' command will put a backend into the STOP state."""),
4097 'backends delete': Action(
4098 function
='BackendsDelete',
4099 usage
='%prog [options] backends <directory> delete <backend>',
4100 short_desc
='Delete a backend.',
4102 The 'backends delete' command will delete a backend."""),
4104 'backends configure': Action(
4105 function
='BackendsConfigure',
4106 usage
='%prog [options] backends <directory> configure <backend>',
4107 short_desc
='Reconfigure a backend without stopping it.',
4109 The 'backends configure' command performs an online update of a backend, without
4110 stopping instances that are currently running. No code or handlers are updated,
4111 only certain configuration settings specified in backends.yaml. Valid settings
4112 are: instances, options: public, and options: failfast."""),
4114 'vacuum_indexes': Action(
4115 function
='VacuumIndexes',
4116 usage
='%prog [options] vacuum_indexes <directory>',
4117 options
=_VacuumIndexesOptions
,
4118 short_desc
='Delete unused indexes from application.',
4120 The 'vacuum_indexes' command will help clean up indexes which are no longer
4121 in use. It does this by comparing the local index configuration with
4122 indexes that are actually defined on the server. If any indexes on the
4123 server do not exist in the index configuration file, the user is given the
4124 option to delete them."""),
4127 function
='Rollback',
4128 usage
='%prog [options] rollback <directory> | <file>',
4129 short_desc
='Rollback an in-progress update.',
4131 The 'update' command requires a server-side transaction.
4132 Use 'rollback' if you experience an error during 'update'
4133 and want to begin a new update transaction."""),
4135 'request_logs': Action(
4136 function
='RequestLogs',
4137 usage
='%prog [options] request_logs [<directory>] <output_file>',
4138 options
=_RequestLogsOptions
,
4139 uses_basepath
=False,
4140 short_desc
='Write request logs in Apache common log format.',
4142 The 'request_logs' command exports the request logs from your application
4143 to a file. It will write Apache common log format records ordered
4144 chronologically. If output file is '-' stdout will be written.""",
4146 Expected an optional <directory> and mandatory <output_file> argument."""),
4148 'cron_info': Action(
4149 function
='CronInfo',
4150 usage
='%prog [options] cron_info <directory>',
4151 options
=_CronInfoOptions
,
4152 short_desc
='Display information about cron jobs.',
4154 The 'cron_info' command will display the next 'number' runs (default 5) for
4155 each cron job defined in the cron.yaml file."""),
4162 uses_basepath
=False,
4163 usage
='%prog [options] start [file, ...]',
4164 short_desc
='Start a server version.',
4166 The 'start' command will put a server version into the START state."""),
4171 uses_basepath
=False,
4172 usage
='%prog [options] stop [file, ...]',
4173 short_desc
='Stop a server version.',
4175 The 'stop' command will put a server version into the STOP state."""),
4181 'upload_data': Action(
4182 function
='PerformUpload',
4183 usage
='%prog [options] upload_data <directory>',
4184 options
=_PerformUploadOptions
,
4185 short_desc
='Upload data records to datastore.',
4187 The 'upload_data' command translates input records into datastore entities and
4188 uploads them into your application's datastore.""",
4189 uses_basepath
=False),
4191 'download_data': Action(
4192 function
='PerformDownload',
4193 usage
='%prog [options] download_data <directory>',
4194 options
=_PerformDownloadOptions
,
4195 short_desc
='Download entities from datastore.',
4197 The 'download_data' command downloads datastore entities and writes them to
4198 file as CSV or developer defined format.""",
4199 uses_basepath
=False),
4201 'create_bulkloader_config': Action(
4202 function
='CreateBulkloadConfig',
4203 usage
='%prog [options] create_bulkload_config <directory>',
4204 options
=_CreateBulkloadConfigOptions
,
4205 short_desc
='Create a bulkloader.yaml from a running application.',
4207 The 'create_bulkloader_config' command creates a bulkloader.yaml configuration
4208 template for use with upload_data or download_data.""",
4209 uses_basepath
=False),
4212 'set_default_version': Action(
4213 function
='SetDefaultVersion',
4214 usage
='%prog [options] set_default_version [directory]',
4215 short_desc
='Set the default (serving) version.',
4217 The 'set_default_version' command sets the default (serving) version of the app.
4218 Defaults to using the application and version specified in app.yaml; use the
4219 --application and --version flags to override these values.""",
4220 uses_basepath
=False),
4222 'resource_limits_info': Action(
4223 function
='ResourceLimitsInfo',
4224 usage
='%prog [options] resource_limits_info <directory>',
4225 short_desc
='Get the resource limits.',
4227 The 'resource_limits_info' command prints the current resource limits that
4235 logging
.basicConfig(format
=('%(asctime)s %(levelname)s %(filename)s:'
4236 '%(lineno)s %(message)s '))
4238 result
= AppCfgApp(argv
).Run()
4241 except KeyboardInterrupt:
4242 StatusUpdate('Interrupted.')
4246 if __name__
== '__main__':