diff options
-rwxr-xr-x | bin/swift | 57 | ||||
-rw-r--r-- | swiftclient/client.py | 130 | ||||
-rw-r--r-- | tests/test_swiftclient.py | 43 | ||||
-rw-r--r-- | tests/utils.py | 8 | ||||
-rw-r--r-- | tools/pip-requires | 1 |
5 files changed, 135 insertions, 104 deletions
@@ -38,9 +38,9 @@ def get_conn(options): return Connection(options.auth, options.user, options.key, - snet=options.snet, - tenant_name=options.os_tenant_name, - auth_version=options.auth_version) + auth_version=options.auth_version, + os_options=options.os_options, + snet=options.snet) def mkdirs(path): @@ -991,13 +991,6 @@ def parse_args(parser, args, enforce_requires=True): # Use 2.0 auth if none of the old args are present options.auth_version = '2.0' - if options.auth_version in ('2.0', '2') and not \ - options.os_tenant_name and options.user and \ - ':' in options.user: - (options.os_tenant_name, - options.os_username) = options.user.split(':') - options.user = options.os_username - # Use new-style args if old ones not present if not options.auth and options.os_auth_url: options.auth = options.os_auth_url @@ -1006,6 +999,15 @@ def parse_args(parser, args, enforce_requires=True): 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, + 'auth_token': options.os_auth_token, + 'object_storage_url': options.os_storage_url, + } + # Handle trailing '/' in URL if options.auth and not options.auth.endswith('/'): options.auth += '/' @@ -1017,8 +1019,8 @@ 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 to be set or overridden with --os_auth_url, --os_username, ---os_password, or --os_tenant_name.'''.strip('\n')) +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 @@ -1051,26 +1053,43 @@ Example: default=environ.get('ST_AUTH_VERSION', '1.0'), type=str, help='Specify a version for authentication'\ - '(default: 1.0)') + '(default: 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_auth_url', dest='os_auth_url', - default=environ.get('OS_AUTH_URL'), - help='Openstack auth URL. Defaults to env[OS_AUTH_URL].') parser.add_option('--os_username', dest='os_username', default=environ.get('OS_USERNAME'), help='Openstack username. Defaults to env[OS_USERNAME].') + parser.add_option('--os_password', dest='os_password', + default=environ.get('OS_PASSWORD'), + help='Openstack password. Defaults to env[OS_PASSWORD].') + parser.add_option('--os_tenant_id', + default=environ.get('OS_TENANT_ID'), + help='OpenStack tenant ID.' \ + 'Defaults to env[OS_TENANT_ID]') parser.add_option('--os_tenant_name', dest='os_tenant_name', default=environ.get('OS_TENANT_NAME'), help='Openstack tenant name.' \ 'Defaults to env[OS_TENANT_NAME].') - parser.add_option('--os_password', dest='os_password', - default=environ.get('OS_PASSWORD'), - help='Openstack password. Defaults to env[OS_PASSWORD].') + parser.add_option('--os_auth_url', dest='os_auth_url', + default=environ.get('OS_AUTH_URL'), + help='Openstack auth URL. Defaults to env[OS_AUTH_URL].') + parser.add_option('--os_auth_token', dest='os_auth_token', + default=environ.get('OS_AUTH_TOKEN'), + help='Openstack token. Defaults to env[OS_AUTH_TOKEN]') + parser.add_option('--os_storage_url', + dest='os_storage_url', + default=environ.get('OS_STORAGE_URL'), + help='Openstack storage URL.' \ + 'Defaults to env[OS_STORAGE_URL]') + parser.add_option('--os_service_type', + dest='os_service_type', + default=environ.get('OS_SERVICE_TYPE'), + help='Openstack Service type.' \ + 'Defaults to env[OS_SERVICE_TYPE]') parser.disable_interspersed_args() (options, args) = parse_args(parser, argv[1:], enforce_requires=False) parser.enable_interspersed_args() diff --git a/swiftclient/client.py b/swiftclient/client.py index 79e6594..8d7fd03 100644 --- a/swiftclient/client.py +++ b/swiftclient/client.py @@ -20,10 +20,9 @@ Cloud Files client library used internally import socket import os import logging -import httplib from urllib import quote as _quote -from urlparse import urlparse, urlunparse, urljoin +from urlparse import urlparse, urlunparse try: from eventlet.green.httplib import HTTPException, HTTPSConnection @@ -202,7 +201,7 @@ def json_request(method, url, **kwargs): return resp, body -def _get_auth_v1_0(url, user, key, snet): +def get_auth_1_0(url, user, key, snet): parsed, conn = http_connection(url) method = 'GET' conn.request(method, parsed.path, '', @@ -230,36 +229,26 @@ def _get_auth_v1_0(url, user, key, snet): resp.getheader('x-auth-token')) -def _get_auth_v2_0(url, user, tenant_name, key, snet): - body = {'auth': - {'passwordCredentials': {'password': key, 'username': user}, - 'tenantName': tenant_name}} - 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['type'] == 'object-store': - url = service['endpoints'][0]['publicURL'] - token_id = body['access']['token']['id'] - if not url: - raise ClientException("There is no object-store 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_keystoneclient_2_0(auth_url, user, key, os_options): + """ + Authenticate against a auth 2.0 server. -def get_auth(url, user, key, snet=False, tenant_name=None, auth_version="1.0"): + We are using the keystoneclient library for our 2.0 authentication. + """ + from keystoneclient.v2_0 import client as ksclient + _ksclient = ksclient.Client(username=user, + password=key, + tenant_name=os_options.get('tenant_name'), + tenant_id=os_options.get('tenant_id'), + auth_url=auth_url) + service_type = os_options.get('service_type') or 'object-store' + endpoint = _ksclient.service_catalog.url_for( + service_type=service_type, + endpoint_type='publicURL') + return (endpoint, _ksclient.auth_token) + + +def get_auth(auth_url, user, key, **kwargs): """ Get authentication/authorization credentials. @@ -268,28 +257,45 @@ def get_auth(url, user, key, snet=False, tenant_name=None, auth_version="1.0"): 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 auth version, default is 1.0 - :param tenant_name: The tenant/account name, required when connecting - to a auth 2.0 system. - :returns: tuple of (storage URL, auth token) - :raises: ClientException: HTTP GET request to auth URL failed """ - if auth_version in ["1.0", "1"]: - return _get_auth_v1_0(url, user, key, snet) - elif auth_version in ["2.0", "2"]: - if not tenant_name and ':' in user: - (tenant_name, user) = user.split(':') - if not tenant_name: + auth_version = kwargs.get('auth_version', '1') + + if auth_version in ['1.0', '1', 1]: + return get_auth_1_0(auth_url, + user, + key, + kwargs.get('snet')) + + if auth_version in ['2.0', '2', 2]: + + # We are allowing to specify a token/storage-url to re-use + # without having to re-authenticate. + if (kwargs['os_options'].get('object_storage_url') and + kwargs['os_options'].get('auth_token')): + return(kwargs['os_options'].get('object_storage_url'), + kwargs['os_options'].get('auth_token')) + + # We are handling a special use case here when we were + # allowing specifying the account/tenant_name with the -U + # argument + if not kwargs.get('tenant_name') and ':' in user: + (kwargs['os_options']['tenant_name'], + user) = user.split(':') + + # We are allowing to have an tenant_name argument in get_auth + # directly without having os_options + if kwargs.get('tenant_name'): + kwargs['os_options']['tenant_name'] = kwargs['tenant_name'] + + if (not 'tenant_name' in kwargs['os_options']): raise ClientException('No tenant specified') - return _get_auth_v2_0(url, user, tenant_name, key, snet) - else: - raise ClientException('Unknown auth_version %s specified.' - % auth_version) + + (auth_url, token) = get_keystoneclient_2_0(auth_url, user, + key, kwargs['os_options']) + return (auth_url, token) + + raise ClientException('Unknown auth_version %s specified.' + % auth_version) def get_account(url, token, marker=None, limit=None, prefix=None, @@ -898,8 +904,7 @@ class Connection(object): def __init__(self, authurl, user, key, retries=5, preauthurl=None, preauthtoken=None, snet=False, starting_backoff=1, - tenant_name=None, - auth_version="1"): + tenant_name=None, os_options={}, auth_version="1"): """ :param authurl: authentication URL :param user: user name to authenticate as @@ -912,6 +917,9 @@ class Connection(object): :param auth_version: OpenStack auth version, default is 1.0 :param tenant_name: The tenant/account name, required when connecting to a auth 2.0 system. + :param os_options: The OpenStack options which can have tenant_id, + auth_token, service_type, tenant_name, + object_storage_url """ self.authurl = authurl self.user = user @@ -924,13 +932,17 @@ class Connection(object): self.snet = snet self.starting_backoff = starting_backoff self.auth_version = auth_version - self.tenant_name = tenant_name + if tenant_name: + os_options['tenant_name'] = tenant_name + self.os_options = os_options def get_auth(self): - return get_auth(self.authurl, self.user, - self.key, snet=self.snet, - tenant_name=self.tenant_name, - auth_version=self.auth_version) + return get_auth(self.authurl, + self.user, + self.key, + snet=self.snet, + auth_version=self.auth_version, + os_options=self.os_options) def http_connection(self): return http_connection(self.url) diff --git a/tests/test_swiftclient.py b/tests/test_swiftclient.py index b165dee..29233e0 100644 --- a/tests/test_swiftclient.py +++ b/tests/test_swiftclient.py @@ -19,7 +19,7 @@ import unittest from urlparse import urlparse # TODO: mock http connection class with more control over headers -from utils import fake_http_connect +from utils import fake_http_connect, fake_get_keystoneclient_2_0 from swiftclient import client as c @@ -175,42 +175,33 @@ class TestGetAuth(MockHttpTest): self.assertEquals(token, None) def test_auth_v2(self): - def read(*args, **kwargs): - acct_url = 'http://127.0.01/AUTH_FOO' - body = {'access': {'serviceCatalog': - [{u'endpoints': [{'publicURL': acct_url}], - 'type': 'object-store'}], - 'token': {'id': 'XXXXXXX'}}} - return c.json_dumps(body) - c.http_connection = self.fake_http_connection(200, return_read=read) + c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0 url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf', - tenant_name='asdf', auth_version="2.0") + os_options={'tenant_name': 'asdf'}, + auth_version="2.0") self.assertTrue(url.startswith("http")) self.assertTrue(token) def test_auth_v2_no_tenant_name(self): - def read(*args, **kwargs): - acct_url = 'http://127.0.01/AUTH_FOO' - body = {'access': {'serviceCatalog': - [{u'endpoints': [{'publicURL': acct_url}], - 'type': 'object-store'}], - 'token': {'id': 'XXXXXXX'}}} - return c.json_dumps(body) - c.http_connection = self.fake_http_connection(200, return_read=read) + c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0 self.assertRaises(c.ClientException, c.get_auth, 'http://www.tests.com', 'asdf', 'asdf', + os_options={}, auth_version='2.0') def test_auth_v2_with_tenant_user_in_user(self): - def read(*args, **kwargs): - acct_url = 'http://127.0.01/AUTH_FOO' - body = {'access': {'serviceCatalog': - [{u'endpoints': [{'publicURL': acct_url}], - 'type': 'object-store'}], - 'token': {'id': 'XXXXXXX'}}} - return c.json_dumps(body) - c.http_connection = self.fake_http_connection(200, return_read=read) + c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0 url, token = c.get_auth('http://www.test.com', 'foo:bar', 'asdf', + os_options={}, + auth_version="2.0") + self.assertTrue(url.startswith("http")) + self.assertTrue(token) + + def test_auth_v2_tenant_name_no_os_options(self): + c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0 + url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf', + tenant_name='asdf', + os_options={}, auth_version="2.0") self.assertTrue(url.startswith("http")) self.assertTrue(token) diff --git a/tests/utils.py b/tests/utils.py index 6a53cbc..95f6335 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -17,6 +17,14 @@ from httplib import HTTPException from eventlet import Timeout, sleep +def fake_get_keystoneclient_2_0(auth_url, + username, + tenant_name, + password, + service_type='object-store'): + return ("http://url/", "token") + + def fake_http_connect(*code_iter, **kwargs): class FakeConn(object): diff --git a/tools/pip-requires b/tools/pip-requires index 322630e..cda3121 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1 +1,2 @@ simplejson +python-keystoneclient |