diff --git a/bin/swift b/bin/swift index 4138db8b17..49a39db38e 100755 --- a/bin/swift +++ b/bin/swift @@ -33,8 +33,8 @@ import socket from cStringIO import StringIO from re import compile, DOTALL from tokenize import generate_tokens, STRING, NAME, OP -from urllib import quote as _quote, unquote -from urlparse import urlparse, urlunparse +from urllib import quote as _quote +from urlparse import urlparse, urlunparse, urljoin try: from eventlet.green.httplib import HTTPException, HTTPSConnection @@ -69,10 +69,12 @@ def quote(value, safe='/'): try: # simplejson is popular and pretty good from simplejson import loads as json_loads + from simplejson import dumps as json_dumps except ImportError: try: # 2.6 will have a json module in the stdlib from json import loads as json_loads + from json import dumps as json_dumps except ImportError: # fall back on local parser otherwise comments = compile(r'/\*.*\*/|//[^\r\n]*', DOTALL) @@ -174,23 +176,18 @@ def http_connection(url, proxy=None): return parsed, conn -def get_auth(url, user, key, snet=False): +def get_conn(options): """ - Get authentication/authorization credentials. - - The snet parameter is used for Rackspace's ServiceNet internal network - implementation. In this function, it simply adds *snet-* to the beginning - of the host name for the returned storage URL. With Rackspace Cloud Files, - use of this network path causes no bandwidth charges but requires the - client to be running on Rackspace's ServiceNet network. - - :param url: authentication/authorization URL - :param user: user to authenticate as - :param key: key or password for authorization - :param snet: use SERVICENET internal network (see above), default is False - :returns: tuple of (storage URL, auth token) - :raises ClientException: HTTP GET request to auth URL failed + Return a connection building it from the options. """ + return Connection(options.auth, + options.user, + options.key, + snet=options.snet, + auth_version=options.auth_version) + + +def _get_auth_v1_0(url, user, key, snet): parsed, conn = http_connection(url) conn.request('GET', parsed.path, '', {'X-Auth-User': user, 'X-Auth-Key': key}) @@ -211,6 +208,88 @@ def get_auth(url, user, key, snet=False): resp.getheader('x-auth-token')) +def _get_auth_v2_0(url, user, key, snet): + if ':' in user: + tenant, user = user.split(':') + else: + tenant = user + + def json_request(method, token_url, **kwargs): + kwargs.setdefault('headers', {}) + if 'body' in kwargs: + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['body'] = json_dumps(kwargs['body']) + parsed, conn = http_connection(token_url) + conn.request(method, parsed.path, **kwargs) + resp = conn.getresponse() + body = resp.read() + if body: + try: + body = json_loads(body) + except ValueError: + pass + else: + body = None + if resp.status < 200 or resp.status >= 300: + raise ClientException('Auth GET failed', http_scheme=parsed.scheme, + http_host=conn.host, + http_port=conn.port, + http_path=parsed.path, + http_status=resp.status, + http_reason=resp.reason) + return resp, body + body = {"auth": {"tenantName": tenant, + "passwordCredentials": + {"username": user, "password": key}}} + token_url = urljoin(url, "tokens") + resp, body = json_request("POST", token_url, body=body) + token_id = None + try: + url = None + catalogs = body['access']['serviceCatalog'] + for service in catalogs: + if service['name'] == 'swift': + url = service['endpoints'][0]['publicURL'] + token_id = body['access']['token']['id'] + if not url: + raise ClientException("There is no swift endpoint " \ + "on this auth server.") + except(KeyError, IndexError): + raise ClientException("Error while getting answers from auth server") + + if snet: + parsed = list(urlparse(url)) + # Second item in the list is the netloc + parsed[1] = 'snet-' + parsed[1] + url = urlunparse(parsed) + + return url, token_id + + +def get_auth(url, user, key, snet=False, auth_version="1.0"): + """ + Get authentication/authorization credentials. + + The snet parameter is used for Rackspace's ServiceNet internal network + implementation. In this function, it simply adds *snet-* to the beginning + of the host name for the returned storage URL. With Rackspace Cloud Files, + use of this network path causes no bandwidth charges but requires the + client to be running on Rackspace's ServiceNet network. + + :param url: authentication/authorization URL + :param user: user to authenticate as + :param key: key or password for authorization + :param snet: use SERVICENET internal network (see above), default is False + :param auth_version: OpenStack authentication version (default is 1.0) + :returns: tuple of (storage URL, auth token) + :raises ClientException: HTTP GET request to auth URL failed + """ + if auth_version == "1.0" or auth_version == "1": + return _get_auth_v1_0(url, user, key, snet) + elif auth_version == "2.0" or auth_version == "2": + return _get_auth_v2_0(url, user, key, snet) + + def get_account(url, token, marker=None, limit=None, prefix=None, http_conn=None, full_listing=False): """ @@ -318,9 +397,12 @@ def post_account(url, token, headers, http_conn=None): resp.read() if resp.status < 200 or resp.status >= 300: raise ClientException('Account POST failed', - http_scheme=parsed.scheme, http_host=conn.host, - http_port=conn.port, http_path=path, http_status=resp.status, - http_reason=resp.reason) + http_scheme=parsed.scheme, + http_host=conn.host, + http_port=conn.port, + http_path=parsed.path, + http_status=resp.status, + http_reason=resp.reason) def get_container(url, token, container, marker=None, limit=None, @@ -749,7 +831,8 @@ class Connection(object): """Convenience class to make requests that will also retry the request""" def __init__(self, authurl, user, key, retries=5, preauthurl=None, - preauthtoken=None, snet=False, starting_backoff=1): + preauthtoken=None, snet=False, starting_backoff=1, + auth_version=1): """ :param authurl: authenitcation URL :param user: user name to authenticate as @@ -759,6 +842,7 @@ class Connection(object): :param preauthtoken: authentication token (if you have already authenticated) :param snet: use SERVICENET internal network default is False + :param auth_version: Openstack auth version. """ self.authurl = authurl self.user = user @@ -770,9 +854,11 @@ class Connection(object): self.attempts = 0 self.snet = snet self.starting_backoff = starting_backoff + self.auth_version = auth_version def get_auth(self): - return get_auth(self.authurl, self.user, self.key, snet=self.snet) + return get_auth(self.authurl, self.user, self.key, snet=self.snet, + auth_version=self.auth_version) def http_connection(self): return http_connection(self.url) @@ -1066,10 +1152,7 @@ def st_delete(parser, args, print_queue, error_queue): raise error_queue.put('Container %s not found' % repr(container)) - url, token = get_auth(options.auth, options.user, options.key, - snet=options.snet) - create_connection = lambda: Connection(options.auth, options.user, - options.key, preauthurl=url, preauthtoken=token, snet=options.snet) + create_connection = lambda: get_conn(options) object_threads = [QueueFunctionThread(object_queue, _delete_object, create_connection()) for _junk in xrange(10)] for thread in object_threads: @@ -1242,10 +1325,7 @@ def st_download(options, args, print_queue, error_queue): raise error_queue.put('Container %s not found' % repr(container)) - url, token = get_auth(options.auth, options.user, options.key, - snet=options.snet) - create_connection = lambda: Connection(options.auth, options.user, - options.key, preauthurl=url, preauthtoken=token, snet=options.snet) + create_connection = lambda: get_conn(options) object_threads = [QueueFunctionThread(object_queue, _download_object, create_connection()) for _junk in xrange(10)] for thread in object_threads: @@ -1323,8 +1403,8 @@ def st_list(options, args, print_queue, error_queue): error_queue.put('Usage: %s [options] %s' % (basename(argv[0]), st_list_help)) return - conn = Connection(options.auth, options.user, options.key, - snet=options.snet) + + conn = get_conn(options) try: marker = '' while True: @@ -1357,7 +1437,7 @@ stat [container] [object] def st_stat(options, args, print_queue, error_queue): (options, args) = parse_args(parser, args) args = args[1:] - conn = Connection(options.auth, options.user, options.key) + conn = get_conn(options) if not args: try: headers = conn.head_account() @@ -1498,7 +1578,7 @@ def st_post(options, args, print_queue, error_queue): 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 = Connection(options.auth, options.user, options.key) + conn = get_conn(options) if not args: headers = {} for item in options.meta: @@ -1735,10 +1815,7 @@ def st_upload(options, args, print_queue, error_queue): else: object_queue.put({'path': subpath}) - url, token = get_auth(options.auth, options.user, options.key, - snet=options.snet) - create_connection = lambda: Connection(options.auth, options.user, - options.key, preauthurl=url, preauthtoken=token, snet=options.snet) + create_connection = lambda: get_conn(options) object_threads = [QueueFunctionThread(object_queue, _object_job, create_connection()) for _junk in xrange(10)] for thread in object_threads: @@ -1809,6 +1886,12 @@ Example: 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'\ + '(default: 1.0)') parser.add_option('-U', '--user', dest='user', default=environ.get('ST_USER'), help='User name for obtaining an auth token')