blob: 4b658e870fb531365e488fcd268761a057f00850 [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
gft_upload: Provides various protocols for uploading report files.
"""
import ftplib
import logging
import os
import re
import sys
import time
import urllib
import urlparse
import xmlrpclib
from common import Error, Shell
# Constants
DEFAULT_RETRY_INTERVAL = 60
DEFAULT_RETRY_TIMEOUT = 30
def RetryCommand(callback, message_prefix, interval):
"""Retries running some commands until success.
Args:
callback: A callback function to execute specified command, and return
if the result is success. Callback accepts a param to hold session
states, including two special values:
'message' to be logged, and 'abort' to return immediately.
message_prefix: Prefix string to be displayed.
interval: Duration (in seconds) between each retry (0 to disable).
"""
results = {}
# Currently we do endless retry, if interval is assigned.
while not callback(results):
message = results.get('message', 'unknown')
abort = results.get('abort', False)
if (not interval) or abort:
raise Error('%s: %s' % message_prefix, message)
logging.error('%s: %s', message_prefix, message)
for i in range(interval, 0, -1):
if i % 10 == 0:
sys.stderr.write(" Retry in %d seconds...\n" % i)
time.sleep(1)
return True
def CustomUpload(source_path, custom_command):
"""Uploads the source file by a customized shell command.
Args:
source_path: File to upload.
custom_command: A shell script command to invoke.
"""
if not custom_command:
raise Error('CustomUpload: need a shell command for customized uploading.')
cmd = '%s %s' % (custom_command, source_path)
logging.debug('CustomUpload: custom: %s', cmd)
if os.system(cmd) != 0:
raise Error('CustomUpload: failed: %s' % cmd)
logging.info('CustomUpload: successfully invoked command: %s.', cmd)
return True
def ShopFloorUpload(source_path, remote_spec,
retry_interval=DEFAULT_RETRY_INTERVAL):
if '#' not in remote_spec:
raise Error('ShopFloorUpload: need a valid parameter in URL#SN format.')
(server_url, _, serial_number) = remote_spec.partition('#')
logging.debug("ShopFloorUpload: [%s].UploadReport(%s, %s)",
server_url, serial_number, source_path)
instance = xmlrpclib.ServerProxy(server_url, allow_none=True, verbose=False)
remote_name = os.path.basename(source_path)
with open(source_path, 'rb') as source_handle:
blob = xmlrpclib.Binary(source_handle.read())
def ShopFloorCallback(result):
try:
instance.UploadReport(serial_number, blob, remote_name)
return True
except xmlrpclib.Fault, err:
result['message'] = 'Remote server fault #%d: %s' % (err.faultCode,
err.faultString)
result['abort'] = True
except:
result['message'] = sys.exc_info()[1]
result['abort'] = False
RetryCommand(ShopFloorCallback, 'ShopFloorUpload', interval=retry_interval)
logging.info('ShopFloorUpload: successfully uploaded to: %s', remote_spec)
return True
def CurlCommand(curl_command, success_string=None, abort_string=None,
retry_interval=DEFAULT_RETRY_INTERVAL):
"""Performs arbitrary curl command with retrying.
Args:
curl_command: Parameters to be invoked with curl.
success_string: String to be recognized as "uploaded successfully".
"""
if not curl_command:
raise Error('CurlCommand: need parameters for curl.')
cmd = 'curl -s -S %s' % curl_command
logging.debug('CurlCommand: %s', cmd)
# man curl(1) for EXIT CODES not related to temporary network failure.
curl_abort_exit_codes = [1, 2, 3, 27, 37, 43, 45, 53, 54, 58, 59, 63]
def CurlCallback(result):
cmd_result = Shell(cmd)
abort = False
message = None
if cmd_result.success:
if abort_string and cmd_result.stdout.find(abort_string) >= 0:
message = "Abort: Found abort pattern: %s" % abort_string
elif ((not success_string) or
(cmd_result.stdout.find(success_string) >= 0)):
return True
else:
message = "Retry: No valid pattern (%s) in response." % success_string
logging.debug("CurlCallback: original response: %s",
' '.join(cmd_result.stdout.splitlines()))
else:
message = '#%d %s' % (cmd_result.status, cmd_result.stderr
if cmd_result.stderr else cmd_result.stdout)
if cmd_result.status in curl_abort_exit_codes:
abort = True
result['abort'] = abort
result['message'] = message
RetryCommand(CurlCallback, 'CurlCommand', interval=retry_interval)
logging.info('CurlCommand: successfully executed: %s', cmd)
return True
def CurlUrlUpload(source_path, params, **kargs):
"""Uploads the source file with URL-like protocols by curl.
Args:
source_path: File to upload.
params: Parameters to be invoked with curl.
retry: Duration (in secnods) for retry.
"""
return CurlCommand('--ftp-ssl -T "%s" %s' % (source_path, params), **kargs)
def CpfeUpload(source_path, cpfe_url, **kargs):
"""Uploads the source file to ChromeOS Partner Front End site.
Args:
source_path: File to upload.
cpfe_url: URL to CPFE.
retry: Duration (in secnods) for retry.
"""
curl_command = '--form "report_file=@%s" %s' % (source_path, cpfe_url)
CPFE_SUCCESS = '[CPFE UPLOAD: OK]'
CPFE_ABORT = '[CPFE UPLOAD: INVALID]'
return CurlCommand(curl_command, success_string=CPFE_SUCCESS,
abort_string=CPFE_ABORT, **kargs)
def FtpUpload(source_path, ftp_url, retry_interval=DEFAULT_RETRY_INTERVAL,
retry_timeout=DEFAULT_RETRY_TIMEOUT):
"""Uploads the source file to a FTP url.
source_path: File to upload.
ftp_url: A ftp url in ftp://user@pass:host:port/path format.
retry_interval: A number in seconds for retry duration. 0 to prevent retry.
retry_timeout: A number in seconds for connection timeout.
Raises:
GFTError: When input url is invalid, or if network issue without retry.
"""
# scheme: ftp, netloc: user:pass@host:port, path: /...
url_struct = urlparse.urlparse(ftp_url)
tokens = re.match('(([^:]*)(:([^@]*))?@)?([^:]*)(:(.*))?', url_struct.netloc)
userid = tokens.group(2)
passwd = tokens.group(4)
host = tokens.group(5)
port = tokens.group(7)
# Check and specify default parameters
if not host:
raise Error('FtpUpload: invalid ftp url: %s' % ftp_url)
if not port:
port = ftplib.FTP_PORT
if not userid:
userid = 'anonymous'
if not passwd:
passwd = ''
# Parse destination path: According to RFC1738, 3.2.2,
# Starting with %2F means absolute path, otherwise relative.
path = urllib.unquote(url_struct.path)
assert path[0] == '/', 'Unknown FTP URL path.'
path = path[1:]
source_name = os.path.split(source_path)[1]
dest_name = os.path.split(path)[1]
logging.debug('source name: %s, dest_name: %s -> %s',
source_name, path, dest_name)
if source_name and (not dest_name):
path = os.path.join(path, source_name)
ftp = ftplib.FTP()
url = 'ftp://%s:%s@%s:%s/ %s' % (userid, passwd, host, port, path)
logging.info('FtpUpload: target is %s', url)
def FtpCallback(result):
try:
ftp.connect(host=host, port=port, timeout=retry_timeout)
return True
except Exception, e:
result['message'] = '%s' % e
RetryCommand(FtpCallback, 'FtpUpload', interval=retry_interval)
# Ready for copying files
logging.debug('FtpUpload: connected, uploading to %s...', path)
ftp.login(user=userid, passwd=passwd)
with open(source_path, 'rb') as fileobj:
ftp.storbinary('STOR %s' % path, fileobj)
logging.debug('FtpUpload: upload complete.')
ftp.quit()
logging.info('FtpUpload: successfully uploaded to %s', ftp_url)
return True
def NoneUpload(source_path, **kargs):
""" Dummy function for bypassing uploads """
logging.warning('NoneUpload%s: skipped uploading %s', kargs, source_path)
return True
def Upload(path, method, **kargs):
"""Uploads a file by given method.
Args:
path: File path to be uploaded.
method: A string to specify the method to upload files.
"""
args = method.split(':', 1)
method = args[0]
param = args[1] if len(args) > 1 else None
if method == 'none':
return NoneUpload(path, **kargs)
elif method == 'custom':
return CustomUpload(path, param, **kargs)
elif method == 'shopfloor':
return ShopFloorUpload(path, param, **kargs)
elif method == 'ftp':
return FtpUpload(path, 'ftp:' + param, **kargs)
elif method == 'ftps':
return CurlUrlUpload(path, '--ftp-ssl-reqd ftp:%s' % param, **kargs)
elif method == 'curl' and param.startswith('ftp://'):
return CurlUrlUpload(path, param, **kargs)
elif method == 'curl' and param.startswith('ftps://'):
return CurlUrlUpload(path, '--ftp-ssl-reqd ' + param, **kargs)
elif method == 'cpfe':
return CpfeUpload(path, param, **kargs)
else:
raise Error('Upload: unknown method: %s' % method)
return False