330 except urllib2.URLError, e: |
356 except urllib2.URLError, e: |
331 logging.info('Update check failed: %s', e) |
357 logging.info('Update check failed: %s', e) |
332 return |
358 return |
333 |
359 |
334 latest = yaml.safe_load(response) |
360 latest = yaml.safe_load(response) |
335 if latest['release'] == version['release']: |
361 if version['release'] == latest['release']: |
336 logging.info('The SDK is up to date.') |
362 logging.info('The SDK is up to date.') |
337 return |
363 return |
|
364 |
|
365 try: |
|
366 this_release = _VersionList(version['release']) |
|
367 except ValueError: |
|
368 logging.warn('Could not parse this release version (%r)', |
|
369 version['release']) |
|
370 else: |
|
371 try: |
|
372 advertised_release = _VersionList(latest['release']) |
|
373 except ValueError: |
|
374 logging.warn('Could not parse advertised release version (%r)', |
|
375 latest['release']) |
|
376 else: |
|
377 if this_release > advertised_release: |
|
378 logging.info('This SDK release is newer than the advertised release.') |
|
379 return |
338 |
380 |
339 api_versions = latest['api_versions'] |
381 api_versions = latest['api_versions'] |
340 if self.config.api_version not in api_versions: |
382 if self.config.api_version not in api_versions: |
341 self._Nag( |
383 self._Nag( |
342 'The api version you are using (%s) is obsolete! You should\n' |
384 'The api version you are using (%s) is obsolete! You should\n' |
962 return sentinel.rstrip('\n') |
1004 return sentinel.rstrip('\n') |
963 finally: |
1005 finally: |
964 fp.close() |
1006 fp.close() |
965 |
1007 |
966 |
1008 |
|
1009 class UploadBatcher(object): |
|
1010 """Helper to batch file uploads.""" |
|
1011 |
|
1012 def __init__(self, what, app_id, version, server): |
|
1013 """Constructor. |
|
1014 |
|
1015 Args: |
|
1016 what: Either 'file' or 'blob' indicating what kind of objects |
|
1017 this batcher uploads. Used in messages and URLs. |
|
1018 app_id: The application ID. |
|
1019 version: The application version string. |
|
1020 server: The RPC server. |
|
1021 """ |
|
1022 assert what in ('file', 'blob'), repr(what) |
|
1023 self.what = what |
|
1024 self.app_id = app_id |
|
1025 self.version = version |
|
1026 self.server = server |
|
1027 self.single_url = '/api/appversion/add' + what |
|
1028 self.batch_url = self.single_url + 's' |
|
1029 self.batching = True |
|
1030 self.batch = [] |
|
1031 self.batch_size = 0 |
|
1032 |
|
1033 def SendBatch(self): |
|
1034 """Send the current batch on its way. |
|
1035 |
|
1036 If successful, resets self.batch and self.batch_size. |
|
1037 |
|
1038 Raises: |
|
1039 HTTPError with code=404 if the server doesn't support batching. |
|
1040 """ |
|
1041 boundary = 'boundary' |
|
1042 parts = [] |
|
1043 for path, payload, mime_type in self.batch: |
|
1044 while boundary in payload: |
|
1045 boundary += '%04x' % random.randint(0, 0xffff) |
|
1046 assert len(boundary) < 80, 'Unexpected error, please try again.' |
|
1047 part = '\n'.join(['', |
|
1048 'X-Appcfg-File: %s' % urllib.quote(path), |
|
1049 'X-Appcfg-Hash: %s' % _Hash(payload), |
|
1050 'Content-Type: %s' % mime_type, |
|
1051 'Content-Length: %d' % len(payload), |
|
1052 'Content-Transfer-Encoding: 8bit', |
|
1053 '', |
|
1054 payload, |
|
1055 ]) |
|
1056 parts.append(part) |
|
1057 parts.insert(0, |
|
1058 'MIME-Version: 1.0\n' |
|
1059 'Content-Type: multipart/mixed; boundary="%s"\n' |
|
1060 '\n' |
|
1061 'This is a message with multiple parts in MIME format.' % |
|
1062 boundary) |
|
1063 parts.append('--\n') |
|
1064 delimiter = '\n--%s' % boundary |
|
1065 payload = delimiter.join(parts) |
|
1066 logging.info('Uploading batch of %d %ss to %s with boundary="%s".', |
|
1067 len(self.batch), self.what, self.batch_url, boundary) |
|
1068 self.server.Send(self.batch_url, |
|
1069 payload=payload, |
|
1070 content_type='message/rfc822', |
|
1071 app_id=self.app_id, |
|
1072 version=self.version) |
|
1073 self.batch = [] |
|
1074 self.batch_size = 0 |
|
1075 |
|
1076 def SendSingleFile(self, path, payload, mime_type): |
|
1077 """Send a single file on its way.""" |
|
1078 logging.info('Uploading %s %s (%s bytes, type=%s) to %s.', |
|
1079 self.what, path, len(payload), mime_type, self.single_url) |
|
1080 self.server.Send(self.single_url, |
|
1081 payload=payload, |
|
1082 content_type=mime_type, |
|
1083 path=path, |
|
1084 app_id=self.app_id, |
|
1085 version=self.version) |
|
1086 |
|
1087 def Flush(self): |
|
1088 """Flush the current batch. |
|
1089 |
|
1090 This first attempts to send the batch as a single request; if that |
|
1091 fails because the server doesn't support batching, the files are |
|
1092 sent one by one, and self.batching is reset to False. |
|
1093 |
|
1094 At the end, self.batch and self.batch_size are reset. |
|
1095 """ |
|
1096 if not self.batch: |
|
1097 return |
|
1098 try: |
|
1099 self.SendBatch() |
|
1100 except urllib2.HTTPError, err: |
|
1101 if err.code != 404: |
|
1102 raise |
|
1103 |
|
1104 logging.info('Old server detected; turning off %s batching.', self.what) |
|
1105 self.batching = False |
|
1106 |
|
1107 for path, payload, mime_type in self.batch: |
|
1108 self.SendSingleFile(path, payload, mime_type) |
|
1109 |
|
1110 self.batch = [] |
|
1111 self.batch_size = 0 |
|
1112 |
|
1113 def AddToBatch(self, path, payload, mime_type): |
|
1114 """Batch a file, possibly flushing first, or perhaps upload it directly. |
|
1115 |
|
1116 Args: |
|
1117 path: The name of the file. |
|
1118 payload: The contents of the file. |
|
1119 mime_type: The MIME Content-type of the file, or None. |
|
1120 |
|
1121 If mime_type is None, application/octet-stream is substituted. |
|
1122 """ |
|
1123 if not mime_type: |
|
1124 mime_type = 'application/octet-stream' |
|
1125 size = len(payload) |
|
1126 if size <= MAX_BATCH_FILE_SIZE: |
|
1127 if (len(self.batch) >= MAX_BATCH_COUNT or |
|
1128 self.batch_size + size > MAX_BATCH_SIZE): |
|
1129 self.Flush() |
|
1130 if self.batching: |
|
1131 logging.info('Adding %s %s (%s bytes, type=%s) to batch.', |
|
1132 self.what, path, size, mime_type) |
|
1133 self.batch.append((path, payload, mime_type)) |
|
1134 self.batch_size += size + BATCH_OVERHEAD |
|
1135 return |
|
1136 self.SendSingleFile(path, payload, mime_type) |
|
1137 |
|
1138 |
|
1139 def _Hash(content): |
|
1140 """Compute the hash of the content. |
|
1141 |
|
1142 Args: |
|
1143 content: The data to hash as a string. |
|
1144 |
|
1145 Returns: |
|
1146 The string representation of the hash. |
|
1147 """ |
|
1148 h = sha.new(content).hexdigest() |
|
1149 return '%s_%s_%s_%s_%s' % (h[0:8], h[8:16], h[16:24], h[24:32], h[32:40]) |
|
1150 |
|
1151 |
967 class AppVersionUpload(object): |
1152 class AppVersionUpload(object): |
968 """Provides facilities to upload a new appversion to the hosting service. |
1153 """Provides facilities to upload a new appversion to the hosting service. |
969 |
1154 |
970 Attributes: |
1155 Attributes: |
971 server: The AbstractRpcServer to use for the upload. |
1156 server: The AbstractRpcServer to use for the upload. |
993 self.app_id = self.config.application |
1178 self.app_id = self.config.application |
994 self.version = self.config.version |
1179 self.version = self.config.version |
995 self.files = {} |
1180 self.files = {} |
996 self.in_transaction = False |
1181 self.in_transaction = False |
997 self.deployed = False |
1182 self.deployed = False |
998 |
1183 self.batching = True |
999 def _Hash(self, content): |
1184 self.file_batcher = UploadBatcher('file', self.app_id, self.version, |
1000 """Compute the hash of the content. |
1185 self.server) |
1001 |
1186 self.blob_batcher = UploadBatcher('blob', self.app_id, self.version, |
1002 Args: |
1187 self.server) |
1003 content: The data to hash as a string. |
|
1004 |
|
1005 Returns: |
|
1006 The string representation of the hash. |
|
1007 """ |
|
1008 h = sha.new(content).hexdigest() |
|
1009 return '%s_%s_%s_%s_%s' % (h[0:8], h[8:16], h[16:24], h[24:32], h[32:40]) |
|
1010 |
1188 |
1011 def AddFile(self, path, file_handle): |
1189 def AddFile(self, path, file_handle): |
1012 """Adds the provided file to the list to be pushed to the server. |
1190 """Adds the provided file to the list to be pushed to the server. |
1013 |
1191 |
1014 Args: |
1192 Args: |
1107 raise KeyError('File \'%s\' is not in the list of files to be uploaded.' |
1285 raise KeyError('File \'%s\' is not in the list of files to be uploaded.' |
1108 % path) |
1286 % path) |
1109 |
1287 |
1110 del self.files[path] |
1288 del self.files[path] |
1111 mime_type = GetMimeTypeIfStaticFile(self.config, path) |
1289 mime_type = GetMimeTypeIfStaticFile(self.config, path) |
1112 if mime_type is not None: |
1290 payload = file_handle.read() |
1113 self.server.Send('/api/appversion/addblob', app_id=self.app_id, |
1291 if mime_type is None: |
1114 version=self.version, path=path, content_type=mime_type, |
1292 self.file_batcher.AddToBatch(path, payload, mime_type) |
1115 payload=file_handle.read()) |
|
1116 else: |
1293 else: |
1117 self.server.Send('/api/appversion/addfile', app_id=self.app_id, |
1294 self.blob_batcher.AddToBatch(path, payload, mime_type) |
1118 version=self.version, path=path, |
|
1119 payload=file_handle.read()) |
|
1120 |
1295 |
1121 def Commit(self): |
1296 def Commit(self): |
1122 """Commits the transaction, making the new app version available. |
1297 """Commits the transaction, making the new app version available. |
1123 |
1298 |
1124 All the files returned by Begin() must have been uploaded with UploadFile() |
1299 All the files returned by Begin() must have been uploaded with UploadFile() |
1247 raise |
1422 raise |
1248 |
1423 |
1249 try: |
1424 try: |
1250 missing_files = self.Begin() |
1425 missing_files = self.Begin() |
1251 if missing_files: |
1426 if missing_files: |
1252 StatusUpdate('Uploading %d files.' % len(missing_files)) |
1427 StatusUpdate('Uploading %d files and blobs.' % len(missing_files)) |
1253 num_files = 0 |
1428 num_files = 0 |
1254 for missing_file in missing_files: |
1429 for missing_file in missing_files: |
1255 logging.info('Uploading file \'%s\'' % missing_file) |
|
1256 file_handle = openfunc(missing_file) |
1430 file_handle = openfunc(missing_file) |
1257 try: |
1431 try: |
1258 self.UploadFile(missing_file, file_handle) |
1432 self.UploadFile(missing_file, file_handle) |
1259 finally: |
1433 finally: |
1260 file_handle.close() |
1434 file_handle.close() |
1261 num_files += 1 |
1435 num_files += 1 |
1262 if num_files % 500 == 0: |
1436 if num_files % 500 == 0: |
1263 StatusUpdate('Uploaded %d files.' % num_files) |
1437 StatusUpdate('Processed %d out of %s.' % |
|
1438 (num_files, len(missing_files))) |
|
1439 self.file_batcher.Flush() |
|
1440 self.blob_batcher.Flush() |
|
1441 StatusUpdate('Uploaded %d files and blobs' % num_files) |
1264 |
1442 |
1265 self.Commit() |
1443 self.Commit() |
1266 |
1444 |
1267 except KeyboardInterrupt: |
1445 except KeyboardInterrupt: |
1268 logging.info('User interrupted. Aborting.') |
1446 logging.info('User interrupted. Aborting.') |
|
1447 self.Rollback() |
|
1448 raise |
|
1449 except urllib2.HTTPError, err: |
|
1450 logging.info('HTTP Error (%s)', err) |
1269 self.Rollback() |
1451 self.Rollback() |
1270 raise |
1452 raise |
1271 except: |
1453 except: |
1272 logging.exception('An unexpected error occurred. Aborting.') |
1454 logging.exception('An unexpected error occurred. Aborting.') |
1273 self.Rollback() |
1455 self.Rollback() |
1854 self.parser.error( |
2036 self.parser.error( |
1855 'Severity range is 0 (DEBUG) through %s (CRITICAL).' % MAX_LOG_LEVEL) |
2037 'Severity range is 0 (DEBUG) through %s (CRITICAL).' % MAX_LOG_LEVEL) |
1856 |
2038 |
1857 if self.options.num_days is None: |
2039 if self.options.num_days is None: |
1858 self.options.num_days = int(not self.options.append) |
2040 self.options.num_days = int(not self.options.append) |
|
2041 |
|
2042 try: |
|
2043 end_date = self._ParseEndDate(self.options.end_date) |
|
2044 except ValueError: |
|
2045 self.parser.error('End date must be in the format YYYY-MM-DD.') |
|
2046 |
1859 basepath = self.args[0] |
2047 basepath = self.args[0] |
1860 appyaml = self._ParseAppYaml(basepath) |
2048 appyaml = self._ParseAppYaml(basepath) |
1861 rpc_server = self._GetRpcServer() |
2049 rpc_server = self._GetRpcServer() |
1862 logs_requester = LogsRequester(rpc_server, appyaml, self.args[1], |
2050 logs_requester = LogsRequester(rpc_server, appyaml, self.args[1], |
1863 self.options.num_days, |
2051 self.options.num_days, |
1864 self.options.append, |
2052 self.options.append, |
1865 self.options.severity, |
2053 self.options.severity, |
1866 time.time(), |
2054 end_date, |
1867 self.options.vhost, |
2055 self.options.vhost, |
1868 self.options.include_vhost) |
2056 self.options.include_vhost) |
1869 logs_requester.DownloadLogs() |
2057 logs_requester.DownloadLogs() |
|
2058 |
|
2059 def _ParseEndDate(self, date, time_func=time.time): |
|
2060 """Translates a user-readable end date to a POSIX timestamp. |
|
2061 |
|
2062 Args: |
|
2063 date: A utc date string as YYYY-MM-DD. |
|
2064 time_func: time.time() function for testing. |
|
2065 |
|
2066 Returns: |
|
2067 A POSIX timestamp representing the last moment of that day. |
|
2068 If no date is given, returns a timestamp representing now. |
|
2069 """ |
|
2070 if not date: |
|
2071 return time_func() |
|
2072 struct_time = time.strptime('%s' % date, '%Y-%m-%d') |
|
2073 return calendar.timegm(struct_time) + 86400 |
1870 |
2074 |
1871 def _RequestLogsOptions(self, parser): |
2075 def _RequestLogsOptions(self, parser): |
1872 """Adds request_logs-specific options to 'parser'. |
2076 """Adds request_logs-specific options to 'parser'. |
1873 |
2077 |
1874 Args: |
2078 Args: |
1894 help='The virtual host of log messages to get. ' |
2098 help='The virtual host of log messages to get. ' |
1895 'If omitted, all log messages are returned.') |
2099 'If omitted, all log messages are returned.') |
1896 parser.add_option('--include_vhost', dest='include_vhost', |
2100 parser.add_option('--include_vhost', dest='include_vhost', |
1897 action='store_true', default=False, |
2101 action='store_true', default=False, |
1898 help='Include virtual host in log messages.') |
2102 help='Include virtual host in log messages.') |
|
2103 parser.add_option('--end_date', dest='end_date', |
|
2104 action='store', default='', |
|
2105 help='End date (as YYYY-MM-DD) of period for log data. ' |
|
2106 'Defaults to today.') |
1899 |
2107 |
1900 def CronInfo(self, now=None, output=sys.stdout): |
2108 def CronInfo(self, now=None, output=sys.stdout): |
1901 """Displays information about cron definitions. |
2109 """Displays information about cron definitions. |
1902 |
2110 |
1903 Args: |
2111 Args: |
2112 help='The name of the authorization domain to use.') |
2331 help='The name of the authorization domain to use.') |
2113 parser.add_option('--log_file', type='string', dest='log_file', |
2332 parser.add_option('--log_file', type='string', dest='log_file', |
2114 help='File to write bulkloader logs. If not supplied ' |
2333 help='File to write bulkloader logs. If not supplied ' |
2115 'then a new log file will be created, named: ' |
2334 'then a new log file will be created, named: ' |
2116 'bulkloader-log-TIMESTAMP.') |
2335 'bulkloader-log-TIMESTAMP.') |
|
2336 parser.add_option('--dry_run', action='store_true', |
|
2337 dest='dry_run', default=False, |
|
2338 help='Do not execute any remote_api calls') |
2117 |
2339 |
2118 def _PerformUploadOptions(self, parser): |
2340 def _PerformUploadOptions(self, parser): |
2119 """Adds 'upload_data' specific options to the 'parser' passed in. |
2341 """Adds 'upload_data' specific options to the 'parser' passed in. |
2120 |
2342 |
2121 Args: |
2343 Args: |