#!/usr/bin/python -u # Copyright (c) 2010-2012 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import signal import socket from errno import EEXIST, ENOENT from hashlib import md5 from optparse import OptionParser, SUPPRESS_HELP from os import environ, listdir, makedirs, utime, _exit as os_exit from os.path import basename, dirname, getmtime, getsize, isdir, join from Queue import Empty, Queue from random import shuffle from sys import argv, exc_info, exit, stderr, stdout from threading import current_thread, enumerate as threading_enumerate, Thread from time import sleep, time from traceback import format_exception from urllib import quote, unquote from swiftclient import Connection, ClientException, HTTPException, utils def get_conn(options): """ Return a connection building it from the options. """ return Connection(options.auth, options.user, options.key, auth_version=options.auth_version, os_options=options.os_options, snet=options.snet, cacert=options.os_cacert, insecure=options.insecure) def mkdirs(path): try: makedirs(path) except OSError, err: if err.errno != EEXIST: raise def put_errors_from_threads(threads, error_queue): """ Places any errors from the threads into error_queue. :param threads: A list of QueueFunctionThread instances. :param error_queue: A queue to put error strings into. :returns: True if any errors were found. """ was_error = False for thread in threads: for info in thread.exc_infos: was_error = True if isinstance(info[1], ClientException): error_queue.put(str(info[1])) else: error_queue.put(''.join(format_exception(*info))) return was_error def attempt_graceful_exit(signum, frame): """ Try to gracefully shut down. Sets abort=True on all non-main threads. More importantly, installs the immediate_exit handler on the signal that triggered this handler. If this function is installed as a signal handler for SIGINT, then pressing Ctrl-C once will cause this program to finish operations in progress, then exit. Pressing it again will cause an immediate exit; no cleanup handlers will get called. """ stderr.write("Attempting graceful exit. " "Press Ctrl-C again to exit immediately.\n") main_thread = current_thread() for thread in [t for t in threading_enumerate() if t is not main_thread]: thread.abort = True signal.signal(signum, immediate_exit) def immediate_exit(signum, frame): os_exit(2) class QueueFunctionThread(Thread): def __init__(self, queue, func, *args, **kwargs): """ Calls func for each item in queue; func is called with a queued item as the first arg followed by *args and **kwargs. Use the abort attribute to have the thread empty the queue (without processing) and exit. """ Thread.__init__(self) self.abort = False self.queue = queue self.func = func self.args = args self.kwargs = kwargs self.exc_infos = [] def run(self): while True: try: item = self.queue.get_nowait() except Empty: if self.abort: break sleep(0.01) else: try: if not self.abort: self.func(item, *self.args, **self.kwargs) except Exception: self.exc_infos.append(exc_info()) finally: self.queue.task_done() st_delete_help = ''' delete [options] --all OR delete container [options] [object] [object] ... Deletes everything in the account (with --all), or everything in a container, or a list of objects depending on the args given. Segments of manifest objects will be deleted as well, unless you specify the --leave-segments option.'''.strip('\n') def st_delete(parser, args, print_queue, error_queue): parser.add_option('-a', '--all', action='store_true', dest='yes_all', default=False, help='Indicates that you really want to delete ' 'everything in the account') parser.add_option('', '--leave-segments', action='store_true', dest='leave_segments', default=False, help='Indicates that you want ' 'the segments of manifest objects left alone') parser.add_option('', '--object-threads', type=int, default=10, help='Number of threads to use for ' 'deleting objects') parser.add_option('', '--container-threads', type=int, default=10, help='Number of threads to use for ' 'deleting containers') (options, args) = parse_args(parser, args) args = args[1:] if (not args and not options.yes_all) or (args and options.yes_all): error_queue.put('Usage: %s [options] %s' % (basename(argv[0]), st_delete_help)) return def _delete_segment((container, obj), conn): conn.delete_object(container, obj) if options.verbose: if conn.attempts > 2: print_queue.put('%s/%s [after %d attempts]' % (container, obj, conn.attempts)) else: print_queue.put('%s/%s' % (container, obj)) object_queue = Queue(10000) def _delete_object((container, obj), conn): try: old_manifest = None if not options.leave_segments: try: old_manifest = conn.head_object(container, obj).get( 'x-object-manifest') except ClientException, err: if err.http_status != 404: raise conn.delete_object(container, obj) if old_manifest: segment_queue = Queue(10000) scontainer, sprefix = old_manifest.split('/', 1) scontainer = unquote(scontainer) sprefix = unquote(sprefix) for delobj in conn.get_container(scontainer, prefix=sprefix)[1]: segment_queue.put((scontainer, delobj['name'])) if not segment_queue.empty(): segment_threads = [QueueFunctionThread(segment_queue, _delete_segment, create_connection()) for _junk in xrange(options.object_threads)] for thread in segment_threads: thread.start() while not segment_queue.empty(): sleep(0.01) for thread in segment_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) put_errors_from_threads(segment_threads, error_queue) if options.verbose: path = options.yes_all and join(container, obj) or obj if path[:1] in ('/', '\\'): path = path[1:] if conn.attempts > 1: print_queue.put('%s [after %d attempts]' % (path, conn.attempts)) else: print_queue.put(path) except ClientException, err: if err.http_status != 404: raise error_queue.put('Object %s not found' % repr('%s/%s' % (container, obj))) container_queue = Queue(10000) def _delete_container(container, conn): try: marker = '' had_objects = False while True: objects = [o['name'] for o in conn.get_container(container, marker=marker)[1]] if not objects: break had_objects = True for obj in objects: object_queue.put((container, obj)) marker = objects[-1] if had_objects: # By using join() instead of empty() we should avoid most # occurrences of 409 below. object_queue.join() attempts = 1 while True: try: conn.delete_container(container) break except ClientException, err: if err.http_status != 409: raise if attempts > 10: raise attempts += 1 sleep(1) except ClientException, err: if err.http_status != 404: raise error_queue.put('Container %s not found' % repr(container)) create_connection = lambda: get_conn(options) object_threads = \ [QueueFunctionThread(object_queue, _delete_object, create_connection()) for _junk in xrange(options.object_threads)] for thread in object_threads: thread.start() container_threads = \ [QueueFunctionThread(container_queue, _delete_container, create_connection()) for _junk in xrange(options.container_threads)] for thread in container_threads: thread.start() if not args: conn = create_connection() try: marker = '' while True: containers = \ [c['name'] for c in conn.get_account(marker=marker)[1]] if not containers: break for container in containers: container_queue.put(container) marker = containers[-1] while not container_queue.empty(): sleep(0.01) while not object_queue.empty(): sleep(0.01) except ClientException, err: if err.http_status != 404: raise error_queue.put('Account not found') elif len(args) == 1: if '/' in args[0]: print >> stderr, 'WARNING: / in container name; you might have ' \ 'meant %r instead of %r.' % \ (args[0].replace('/', ' ', 1), args[0]) conn = create_connection() _delete_container(args[0], conn) else: for obj in args[1:]: object_queue.put((args[0], obj)) while not container_queue.empty(): sleep(0.01) for thread in container_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) put_errors_from_threads(container_threads, error_queue) while not object_queue.empty(): sleep(0.01) for thread in object_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) put_errors_from_threads(object_threads, error_queue) st_download_help = ''' download --all OR download container [options] [object] [object] ... Downloads everything in the account (with --all), or everything in a container, or a list of objects depending on the args given. For a single object download, you may use the -o [--output] option to redirect the output to a specific file or if "-" then just redirect to stdout.'''.strip('\n') def st_download(parser, args, print_queue, error_queue): parser.add_option('-a', '--all', action='store_true', dest='yes_all', default=False, help='Indicates that you really want to download ' 'everything in the account') parser.add_option('-m', '--marker', dest='marker', default='', help='Marker to use when starting a container or ' 'account download') parser.add_option('-o', '--output', dest='out_file', help='For a single ' 'file download, stream the output to an alternate location ') parser.add_option('', '--object-threads', type=int, default=10, help='Number of threads to use for ' 'downloading objects') parser.add_option('', '--container-threads', type=int, default=10, help='Number of threads to use for ' 'listing containers') parser.add_option('', '--no-download', action='store_true', default=False, help="Perform download(s), but don't " "actually write anything to disk") (options, args) = parse_args(parser, args) args = args[1:] if options.out_file == '-': options.verbose = 0 if options.out_file and len(args) != 2: exit('-o option only allowed for single file downloads') if (not args and not options.yes_all) or (args and options.yes_all): error_queue.put('Usage: %s [options] %s' % (basename(argv[0]), st_download_help)) return object_queue = Queue(10000) def _download_object(queue_arg, conn): if len(queue_arg) == 2: container, obj = queue_arg out_file = None elif len(queue_arg) == 3: container, obj, out_file = queue_arg else: raise Exception("Invalid queue_arg length of %s" % len(queue_arg)) try: start_time = time() headers, body = \ conn.get_object(container, obj, resp_chunk_size=65536) header_receipt = time() content_type = headers.get('content-type') if 'content-length' in headers: content_length = int(headers.get('content-length')) else: content_length = None etag = headers.get('etag') path = options.yes_all and join(container, obj) or obj if path[:1] in ('/', '\\'): path = path[1:] md5sum = None make_dir = not options.no_download and out_file != "-" if content_type.split(';', 1)[0] == 'text/directory': if make_dir and not isdir(path): mkdirs(path) read_length = 0 if 'x-object-manifest' not in headers: md5sum = md5() for chunk in body: read_length += len(chunk) if md5sum: md5sum.update(chunk) else: dirpath = dirname(path) if make_dir and dirpath and not isdir(dirpath): mkdirs(dirpath) if not options.no_download: if out_file == "-": fp = stdout elif out_file: fp = open(out_file, 'wb') else: fp = open(path, 'wb') read_length = 0 if 'x-object-manifest' not in headers: md5sum = md5() for chunk in body: if not options.no_download: fp.write(chunk) read_length += len(chunk) if md5sum: md5sum.update(chunk) if not options.no_download: fp.close() if md5sum and md5sum.hexdigest() != etag: error_queue.put('%s: md5sum != etag, %s != %s' % (path, md5sum.hexdigest(), etag)) if content_length is not None and read_length != content_length: error_queue.put('%s: read_length != content_length, %d != %d' % (path, read_length, content_length)) if 'x-object-meta-mtime' in headers and not options.out_file \ and not options.no_download: mtime = float(headers['x-object-meta-mtime']) utime(path, (mtime, mtime)) if options.verbose: finish_time = time() time_str = 'headers %.3fs, total %.3fs, %.3fs MB/s' % ( header_receipt - start_time, finish_time - start_time, float(read_length) / (finish_time - start_time) / 1000000) if conn.attempts > 1: print_queue.put('%s [%s after %d attempts]' % (path, time_str, conn.attempts)) else: print_queue.put('%s [%s]' % (path, time_str)) except ClientException, err: if err.http_status != 404: raise error_queue.put('Object %s not found' % repr('%s/%s' % (container, obj))) container_queue = Queue(10000) def _download_container(container, conn): try: marker = options.marker while True: objects = [o['name'] for o in conn.get_container(container, marker=marker)[1]] if not objects: break marker = objects[-1] shuffle(objects) for obj in objects: object_queue.put((container, obj)) except ClientException, err: if err.http_status != 404: raise error_queue.put('Container %s not found' % repr(container)) create_connection = lambda: get_conn(options) object_threads = [QueueFunctionThread(object_queue, _download_object, create_connection()) for _junk in xrange(options.object_threads)] for thread in object_threads: thread.start() container_threads = [QueueFunctionThread(container_queue, _download_container, create_connection()) for _junk in xrange(options.container_threads)] for thread in container_threads: thread.start() if not args: conn = create_connection() try: marker = options.marker while True: containers = [c['name'] for c in conn.get_account(marker=marker)[1]] if not containers: break marker = containers[-1] shuffle(containers) for container in containers: container_queue.put(container) except ClientException, err: if err.http_status != 404: raise error_queue.put('Account not found') elif len(args) == 1: if '/' in args[0]: print >> stderr, 'WARNING: / in container name; you might have ' \ 'meant %r instead of %r.' % \ (args[0].replace('/', ' ', 1), args[0]) _download_container(args[0], create_connection()) else: if len(args) == 2: obj = args[1] object_queue.put((args[0], obj, options.out_file)) else: for obj in args[1:]: object_queue.put((args[0], obj)) while not container_queue.empty(): sleep(0.01) for thread in container_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) put_errors_from_threads(container_threads, error_queue) while not object_queue.empty(): sleep(0.01) for thread in object_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) put_errors_from_threads(object_threads, error_queue) st_list_help = ''' list [options] [container] Lists the containers for the account or the objects for a container. -p or --prefix is an option that will only list items beginning with that prefix. -d or --delimiter is option (for container listings only) that will roll up items with the given delimiter (see Cloud Files general documentation for what this means). '''.strip('\n') def st_list(parser, args, print_queue, error_queue): parser.add_option('-p', '--prefix', dest='prefix', help='Will only list ' 'items beginning with the prefix') parser.add_option('-d', '--delimiter', dest='delimiter', help='Will roll ' 'up items with the given delimiter (see Cloud Files general ' 'documentation for what this means)') (options, args) = parse_args(parser, args) args = args[1:] if options.delimiter and not args: exit('-d option only allowed for container listings') if len(args) > 1: error_queue.put('Usage: %s [options] %s' % (basename(argv[0]), st_list_help)) return conn = get_conn(options) try: marker = '' while True: if not args: items = \ conn.get_account(marker=marker, prefix=options.prefix)[1] else: items = conn.get_container(args[0], marker=marker, prefix=options.prefix, delimiter=options.delimiter)[1] if not items: break for item in items: print_queue.put(item.get('name', item.get('subdir'))) marker = items[-1].get('name', items[-1].get('subdir')) except ClientException, err: if err.http_status != 404: raise if not args: error_queue.put('Account not found') else: error_queue.put('Container %s not found' % repr(args[0])) st_stat_help = ''' stat [container] [object] Displays information for the account, container, or object depending on the args given (if any).'''.strip('\n') def st_stat(parser, args, print_queue, error_queue): (options, args) = parse_args(parser, args) args = args[1:] conn = get_conn(options) if not args: try: headers = conn.head_account() if options.verbose > 1: print_queue.put(''' StorageURL: %s Auth Token: %s '''.strip('\n') % (conn.url, conn.token)) container_count = int(headers.get('x-account-container-count', 0)) object_count = int(headers.get('x-account-object-count', 0)) bytes_used = int(headers.get('x-account-bytes-used', 0)) print_queue.put(''' Account: %s Containers: %d Objects: %d Bytes: %d'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], container_count, object_count, bytes_used)) for key, value in headers.items(): if key.startswith('x-account-meta-'): print_queue.put('%10s: %s' % ('Meta %s' % key[len('x-account-meta-'):].title(), value)) for key, value in headers.items(): if not key.startswith('x-account-meta-') and key not in ( 'content-length', 'date', 'x-account-container-count', 'x-account-object-count', 'x-account-bytes-used'): print_queue.put( '%10s: %s' % (key.title(), value)) except ClientException, err: if err.http_status != 404: raise error_queue.put('Account not found') elif len(args) == 1: if '/' in args[0]: print >> stderr, 'WARNING: / in container name; you might have ' \ 'meant %r instead of %r.' % \ (args[0].replace('/', ' ', 1), args[0]) try: headers = conn.head_container(args[0]) object_count = int(headers.get('x-container-object-count', 0)) bytes_used = int(headers.get('x-container-bytes-used', 0)) print_queue.put(''' Account: %s Container: %s Objects: %d Bytes: %d Read ACL: %s Write ACL: %s Sync To: %s Sync Key: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], object_count, bytes_used, headers.get('x-container-read', ''), headers.get('x-container-write', ''), headers.get('x-container-sync-to', ''), headers.get('x-container-sync-key', ''))) for key, value in headers.items(): if key.startswith('x-container-meta-'): print_queue.put('%9s: %s' % ('Meta %s' % key[len('x-container-meta-'):].title(), value)) for key, value in headers.items(): if not key.startswith('x-container-meta-') and key not in ( 'content-length', 'date', 'x-container-object-count', 'x-container-bytes-used', 'x-container-read', 'x-container-write', 'x-container-sync-to', 'x-container-sync-key'): print_queue.put( '%9s: %s' % (key.title(), value)) except ClientException, err: if err.http_status != 404: raise error_queue.put('Container %s not found' % repr(args[0])) elif len(args) == 2: try: headers = conn.head_object(args[0], args[1]) print_queue.put(''' Account: %s Container: %s Object: %s Content Type: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], args[1], headers.get('content-type'))) if 'content-length' in headers: print_queue.put('Content Length: %s' % headers['content-length']) if 'last-modified' in headers: print_queue.put(' Last Modified: %s' % headers['last-modified']) if 'etag' in headers: print_queue.put(' ETag: %s' % headers['etag']) if 'x-object-manifest' in headers: print_queue.put(' Manifest: %s' % headers['x-object-manifest']) for key, value in headers.items(): if key.startswith('x-object-meta-'): print_queue.put('%14s: %s' % ('Meta %s' % key[len('x-object-meta-'):].title(), value)) for key, value in headers.items(): if not key.startswith('x-object-meta-') and key not in ( 'content-type', 'content-length', 'last-modified', 'etag', 'date', 'x-object-manifest'): print_queue.put( '%14s: %s' % (key.title(), value)) except ClientException, err: if err.http_status != 404: raise error_queue.put('Object %s not found' % repr('%s/%s' % (args[0], args[1]))) else: error_queue.put('Usage: %s [options] %s' % (basename(argv[0]), st_stat_help)) st_post_help = ''' post [options] [container] [object] Updates meta information for the account, container, or object depending on the args given. If the container is not found, it will be created automatically; but this is not true for accounts and objects. Containers also allow the -r (or --read-acl) and -w (or --write-acl) options. The -m or --meta option is allowed on all and used to define the user meta data items to set in the form Name:Value. This option can be repeated. Example: post -m Color:Blue -m Size:Large'''.strip('\n') def st_post(parser, args, print_queue, error_queue): parser.add_option('-r', '--read-acl', dest='read_acl', help='Sets the ' 'Read ACL for containers. Quick summary of ACL syntax: .r:*, ' '.r:-.example.com, .r:www.example.com, account1, account2:user2') parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the ' 'Write ACL for containers. Quick summary of ACL syntax: account1, ' 'account2:user2') parser.add_option('-t', '--sync-to', dest='sync_to', help='Sets the ' 'Sync To for containers, for multi-cluster replication.') parser.add_option('-k', '--sync-key', dest='sync_key', help='Sets the ' 'Sync Key for containers, for multi-cluster replication.') parser.add_option('-m', '--meta', action='append', dest='meta', default=[], help='Sets a meta data item with the syntax name:value. This option ' 'may be repeated. Example: -m Color:Blue -m Size:Large') (options, args) = parse_args(parser, args) args = args[1:] if (options.read_acl or options.write_acl or options.sync_to or options.sync_key) and not args: exit('-r, -w, -t, and -k options only allowed for containers') conn = get_conn(options) if not args: headers = split_headers(options.meta, 'X-Account-Meta-', error_queue) try: conn.post_account(headers=headers) except ClientException, err: if err.http_status != 404: raise error_queue.put('Account not found') elif len(args) == 1: if '/' in args[0]: print >> stderr, 'WARNING: / in container name; you might have ' \ 'meant %r instead of %r.' % \ (args[0].replace('/', ' ', 1), args[0]) headers = split_headers(options.meta, 'X-Container-Meta-', error_queue) if options.read_acl is not None: headers['X-Container-Read'] = options.read_acl if options.write_acl is not None: headers['X-Container-Write'] = options.write_acl if options.sync_to is not None: headers['X-Container-Sync-To'] = options.sync_to if options.sync_key is not None: headers['X-Container-Sync-Key'] = options.sync_key try: conn.post_container(args[0], headers=headers) except ClientException, err: if err.http_status != 404: raise conn.put_container(args[0], headers=headers) elif len(args) == 2: headers = split_headers(options.meta, 'X-Object-Meta-', error_queue) try: conn.post_object(args[0], args[1], headers=headers) except ClientException, err: if err.http_status != 404: raise error_queue.put('Object %s not found' % repr('%s/%s' % (args[0], args[1]))) else: error_queue.put('Usage: %s [options] %s' % (basename(argv[0]), st_post_help)) st_upload_help = ''' upload [options] container file_or_directory [file_or_directory] [...] Uploads to the given container the files and directories specified by the remaining args. -c or --changed is an option that will only upload files that have changed since the last upload. -S or --segment-size and --leave-segments are options as well (see --help for more). '''.strip('\n') def st_upload(parser, args, print_queue, error_queue): parser.add_option('-c', '--changed', action='store_true', dest='changed', default=False, help='Will only upload files that have changed since ' 'the last upload') parser.add_option('-S', '--segment-size', dest='segment_size', help='Will ' 'upload files in segments no larger than and then create a ' '"manifest" file that will download all the segments as if it were ' 'the original file. The segments will be uploaded to a ' '_segments container so as to not pollute the main ' ' listings.') parser.add_option('', '--leave-segments', action='store_true', dest='leave_segments', default=False, help='Indicates that you want ' 'the older segments of manifest objects left alone (in the case of ' 'overwrites)') parser.add_option('', '--object-threads', type=int, default=10, help='Number of threads to use for ' 'uploading full objects') parser.add_option('', '--segment-threads', type=int, default=10, help='Number of threads to use for ' 'uploading object segments') (options, args) = parse_args(parser, args) args = args[1:] if len(args) < 2: error_queue.put('Usage: %s [options] %s' % (basename(argv[0]), st_upload_help)) return object_queue = Queue(10000) def _segment_job(job, conn): if job.get('delete', False): conn.delete_object(job['container'], job['obj']) else: fp = open(job['path'], 'rb') fp.seek(job['segment_start']) conn.put_object(job.get('container', args[0] + '_segments'), job['obj'], fp, content_length=job['segment_size']) if options.verbose and 'log_line' in job: if conn.attempts > 1: print_queue.put('%s [after %d attempts]' % (job['log_line'], conn.attempts)) else: print_queue.put(job['log_line']) def _object_job(job, conn): path = job['path'] container = job.get('container', args[0]) dir_marker = job.get('dir_marker', False) try: obj = path if obj.startswith('./') or obj.startswith('.\\'): obj = obj[2:] if obj.startswith('/'): obj = obj[1:] put_headers = {'x-object-meta-mtime': "%f" % getmtime(path)} if dir_marker: if options.changed: try: headers = conn.head_object(container, obj) ct = headers.get('content-type') cl = int(headers.get('content-length')) et = headers.get('etag') mt = headers.get('x-object-meta-mtime') if ct.split(';', 1)[0] == 'text/directory' and \ cl == 0 and \ et == 'd41d8cd98f00b204e9800998ecf8427e' and \ mt == put_headers['x-object-meta-mtime']: return except ClientException, err: if err.http_status != 404: raise conn.put_object(container, obj, '', content_length=0, content_type='text/directory', headers=put_headers) else: # We need to HEAD all objects now in case we're overwriting a # manifest object and need to delete the old segments # ourselves. old_manifest = None if options.changed or not options.leave_segments: try: headers = conn.head_object(container, obj) cl = int(headers.get('content-length')) mt = headers.get('x-object-meta-mtime') if options.changed and cl == getsize(path) and \ mt == put_headers['x-object-meta-mtime']: return if not options.leave_segments: old_manifest = headers.get('x-object-manifest') except ClientException, err: if err.http_status != 404: raise # Don't do segment job if object is not big enough if options.segment_size and \ getsize(path) > int(options.segment_size): full_size = getsize(path) segment_queue = Queue(10000) segment_threads = [QueueFunctionThread(segment_queue, _segment_job, create_connection()) for _junk in xrange(options.segment_threads)] for thread in segment_threads: thread.start() segment = 0 segment_start = 0 while segment_start < full_size: segment_size = int(options.segment_size) if segment_start + segment_size > full_size: segment_size = full_size - segment_start segment_queue.put({'path': path, 'obj': '%s/%s/%s/%s/%08d' % (obj, put_headers['x-object-meta-mtime'], full_size, options.segment_size, segment), 'segment_start': segment_start, 'segment_size': segment_size, 'log_line': '%s segment %s' % (obj, segment)}) segment += 1 segment_start += segment_size while not segment_queue.empty(): sleep(0.01) for thread in segment_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) if put_errors_from_threads(segment_threads, error_queue): raise ClientException('Aborting manifest creation ' 'because not all segments could be uploaded. %s/%s' % (container, obj)) new_object_manifest = '%s_segments/%s/%s/%s/%s' % ( quote(container), quote(obj), put_headers['x-object-meta-mtime'], full_size, options.segment_size) if old_manifest == new_object_manifest: old_manifest = None put_headers['x-object-manifest'] = new_object_manifest conn.put_object(container, obj, '', content_length=0, headers=put_headers) else: conn.put_object(container, obj, open(path, 'rb'), content_length=getsize(path), headers=put_headers) if old_manifest: segment_queue = Queue(10000) scontainer, sprefix = old_manifest.split('/', 1) scontainer = unquote(scontainer) sprefix = unquote(sprefix) for delobj in conn.get_container(scontainer, prefix=sprefix)[1]: segment_queue.put({'delete': True, 'container': scontainer, 'obj': delobj['name']}) if not segment_queue.empty(): segment_threads = [QueueFunctionThread(segment_queue, _segment_job, create_connection()) for _junk in xrange(options.segment_threads)] for thread in segment_threads: thread.start() while not segment_queue.empty(): sleep(0.01) for thread in segment_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) put_errors_from_threads(segment_threads, error_queue) if options.verbose: if conn.attempts > 1: print_queue.put( '%s [after %d attempts]' % (obj, conn.attempts)) else: print_queue.put(obj) except OSError, err: if err.errno != ENOENT: raise error_queue.put('Local file %s not found' % repr(path)) def _upload_dir(path): names = listdir(path) if not names: object_queue.put({'path': path, 'dir_marker': True}) else: for name in listdir(path): subpath = join(path, name) if isdir(subpath): _upload_dir(subpath) else: object_queue.put({'path': subpath}) create_connection = lambda: get_conn(options) object_threads = [QueueFunctionThread(object_queue, _object_job, create_connection()) for _junk in xrange(options.object_threads)] for thread in object_threads: thread.start() conn = create_connection() # Try to create the container, just in case it doesn't exist. If this # fails, it might just be because the user doesn't have container PUT # permissions, so we'll ignore any error. If there's really a problem, # it'll surface on the first object PUT. try: conn.put_container(args[0]) if options.segment_size is not None: conn.put_container(args[0] + '_segments') except ClientException, err: msg = ' '.join(str(x) for x in (err.http_status, err.http_reason)) if err.http_response_content: if msg: msg += ': ' msg += err.http_response_content[:60] error_queue.put( 'Error trying to create container %r: %s' % (args[0], msg)) except Exception, err: error_queue.put( 'Error trying to create container %r: %s' % (args[0], err)) try: for arg in args[1:]: if isdir(arg): _upload_dir(arg) else: object_queue.put({'path': arg}) while not object_queue.empty(): sleep(0.01) for thread in object_threads: thread.abort = True while thread.isAlive(): thread.join(0.01) put_errors_from_threads(object_threads, error_queue) except ClientException, err: if err.http_status != 404: raise error_queue.put('Account not found') def split_headers(options, prefix='', error_queue=None): """ Splits 'Key: Value' strings and returns them as a dictionary. :param options: An array of 'Key: Value' strings :param prefix: String to prepend to all of the keys in the dictionary. :param error_queue: Queue for thread safe error reporting. """ headers = {} for item in options: split_item = item.split(':', 1) if len(split_item) == 2: headers[prefix + split_item[0]] = split_item[1] else: error_string = "Metadata parameter %s must contain a ':'.\n%s" \ % (item, st_post_help) if error_queue: error_queue.put(error_string) else: exit(error_string) return headers def parse_args(parser, args, enforce_requires=True): if not args: args = ['-h'] (options, args) = parser.parse_args(args) if (not (options.auth and options.user and options.key)): # Use 2.0 auth if none of the old args are present options.auth_version = '2.0' # Use new-style args if old ones not present if not options.auth and options.os_auth_url: options.auth = options.os_auth_url if not options.user and options.os_username: options.user = options.os_username if not options.key and options.os_password: options.key = options.os_password # Specific OpenStack options options.os_options = { 'tenant_id': options.os_tenant_id, 'tenant_name': options.os_tenant_name, 'service_type': options.os_service_type, 'endpoint_type': options.os_endpoint_type, 'auth_token': options.os_auth_token, 'object_storage_url': options.os_storage_url, 'region_name': options.os_region_name, } if enforce_requires and \ not (options.auth and options.user and options.key): exit(''' Auth version 1.0 requires ST_AUTH, ST_USER, and ST_KEY environment variables to be set or overridden with -A, -U, or -K. Auth version 2.0 requires OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, and OS_TENANT_NAME OS_TENANT_ID to be set or overridden with --os-auth-url, --os-username, --os-password, --os-tenant-name or os-tenant-id.'''.strip('\n')) return options, args if __name__ == '__main__': parser = OptionParser(version='%prog 1.0', usage=''' Usage: %%prog [options] [args] Commands: %(st_stat_help)s %(st_list_help)s %(st_upload_help)s %(st_post_help)s %(st_download_help)s %(st_delete_help)s Example: %%prog -A https://auth.api.rackspacecloud.com/v1.0 -U user -K key stat '''.strip('\n') % globals()) parser.add_option('-s', '--snet', action='store_true', dest='snet', default=False, help='Use SERVICENET internal network') parser.add_option('-v', '--verbose', action='count', dest='verbose', default=1, help='Print more info') parser.add_option('-q', '--quiet', action='store_const', dest='verbose', const=0, default=1, help='Suppress status output') parser.add_option('-A', '--auth', dest='auth', default=environ.get('ST_AUTH'), help='URL for obtaining an auth token') parser.add_option('-V', '--auth-version', dest='auth_version', default=environ.get('ST_AUTH_VERSION', '1.0'), type=str, help='Specify a version for authentication. ' 'Defaults to 1.0.') parser.add_option('-U', '--user', dest='user', default=environ.get('ST_USER'), help='User name for obtaining an auth token.') parser.add_option('-K', '--key', dest='key', default=environ.get('ST_KEY'), help='Key for obtaining an auth token.') parser.add_option('--os-username', metavar='', default=environ.get('OS_USERNAME'), help='Openstack username. Defaults to env[OS_USERNAME].') parser.add_option('--os_username', help=SUPPRESS_HELP) parser.add_option('--os-password', metavar='', default=environ.get('OS_PASSWORD'), help='Openstack password. Defaults to env[OS_PASSWORD].') parser.add_option('--os_password', help=SUPPRESS_HELP) parser.add_option('--os-tenant-id', metavar='', default=environ.get('OS_TENANT_ID'), help='OpenStack tenant ID. Defaults to env[OS_TENANT_ID]') parser.add_option('--os_tenant_id', help=SUPPRESS_HELP) parser.add_option('--os-tenant-name', metavar='', default=environ.get('OS_TENANT_NAME'), help='Openstack tenant name. ' 'Defaults to env[OS_TENANT_NAME].') parser.add_option('--os_tenant_name', help=SUPPRESS_HELP) parser.add_option('--os-auth-url', metavar='', default=environ.get('OS_AUTH_URL'), help='Openstack auth URL. Defaults to env[OS_AUTH_URL].') parser.add_option('--os_auth_url', help=SUPPRESS_HELP) parser.add_option('--os-auth-token', metavar='', default=environ.get('OS_AUTH_TOKEN'), help='Openstack token. Defaults to env[OS_AUTH_TOKEN]') parser.add_option('--os_auth_token', help=SUPPRESS_HELP) parser.add_option('--os-storage-url', metavar='', default=environ.get('OS_STORAGE_URL'), help='Openstack storage URL. ' 'Defaults to env[OS_STORAGE_URL]') parser.add_option('--os_storage_url', help=SUPPRESS_HELP) parser.add_option('--os-region-name', metavar='', default=environ.get('OS_REGION_NAME'), help='Openstack region name. ' 'Defaults to env[OS_REGION_NAME]') parser.add_option('--os_region_name', help=SUPPRESS_HELP) parser.add_option('--os-service-type', metavar='', default=environ.get('OS_SERVICE_TYPE'), help='Openstack Service type. ' 'Defaults to env[OS_SERVICE_TYPE]') parser.add_option('--os_service_type', help=SUPPRESS_HELP) parser.add_option('--os-endpoint-type', metavar='', default=environ.get('OS_ENDPOINT_TYPE'), help='Openstack Endpoint type. ' \ 'Defaults to env[OS_ENDPOINT_TYPE]') parser.add_option('--os-cacert', metavar='', default=environ.get('OS_CACERT'), help='Specify a CA bundle file to use in verifying a ' 'TLS (https) server certificate. ' 'Defaults to env[OS_CACERT]') default_val = utils.config_true_value( environ.get('SWIFTCLIENT_INSECURE')) parser.add_option('--insecure', action="store_true", dest="insecure", default=default_val, help='Allow swiftclient to access insecure keystone ' 'server. The keystone\'s certificate will not ' 'be verified. ' 'Defaults to env[SWIFTCLIENT_INSECURE] ' '(set to \'true\' to enable).') parser.disable_interspersed_args() (options, args) = parse_args(parser, argv[1:], enforce_requires=False) parser.enable_interspersed_args() commands = ('delete', 'download', 'list', 'post', 'stat', 'upload') if not args or args[0] not in commands: parser.print_usage() if args: exit('no such command: %s' % args[0]) exit() signal.signal(signal.SIGINT, attempt_graceful_exit) print_queue = Queue(10000) def _print(item): if isinstance(item, unicode): item = item.encode('utf8') print item print_thread = QueueFunctionThread(print_queue, _print) print_thread.start() error_count = 0 error_queue = Queue(10000) def _error(item): global error_count error_count += 1 if isinstance(item, unicode): item = item.encode('utf8') print >> stderr, item error_thread = QueueFunctionThread(error_queue, _error) error_thread.start() try: parser.usage = globals()['st_%s_help' % args[0]] try: globals()['st_%s' % args[0]](parser, argv[1:], print_queue, error_queue) except (ClientException, HTTPException, socket.error), err: error_queue.put(str(err)) while not print_queue.empty(): sleep(0.01) print_thread.abort = True while print_thread.isAlive(): print_thread.join(0.01) while not error_queue.empty(): sleep(0.01) error_thread.abort = True while error_thread.isAlive(): error_thread.join(0.01) if error_count: exit(1) except (SystemExit, Exception): for thread in threading_enumerate(): thread.abort = True raise