thirdparty/google_appengine/google/appengine/tools/appcfg.py
changeset 2864 2e0b0af889be
parent 2413 d0b7dac5325c
child 3031 7678f72140e6
equal deleted inserted replaced
2862:27971a13089f 2864:2e0b0af889be
    34 import getpass
    34 import getpass
    35 import logging
    35 import logging
    36 import mimetypes
    36 import mimetypes
    37 import optparse
    37 import optparse
    38 import os
    38 import os
       
    39 import random
    39 import re
    40 import re
    40 import sha
    41 import sha
    41 import sys
    42 import sys
    42 import tempfile
    43 import tempfile
    43 import time
    44 import time
       
    45 import urllib
    44 import urllib2
    46 import urllib2
    45 
    47 
    46 import google
    48 import google
    47 import yaml
    49 import yaml
    48 from google.appengine.cron import groctimespecification
    50 from google.appengine.cron import groctimespecification
    66 UPDATE_CHECK_TIMEOUT = 3
    68 UPDATE_CHECK_TIMEOUT = 3
    67 
    69 
    68 NAG_FILE = '.appcfg_nag'
    70 NAG_FILE = '.appcfg_nag'
    69 
    71 
    70 MAX_LOG_LEVEL = 4
    72 MAX_LOG_LEVEL = 4
       
    73 
       
    74 MAX_BATCH_SIZE = 1000000
       
    75 MAX_BATCH_COUNT = 100
       
    76 MAX_BATCH_FILE_SIZE = 200000
       
    77 BATCH_OVERHEAD = 500
    71 
    78 
    72 verbosity = 1
    79 verbosity = 1
    73 
    80 
    74 
    81 
    75 appinfo.AppInfoExternal.ATTRIBUTES[appinfo.RUNTIME] = 'python'
    82 appinfo.AppInfoExternal.ATTRIBUTES[appinfo.RUNTIME] = 'python'
   218     delay *= backoff_factor
   225     delay *= backoff_factor
   219     max_tries -= 1
   226     max_tries -= 1
   220   return max_tries > 0
   227   return max_tries > 0
   221 
   228 
   222 
   229 
       
   230 def _VersionList(release):
       
   231   """Parse a version string into a list of ints.
       
   232 
       
   233   Args:
       
   234     release: The 'release' version, e.g. '1.2.4'.
       
   235         (Due to YAML parsing this may also be an int or float.)
       
   236 
       
   237   Returns:
       
   238     A list of ints corresponding to the parts of the version string
       
   239     between periods.  Example:
       
   240       '1.2.4' -> [1, 2, 4]
       
   241       '1.2.3.4' -> [1, 2, 3, 4]
       
   242 
       
   243   Raises:
       
   244     ValueError if not all the parts are valid integers.
       
   245   """
       
   246   return [int(part) for part in str(release).split('.')]
       
   247 
       
   248 
   223 class UpdateCheck(object):
   249 class UpdateCheck(object):
   224   """Determines if the local SDK is the latest version.
   250   """Determines if the local SDK is the latest version.
   225 
   251 
   226   Nags the user when there are updates to the SDK.  As the SDK becomes
   252   Nags the user when there are updates to the SDK.  As the SDK becomes
   227   more out of date, the language in the nagging gets stronger.  We
   253   more out of date, the language in the nagging gets stronger.  We
   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:
  1022     if reason:
  1200     if reason:
  1023       logging.error(reason)
  1201       logging.error(reason)
  1024       return
  1202       return
  1025 
  1203 
  1026     pos = file_handle.tell()
  1204     pos = file_handle.tell()
  1027     content_hash = self._Hash(file_handle.read())
  1205     content_hash = _Hash(file_handle.read())
  1028     file_handle.seek(pos, 0)
  1206     file_handle.seek(pos, 0)
  1029 
  1207 
  1030     self.files[path] = content_hash
  1208     self.files[path] = content_hash
  1031 
  1209 
  1032   def Begin(self):
  1210   def Begin(self):
  1082               (f, self.files[f]) for f in result.split(LIST_DELIMITER)))
  1260               (f, self.files[f]) for f in result.split(LIST_DELIMITER)))
  1083 
  1261 
  1084     CloneFiles('/api/appversion/cloneblobs', blobs_to_clone, 'static')
  1262     CloneFiles('/api/appversion/cloneblobs', blobs_to_clone, 'static')
  1085     CloneFiles('/api/appversion/clonefiles', files_to_clone, 'application')
  1263     CloneFiles('/api/appversion/clonefiles', files_to_clone, 'application')
  1086 
  1264 
  1087     logging.info('Files to upload: ' + str(files_to_upload))
  1265     logging.debug('Files to upload: %s', files_to_upload)
  1088 
  1266 
  1089     self.files = files_to_upload
  1267     self.files = files_to_upload
  1090     return sorted(files_to_upload.iterkeys())
  1268     return sorted(files_to_upload.iterkeys())
  1091 
  1269 
  1092   def UploadFile(self, path, file_handle):
  1270   def UploadFile(self, path, file_handle):
  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:
  2030                      'log_file',
  2238                      'log_file',
  2031                      'passin',
  2239                      'passin',
  2032                      'email',
  2240                      'email',
  2033                      'debug',
  2241                      'debug',
  2034                      'exporter_opts',
  2242                      'exporter_opts',
       
  2243                      'mapper_opts',
  2035                      'result_db_filename',
  2244                      'result_db_filename',
       
  2245                      'mapper_opts',
       
  2246                      'dry_run',
       
  2247                      'dump',
       
  2248                      'restore',
  2036                      )])
  2249                      )])
  2037 
  2250 
  2038   def PerformDownload(self, run_fn=None):
  2251   def PerformDownload(self, run_fn=None):
  2039     """Performs a datastore download via the bulkloader.
  2252     """Performs a datastore download via the bulkloader.
  2040 
  2253 
  2048     StatusUpdate('Downloading data records.')
  2261     StatusUpdate('Downloading data records.')
  2049 
  2262 
  2050     args = self._MakeLoaderArgs()
  2263     args = self._MakeLoaderArgs()
  2051     args['download'] = True
  2264     args['download'] = True
  2052     args['has_header'] = False
  2265     args['has_header'] = False
       
  2266     args['map'] = False
       
  2267     args['dump'] = False
       
  2268     args['restore'] = False
  2053 
  2269 
  2054     run_fn(args)
  2270     run_fn(args)
  2055 
  2271 
  2056   def PerformUpload(self, run_fn=None):
  2272   def PerformUpload(self, run_fn=None):
  2057     """Performs a datastore upload via the bulkloader.
  2273     """Performs a datastore upload via the bulkloader.
  2065 
  2281 
  2066     StatusUpdate('Uploading data records.')
  2282     StatusUpdate('Uploading data records.')
  2067 
  2283 
  2068     args = self._MakeLoaderArgs()
  2284     args = self._MakeLoaderArgs()
  2069     args['download'] = False
  2285     args['download'] = False
       
  2286     args['map'] = False
       
  2287     args['dump'] = False
       
  2288     args['restore'] = False
  2070 
  2289 
  2071     run_fn(args)
  2290     run_fn(args)
  2072 
  2291 
  2073   def _PerformLoadOptions(self, parser):
  2292   def _PerformLoadOptions(self, parser):
  2074     """Adds options common to 'upload_data' and 'download_data'.
  2293     """Adds options common to 'upload_data' and 'download_data'.
  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: