| # 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 |