2012-05-08 11:17:04 +01:00
|
|
|
# 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.
|
|
|
|
|
|
|
|
"""
|
2013-11-01 11:40:44 -07:00
|
|
|
OpenStack Swift client library used internally
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
import socket
|
2014-01-24 17:40:16 +01:00
|
|
|
import requests
|
2012-05-09 15:50:17 +01:00
|
|
|
import logging
|
2013-03-05 16:56:02 +00:00
|
|
|
import warnings
|
2014-11-06 14:10:13 +00:00
|
|
|
try:
|
|
|
|
from simplejson import loads as json_loads
|
|
|
|
except ImportError:
|
|
|
|
from json import loads as json_loads
|
2012-05-09 15:50:17 +01:00
|
|
|
|
2014-01-24 17:40:16 +01:00
|
|
|
from distutils.version import StrictVersion
|
|
|
|
from requests.exceptions import RequestException, SSLError
|
2014-03-24 18:20:22 +01:00
|
|
|
from six.moves.urllib.parse import quote as _quote
|
|
|
|
from six.moves.urllib.parse import urlparse, urlunparse
|
2013-09-04 19:32:09 +01:00
|
|
|
from time import sleep, time
|
2014-03-24 18:16:51 +01:00
|
|
|
import six
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2014-03-28 15:45:37 -07:00
|
|
|
from swiftclient import version as swiftclient_version
|
2014-04-15 14:50:10 -04:00
|
|
|
from swiftclient.exceptions import ClientException
|
2014-10-29 10:11:29 +00:00
|
|
|
from swiftclient.utils import LengthWrapper, ReadableToIterable
|
2013-08-15 18:46:33 -07:00
|
|
|
|
2014-03-25 08:21:21 +00:00
|
|
|
AUTH_VERSIONS_V1 = ('1.0', '1', 1)
|
|
|
|
AUTH_VERSIONS_V2 = ('2.0', '2', 2)
|
|
|
|
AUTH_VERSIONS_V3 = ('3.0', '3', 3)
|
2014-11-06 14:10:13 +00:00
|
|
|
USER_METADATA_TYPE = tuple('x-%s-meta-' % type_ for type_ in
|
|
|
|
('container', 'account', 'object'))
|
2014-03-25 08:21:21 +00:00
|
|
|
|
2013-08-15 18:46:33 -07:00
|
|
|
try:
|
|
|
|
from logging import NullHandler
|
|
|
|
except ImportError:
|
|
|
|
class NullHandler(logging.Handler):
|
|
|
|
def handle(self, record):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def emit(self, record):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def createLock(self):
|
|
|
|
self.lock = None
|
|
|
|
|
2014-01-24 17:40:16 +01:00
|
|
|
# requests version 1.2.3 try to encode headers in ascii, preventing
|
|
|
|
# utf-8 encoded header to be 'prepared'
|
|
|
|
if StrictVersion(requests.__version__) < StrictVersion('2.0.0'):
|
|
|
|
from requests.structures import CaseInsensitiveDict
|
|
|
|
|
|
|
|
def prepare_unicode_headers(self, headers):
|
|
|
|
if headers:
|
|
|
|
self.headers = CaseInsensitiveDict(headers)
|
|
|
|
else:
|
|
|
|
self.headers = CaseInsensitiveDict()
|
|
|
|
requests.models.PreparedRequest.prepare_headers = prepare_unicode_headers
|
|
|
|
|
2012-05-09 15:50:17 +01:00
|
|
|
logger = logging.getLogger("swiftclient")
|
2013-08-15 18:46:33 -07:00
|
|
|
logger.addHandler(NullHandler())
|
2012-05-09 15:50:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
def http_log(args, kwargs, resp, body):
|
2013-10-15 09:46:46 -07:00
|
|
|
if not logger.isEnabledFor(logging.INFO):
|
2012-05-09 15:50:17 +01:00
|
|
|
return
|
|
|
|
|
2013-10-15 09:46:46 -07:00
|
|
|
# create and log equivalent curl command
|
2012-05-09 15:50:17 +01:00
|
|
|
string_parts = ['curl -i']
|
|
|
|
for element in args:
|
2012-11-16 15:23:25 +10:00
|
|
|
if element == 'HEAD':
|
|
|
|
string_parts.append(' -I')
|
|
|
|
elif element in ('GET', 'POST', 'PUT'):
|
2012-05-09 15:50:17 +01:00
|
|
|
string_parts.append(' -X %s' % element)
|
|
|
|
else:
|
|
|
|
string_parts.append(' %s' % element)
|
|
|
|
if 'headers' in kwargs:
|
|
|
|
for element in kwargs['headers']:
|
|
|
|
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
|
|
|
string_parts.append(header)
|
|
|
|
|
2013-10-15 09:46:46 -07:00
|
|
|
# log response as debug if good, or info if error
|
|
|
|
if resp.status < 300:
|
|
|
|
log_method = logger.debug
|
|
|
|
else:
|
|
|
|
log_method = logger.info
|
2012-05-09 15:50:17 +01:00
|
|
|
|
2014-05-19 19:52:35 +02:00
|
|
|
log_method("REQ: %s", "".join(string_parts))
|
|
|
|
log_method("RESP STATUS: %s %s", resp.status, resp.reason)
|
2013-10-15 09:46:46 -07:00
|
|
|
log_method("RESP HEADERS: %s", resp.getheaders())
|
2012-05-09 15:50:17 +01:00
|
|
|
if body:
|
2013-10-15 09:46:46 -07:00
|
|
|
log_method("RESP BODY: %s", body)
|
2012-05-09 15:50:17 +01:00
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
def quote(value, safe='/'):
|
|
|
|
"""
|
2014-03-31 12:40:24 +02:00
|
|
|
Patched version of urllib.quote that encodes utf8 strings before quoting.
|
|
|
|
On Python 3, call directly urllib.parse.quote().
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
2014-03-31 12:40:24 +02:00
|
|
|
if six.PY3:
|
|
|
|
return _quote(value, safe=safe)
|
2012-10-11 15:04:00 +03:00
|
|
|
value = encode_utf8(value)
|
2014-03-24 18:16:51 +01:00
|
|
|
if isinstance(value, bytes):
|
2012-10-11 15:04:00 +03:00
|
|
|
return _quote(value, safe)
|
|
|
|
else:
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def encode_utf8(value):
|
2014-03-24 18:16:51 +01:00
|
|
|
if isinstance(value, six.text_type):
|
2012-05-08 11:17:04 +01:00
|
|
|
value = value.encode('utf8')
|
2012-10-11 15:04:00 +03:00
|
|
|
return value
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2014-11-06 14:10:13 +00:00
|
|
|
def encode_meta_headers(headers):
|
|
|
|
"""Only encode metadata headers keys"""
|
|
|
|
ret = {}
|
|
|
|
for header, value in headers.items():
|
|
|
|
value = encode_utf8(value)
|
|
|
|
header = header.lower()
|
|
|
|
|
|
|
|
if (isinstance(header, six.string_types)
|
|
|
|
and header.startswith(USER_METADATA_TYPE)):
|
|
|
|
header = encode_utf8(header)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2014-11-06 14:10:13 +00:00
|
|
|
ret[header] = value
|
|
|
|
return ret
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2014-11-06 14:10:13 +00:00
|
|
|
|
|
|
|
class HTTPConnection(object):
|
2014-01-24 17:40:16 +01:00
|
|
|
def __init__(self, url, proxy=None, cacert=None, insecure=False,
|
2014-03-28 15:45:37 -07:00
|
|
|
ssl_compression=False, default_user_agent=None):
|
2014-01-24 17:40:16 +01:00
|
|
|
"""
|
|
|
|
Make an HTTPConnection or HTTPSConnection
|
|
|
|
|
|
|
|
:param url: url to connect to
|
|
|
|
:param proxy: proxy to connect through, if any; None by default; str
|
|
|
|
of the format 'http://127.0.0.1:8888' to set one
|
|
|
|
:param cacert: A CA bundle file to use in verifying a TLS server
|
|
|
|
certificate.
|
|
|
|
:param insecure: Allow to access servers without checking SSL certs.
|
|
|
|
The server's certificate will not be verified.
|
|
|
|
:param ssl_compression: SSL compression should be disabled by default
|
|
|
|
and this setting is not usable as of now. The
|
|
|
|
parameter is kept for backward compatibility.
|
2014-03-28 15:45:37 -07:00
|
|
|
:param default_user_agent: Set the User-Agent header on every request.
|
|
|
|
If set to None (default), the user agent
|
|
|
|
will be "python-swiftclient-<version>". This
|
|
|
|
may be overridden on a per-request basis by
|
|
|
|
explicitly setting the user-agent header on
|
|
|
|
a call to request().
|
2014-01-24 17:40:16 +01:00
|
|
|
:raises ClientException: Unable to handle protocol scheme
|
|
|
|
"""
|
|
|
|
self.url = url
|
|
|
|
self.parsed_url = urlparse(url)
|
|
|
|
self.host = self.parsed_url.netloc
|
|
|
|
self.port = self.parsed_url.port
|
|
|
|
self.requests_args = {}
|
2014-02-18 18:11:58 +01:00
|
|
|
self.request_session = requests.Session()
|
2014-01-24 17:40:16 +01:00
|
|
|
if self.parsed_url.scheme not in ('http', 'https'):
|
|
|
|
raise ClientException("Unsupported scheme")
|
|
|
|
self.requests_args['verify'] = not insecure
|
2014-02-13 23:33:01 -08:00
|
|
|
if cacert and not insecure:
|
2014-01-24 17:40:16 +01:00
|
|
|
# verify requests parameter is used to pass the CA_BUNDLE file
|
|
|
|
# see: http://docs.python-requests.org/en/latest/user/advanced/
|
|
|
|
self.requests_args['verify'] = cacert
|
|
|
|
if proxy:
|
|
|
|
proxy_parsed = urlparse(proxy)
|
|
|
|
if not proxy_parsed.scheme:
|
|
|
|
raise ClientException("Proxy's missing scheme")
|
|
|
|
self.requests_args['proxies'] = {
|
|
|
|
proxy_parsed.scheme: '%s://%s' % (
|
|
|
|
proxy_parsed.scheme, proxy_parsed.netloc
|
|
|
|
)
|
|
|
|
}
|
|
|
|
self.requests_args['stream'] = True
|
2014-03-28 15:45:37 -07:00
|
|
|
if default_user_agent is None:
|
|
|
|
default_user_agent = \
|
|
|
|
'python-swiftclient-%s' % swiftclient_version.version_string
|
|
|
|
self.default_user_agent = default_user_agent
|
2014-01-24 17:40:16 +01:00
|
|
|
|
|
|
|
def _request(self, *arg, **kwarg):
|
|
|
|
""" Final wrapper before requests call, to be patched in tests """
|
2014-02-18 18:11:58 +01:00
|
|
|
return self.request_session.request(*arg, **kwarg)
|
2014-01-24 17:40:16 +01:00
|
|
|
|
2014-03-30 10:08:09 -07:00
|
|
|
def request(self, method, full_path, data=None, headers=None, files=None):
|
2014-01-24 17:40:16 +01:00
|
|
|
""" Encode url and header, then call requests.request """
|
2014-03-30 10:08:09 -07:00
|
|
|
if headers is None:
|
|
|
|
headers = {}
|
2014-04-15 16:28:50 -04:00
|
|
|
else:
|
2014-11-06 14:10:13 +00:00
|
|
|
headers = encode_meta_headers(headers)
|
2014-04-15 16:28:50 -04:00
|
|
|
|
2014-03-28 15:45:37 -07:00
|
|
|
# set a default User-Agent header if it wasn't passed in
|
|
|
|
if 'user-agent' not in headers:
|
|
|
|
headers['user-agent'] = self.default_user_agent
|
2014-04-24 17:04:21 +02:00
|
|
|
url = "%s://%s%s" % (
|
2014-01-24 17:40:16 +01:00
|
|
|
self.parsed_url.scheme,
|
|
|
|
self.parsed_url.netloc,
|
2014-04-24 17:04:21 +02:00
|
|
|
full_path)
|
2014-01-24 17:40:16 +01:00
|
|
|
self.resp = self._request(method, url, headers=headers, data=data,
|
|
|
|
files=files, **self.requests_args)
|
|
|
|
return self.resp
|
|
|
|
|
2014-03-30 10:08:09 -07:00
|
|
|
def putrequest(self, full_path, data=None, headers=None, files=None):
|
2014-01-24 17:40:16 +01:00
|
|
|
"""
|
|
|
|
Use python-requests files upload
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2014-01-24 17:40:16 +01:00
|
|
|
:param data: Use data generator for chunked-transfer
|
|
|
|
:param files: Use files for default transfer
|
|
|
|
"""
|
|
|
|
return self.request('PUT', full_path, data, headers, files)
|
|
|
|
|
|
|
|
def getresponse(self):
|
|
|
|
""" Adapt requests response to httplib interface """
|
|
|
|
self.resp.status = self.resp.status_code
|
|
|
|
old_getheader = self.resp.raw.getheader
|
|
|
|
|
|
|
|
def getheaders():
|
|
|
|
return self.resp.headers.items()
|
|
|
|
|
|
|
|
def getheader(k, v=None):
|
|
|
|
return old_getheader(k.lower(), v)
|
|
|
|
|
2015-01-21 14:38:02 -06:00
|
|
|
def releasing_read(*args, **kwargs):
|
|
|
|
kwargs['decode_content'] = True
|
|
|
|
chunk = self.resp.raw.read(*args, **kwargs)
|
|
|
|
if not chunk:
|
|
|
|
# NOTE(sigmavirus24): Release the connection back to the
|
|
|
|
# urllib3's connection pool. This will reduce the number of
|
|
|
|
# log messages seen in bug #1341777. This does not actually
|
|
|
|
# close a socket. It will also prevent people from being
|
|
|
|
# mislead as to the cause of a bug as in bug #1424732.
|
|
|
|
self.resp.close()
|
|
|
|
return chunk
|
|
|
|
|
2014-01-24 17:40:16 +01:00
|
|
|
self.resp.getheaders = getheaders
|
|
|
|
self.resp.getheader = getheader
|
2015-01-21 14:38:02 -06:00
|
|
|
self.resp.read = releasing_read
|
|
|
|
|
2014-01-24 17:40:16 +01:00
|
|
|
return self.resp
|
|
|
|
|
|
|
|
|
|
|
|
def http_connection(*arg, **kwarg):
|
|
|
|
""" :returns: tuple of (parsed url, connection object) """
|
|
|
|
conn = HTTPConnection(*arg, **kwarg)
|
|
|
|
return conn.parsed_url, conn
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2014-02-13 23:33:01 -08:00
|
|
|
def get_auth_1_0(url, user, key, snet, **kwargs):
|
2015-02-07 08:51:40 +08:00
|
|
|
cacert = kwargs.get('cacert', None)
|
2014-02-13 23:33:01 -08:00
|
|
|
insecure = kwargs.get('insecure', False)
|
2015-02-07 08:51:40 +08:00
|
|
|
parsed, conn = http_connection(url, cacert=cacert, insecure=insecure)
|
2012-05-09 15:50:17 +01:00
|
|
|
method = 'GET'
|
|
|
|
conn.request(method, parsed.path, '',
|
2012-05-08 11:17:04 +01:00
|
|
|
{'X-Auth-User': user, 'X-Auth-Key': key})
|
|
|
|
resp = conn.getresponse()
|
2012-05-08 14:10:14 +01:00
|
|
|
body = resp.read()
|
2012-05-09 15:50:17 +01:00
|
|
|
http_log((url, method,), {}, resp, body)
|
2013-05-22 18:17:59 -07:00
|
|
|
url = resp.getheader('x-storage-url')
|
2012-05-08 14:10:14 +01:00
|
|
|
|
|
|
|
# There is a side-effect on current Rackspace 1.0 server where a
|
|
|
|
# bad URL would get you that document page and a 200. We error out
|
|
|
|
# if we don't have a x-storage-url header and if we get a body.
|
|
|
|
if resp.status < 200 or resp.status >= 300 or (body and not url):
|
2012-05-08 11:17:04 +01:00
|
|
|
raise ClientException('Auth GET failed', http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=parsed.path,
|
|
|
|
http_status=resp.status, http_reason=resp.reason)
|
2012-05-08 11:17:04 +01:00
|
|
|
if snet:
|
|
|
|
parsed = list(urlparse(url))
|
|
|
|
# Second item in the list is the netloc
|
|
|
|
netloc = parsed[1]
|
|
|
|
parsed[1] = 'snet-' + netloc
|
|
|
|
url = urlunparse(parsed)
|
|
|
|
return url, resp.getheader('x-storage-token',
|
2012-06-15 12:18:08 -07:00
|
|
|
resp.getheader('x-auth-token'))
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2012-12-05 13:18:27 +09:00
|
|
|
def get_keystoneclient_2_0(auth_url, user, key, os_options, **kwargs):
|
2014-03-25 08:21:21 +00:00
|
|
|
# this function is only here to preserve the historic 'public'
|
|
|
|
# interface of this module
|
|
|
|
kwargs.update({'auth_version': '2.0'})
|
|
|
|
return get_auth_keystone(auth_url, user, key, os_options, **kwargs)
|
|
|
|
|
|
|
|
|
2014-09-22 11:52:44 +01:00
|
|
|
def _import_keystone_client(auth_version):
|
|
|
|
# the attempted imports are encapsulated in this function to allow
|
|
|
|
# mocking for tests
|
2013-01-21 10:20:06 +01:00
|
|
|
try:
|
2014-03-25 08:21:21 +00:00
|
|
|
if auth_version in AUTH_VERSIONS_V3:
|
|
|
|
from keystoneclient.v3 import client as ksclient
|
|
|
|
else:
|
|
|
|
from keystoneclient.v2_0 import client as ksclient
|
2013-01-21 10:20:06 +01:00
|
|
|
from keystoneclient import exceptions
|
2014-11-13 12:18:41 +00:00
|
|
|
# prevent keystoneclient warning us that it has no log handlers
|
|
|
|
logging.getLogger('keystoneclient').addHandler(NullHandler())
|
2014-09-22 11:52:44 +01:00
|
|
|
return ksclient, exceptions
|
2013-01-21 10:20:06 +01:00
|
|
|
except ImportError:
|
2014-11-25 13:11:51 +01:00
|
|
|
raise ClientException('''
|
2014-03-25 08:21:21 +00:00
|
|
|
Auth versions 2.0 and 3 require python-keystoneclient, install it or use Auth
|
2013-01-21 10:20:06 +01:00
|
|
|
version 1.0 which requires ST_AUTH, ST_USER, and ST_KEY environment
|
|
|
|
variables to be set or overridden with -A, -U, or -K.''')
|
|
|
|
|
2014-09-22 11:52:44 +01:00
|
|
|
|
|
|
|
def get_auth_keystone(auth_url, user, key, os_options, **kwargs):
|
|
|
|
"""
|
|
|
|
Authenticate against a keystone server.
|
|
|
|
|
|
|
|
We are using the keystoneclient library for authentication.
|
|
|
|
"""
|
|
|
|
|
|
|
|
insecure = kwargs.get('insecure', False)
|
|
|
|
auth_version = kwargs.get('auth_version', '2.0')
|
|
|
|
debug = logger.isEnabledFor(logging.DEBUG) and True or False
|
|
|
|
|
|
|
|
ksclient, exceptions = _import_keystone_client(auth_version)
|
|
|
|
|
2012-09-17 16:42:16 +01:00
|
|
|
try:
|
2014-03-25 08:21:21 +00:00
|
|
|
_ksclient = ksclient.Client(
|
|
|
|
username=user,
|
|
|
|
password=key,
|
|
|
|
tenant_name=os_options.get('tenant_name'),
|
|
|
|
tenant_id=os_options.get('tenant_id'),
|
|
|
|
user_id=os_options.get('user_id'),
|
|
|
|
user_domain_name=os_options.get('user_domain_name'),
|
|
|
|
user_domain_id=os_options.get('user_domain_id'),
|
|
|
|
project_name=os_options.get('project_name'),
|
|
|
|
project_id=os_options.get('project_id'),
|
|
|
|
project_domain_name=os_options.get('project_domain_name'),
|
|
|
|
project_domain_id=os_options.get('project_domain_id'),
|
|
|
|
debug=debug,
|
|
|
|
cacert=kwargs.get('cacert'),
|
|
|
|
auth_url=auth_url, insecure=insecure)
|
2012-09-17 16:42:16 +01:00
|
|
|
except exceptions.Unauthorized:
|
2014-03-25 08:21:21 +00:00
|
|
|
msg = 'Unauthorized. Check username, password and tenant name/id.'
|
|
|
|
if auth_version in AUTH_VERSIONS_V3:
|
2014-10-29 10:11:29 +00:00
|
|
|
msg = ('Unauthorized. Check username/id, password, '
|
|
|
|
'tenant name/id and user/tenant domain name/id.')
|
2014-03-25 08:21:21 +00:00
|
|
|
raise ClientException(msg)
|
2013-04-25 23:19:52 +02:00
|
|
|
except exceptions.AuthorizationFailure as err:
|
2012-09-17 16:42:16 +01:00
|
|
|
raise ClientException('Authorization Failure. %s' % err)
|
2012-07-04 21:46:02 +02:00
|
|
|
service_type = os_options.get('service_type') or 'object-store'
|
2012-08-28 10:25:42 -04:00
|
|
|
endpoint_type = os_options.get('endpoint_type') or 'publicURL'
|
2012-09-05 15:55:53 +01:00
|
|
|
try:
|
|
|
|
endpoint = _ksclient.service_catalog.url_for(
|
|
|
|
attr='region',
|
|
|
|
filter_value=os_options.get('region_name'),
|
|
|
|
service_type=service_type,
|
|
|
|
endpoint_type=endpoint_type)
|
|
|
|
except exceptions.EndpointNotFound:
|
|
|
|
raise ClientException('Endpoint for %s not found - '
|
|
|
|
'have you specified a region?' % service_type)
|
2014-11-06 14:10:13 +00:00
|
|
|
return endpoint, _ksclient.auth_token
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2012-07-04 21:46:02 +02:00
|
|
|
def get_auth(auth_url, user, key, **kwargs):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Get authentication/authorization credentials.
|
|
|
|
|
2014-10-24 01:02:53 -07:00
|
|
|
:kwarg auth_version: the api version of the supplied auth params
|
|
|
|
:kwarg os_options: a dict, the openstack idenity service options
|
|
|
|
|
|
|
|
:returns: a tuple, (storage_url, token)
|
|
|
|
|
|
|
|
N.B. if the optional os_options paramater includes an non-empty
|
|
|
|
'object_storage_url' key it will override the the default storage url
|
|
|
|
returned by the auth service.
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
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.
|
2012-07-04 21:46:02 +02:00
|
|
|
"""
|
|
|
|
auth_version = kwargs.get('auth_version', '1')
|
2012-09-19 15:47:36 +00:00
|
|
|
os_options = kwargs.get('os_options', {})
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-06-28 21:26:54 -07:00
|
|
|
storage_url, token = None, None
|
2015-02-07 08:51:40 +08:00
|
|
|
cacert = kwargs.get('cacert', None)
|
2014-02-13 23:33:01 -08:00
|
|
|
insecure = kwargs.get('insecure', False)
|
2014-03-25 08:21:21 +00:00
|
|
|
if auth_version in AUTH_VERSIONS_V1:
|
2013-06-28 21:26:54 -07:00
|
|
|
storage_url, token = get_auth_1_0(auth_url,
|
|
|
|
user,
|
|
|
|
key,
|
2014-02-13 23:33:01 -08:00
|
|
|
kwargs.get('snet'),
|
2015-02-07 08:51:40 +08:00
|
|
|
cacert=cacert,
|
2014-02-13 23:33:01 -08:00
|
|
|
insecure=insecure)
|
2014-03-25 08:21:21 +00:00
|
|
|
elif auth_version in AUTH_VERSIONS_V2 + AUTH_VERSIONS_V3:
|
|
|
|
# We are handling a special use case here where the user argument
|
|
|
|
# specifies both the user name and tenant name in the form tenant:user
|
|
|
|
if user and not kwargs.get('tenant_name') and ':' in user:
|
2014-10-29 10:11:29 +00:00
|
|
|
os_options['tenant_name'], user = user.split(':')
|
2012-07-04 21:46:02 +02:00
|
|
|
|
|
|
|
# We are allowing to have an tenant_name argument in get_auth
|
|
|
|
# directly without having os_options
|
|
|
|
if kwargs.get('tenant_name'):
|
2012-09-19 15:47:36 +00:00
|
|
|
os_options['tenant_name'] = kwargs['tenant_name']
|
2012-07-04 21:46:02 +02:00
|
|
|
|
2014-03-25 08:21:21 +00:00
|
|
|
if not (os_options.get('tenant_name') or os_options.get('tenant_id')
|
|
|
|
or os_options.get('project_name')
|
|
|
|
or os_options.get('project_id')):
|
2014-10-20 18:00:01 +01:00
|
|
|
if auth_version in AUTH_VERSIONS_V2:
|
|
|
|
raise ClientException('No tenant specified')
|
|
|
|
raise ClientException('No project name or project id specified.')
|
2012-07-04 21:46:02 +02:00
|
|
|
|
2014-03-25 08:21:21 +00:00
|
|
|
storage_url, token = get_auth_keystone(auth_url, user,
|
|
|
|
key, os_options,
|
|
|
|
cacert=cacert,
|
|
|
|
insecure=insecure,
|
|
|
|
auth_version=auth_version)
|
2013-06-28 21:26:54 -07:00
|
|
|
else:
|
|
|
|
raise ClientException('Unknown auth_version %s specified.'
|
|
|
|
% auth_version)
|
2012-07-04 21:46:02 +02:00
|
|
|
|
2013-06-28 21:26:54 -07:00
|
|
|
# Override storage url, if necessary
|
|
|
|
if os_options.get('object_storage_url'):
|
|
|
|
return os_options['object_storage_url'], token
|
|
|
|
else:
|
|
|
|
return storage_url, token
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def store_response(resp, response_dict):
|
|
|
|
"""
|
|
|
|
store information about an operation into a dict
|
|
|
|
|
|
|
|
:param resp: an http response object containing the response
|
|
|
|
headers
|
|
|
|
:param response_dict: a dict into which are placed the
|
|
|
|
status, reason and a dict of lower-cased headers
|
|
|
|
"""
|
|
|
|
if response_dict is not None:
|
|
|
|
resp_headers = {}
|
|
|
|
for header, value in resp.getheaders():
|
|
|
|
resp_headers[header.lower()] = value
|
|
|
|
|
|
|
|
response_dict['status'] = resp.status
|
|
|
|
response_dict['reason'] = resp.reason
|
|
|
|
response_dict['headers'] = resp_headers
|
|
|
|
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
def get_account(url, token, marker=None, limit=None, prefix=None,
|
2013-05-02 00:15:11 +09:00
|
|
|
end_marker=None, http_conn=None, full_listing=False):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Get a listing of containers for the account.
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param marker: marker query
|
|
|
|
:param limit: limit query
|
|
|
|
:param prefix: prefix query
|
2013-05-02 00:15:11 +09:00
|
|
|
:param end_marker: end_marker query
|
2012-05-08 11:17:04 +01:00
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:param full_listing: if True, return a full listing, else returns a max
|
|
|
|
of 10000 listings
|
|
|
|
:returns: a tuple of (response headers, a list of containers) The response
|
|
|
|
headers will be a dict and all header names will be lowercase.
|
|
|
|
:raises ClientException: HTTP GET request failed
|
|
|
|
"""
|
|
|
|
if not http_conn:
|
|
|
|
http_conn = http_connection(url)
|
|
|
|
if full_listing:
|
2013-05-02 00:15:11 +09:00
|
|
|
rv = get_account(url, token, marker, limit, prefix,
|
|
|
|
end_marker, http_conn)
|
2012-05-08 11:17:04 +01:00
|
|
|
listing = rv[1]
|
|
|
|
while listing:
|
|
|
|
marker = listing[-1]['name']
|
2014-11-06 14:10:13 +00:00
|
|
|
listing = get_account(url, token, marker, limit, prefix,
|
|
|
|
end_marker, http_conn)[1]
|
2012-05-08 11:17:04 +01:00
|
|
|
if listing:
|
|
|
|
rv[1].extend(listing)
|
|
|
|
return rv
|
|
|
|
parsed, conn = http_conn
|
|
|
|
qs = 'format=json'
|
|
|
|
if marker:
|
|
|
|
qs += '&marker=%s' % quote(marker)
|
|
|
|
if limit:
|
|
|
|
qs += '&limit=%d' % limit
|
|
|
|
if prefix:
|
|
|
|
qs += '&prefix=%s' % quote(prefix)
|
2013-05-02 00:15:11 +09:00
|
|
|
if end_marker:
|
|
|
|
qs += '&end_marker=%s' % quote(end_marker)
|
2012-05-09 15:50:17 +01:00
|
|
|
full_path = '%s?%s' % (parsed.path, qs)
|
|
|
|
headers = {'X-Auth-Token': token}
|
2012-11-16 15:23:25 +10:00
|
|
|
method = 'GET'
|
|
|
|
conn.request(method, full_path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
2012-05-09 15:50:17 +01:00
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(("%s?%s" % (url, qs), method,), {'headers': headers}, resp, body)
|
2012-05-09 15:50:17 +01:00
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
resp_headers = {}
|
|
|
|
for header, value in resp.getheaders():
|
|
|
|
resp_headers[header.lower()] = value
|
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Account GET failed', http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=parsed.path,
|
|
|
|
http_query=qs, http_status=resp.status,
|
|
|
|
http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status == 204:
|
|
|
|
return resp_headers, []
|
2012-05-09 15:50:17 +01:00
|
|
|
return resp_headers, json_loads(body)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
def head_account(url, token, http_conn=None):
|
|
|
|
"""
|
|
|
|
Get account stats.
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:returns: a dict containing the response's headers (all header names will
|
|
|
|
be lowercase)
|
|
|
|
:raises ClientException: HTTP HEAD request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
2012-05-09 15:50:17 +01:00
|
|
|
method = "HEAD"
|
|
|
|
headers = {'X-Auth-Token': token}
|
|
|
|
conn.request(method, parsed.path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-05-09 15:50:17 +01:00
|
|
|
http_log((url, method,), {'headers': headers}, resp, body)
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Account HEAD failed', http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=parsed.path,
|
|
|
|
http_status=resp.status, http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp_headers = {}
|
|
|
|
for header, value in resp.getheaders():
|
|
|
|
resp_headers[header.lower()] = value
|
|
|
|
return resp_headers
|
|
|
|
|
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def post_account(url, token, headers, http_conn=None, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Update an account's metadata.
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param headers: additional headers to include in the request
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
2012-05-08 11:17:04 +01:00
|
|
|
:raises ClientException: HTTP POST request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
2012-05-09 15:50:17 +01:00
|
|
|
method = 'POST'
|
2012-05-08 11:17:04 +01:00
|
|
|
headers['X-Auth-Token'] = token
|
2012-05-09 15:50:17 +01:00
|
|
|
conn.request(method, parsed.path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-05-09 15:50:17 +01:00
|
|
|
http_log((url, method,), {'headers': headers}, resp, body)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
store_response(resp, response_dict)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Account POST failed',
|
|
|
|
http_scheme=parsed.scheme,
|
|
|
|
http_host=conn.host,
|
|
|
|
http_path=parsed.path,
|
|
|
|
http_status=resp.status,
|
|
|
|
http_reason=resp.reason,
|
|
|
|
http_response_content=body)
|
|
|
|
|
|
|
|
|
|
|
|
def get_container(url, token, container, marker=None, limit=None,
|
2013-05-02 00:15:11 +09:00
|
|
|
prefix=None, delimiter=None, end_marker=None,
|
|
|
|
path=None, http_conn=None,
|
2012-05-08 11:17:04 +01:00
|
|
|
full_listing=False):
|
|
|
|
"""
|
|
|
|
Get a listing of objects for the container.
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name to get a listing for
|
|
|
|
:param marker: marker query
|
|
|
|
:param limit: limit query
|
|
|
|
:param prefix: prefix query
|
2013-05-02 00:15:11 +09:00
|
|
|
:param delimiter: string to delimit the queries on
|
|
|
|
:param end_marker: marker query
|
|
|
|
:param path: path query (equivalent: "delimiter=/" and "prefix=path/")
|
2012-05-08 11:17:04 +01:00
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:param full_listing: if True, return a full listing, else returns a max
|
|
|
|
of 10000 listings
|
|
|
|
:returns: a tuple of (response headers, a list of objects) The response
|
|
|
|
headers will be a dict and all header names will be lowercase.
|
|
|
|
:raises ClientException: HTTP GET request failed
|
|
|
|
"""
|
|
|
|
if not http_conn:
|
|
|
|
http_conn = http_connection(url)
|
|
|
|
if full_listing:
|
|
|
|
rv = get_container(url, token, container, marker, limit, prefix,
|
2013-05-02 00:15:11 +09:00
|
|
|
delimiter, end_marker, path, http_conn)
|
2012-05-08 11:17:04 +01:00
|
|
|
listing = rv[1]
|
|
|
|
while listing:
|
|
|
|
if not delimiter:
|
|
|
|
marker = listing[-1]['name']
|
|
|
|
else:
|
|
|
|
marker = listing[-1].get('name', listing[-1].get('subdir'))
|
|
|
|
listing = get_container(url, token, container, marker, limit,
|
2013-05-02 00:15:11 +09:00
|
|
|
prefix, delimiter, end_marker, path,
|
|
|
|
http_conn)[1]
|
2012-05-08 11:17:04 +01:00
|
|
|
if listing:
|
|
|
|
rv[1].extend(listing)
|
|
|
|
return rv
|
|
|
|
parsed, conn = http_conn
|
2013-05-02 00:15:11 +09:00
|
|
|
cont_path = '%s/%s' % (parsed.path, quote(container))
|
2012-05-08 11:17:04 +01:00
|
|
|
qs = 'format=json'
|
|
|
|
if marker:
|
|
|
|
qs += '&marker=%s' % quote(marker)
|
|
|
|
if limit:
|
|
|
|
qs += '&limit=%d' % limit
|
|
|
|
if prefix:
|
|
|
|
qs += '&prefix=%s' % quote(prefix)
|
|
|
|
if delimiter:
|
|
|
|
qs += '&delimiter=%s' % quote(delimiter)
|
2013-05-02 00:15:11 +09:00
|
|
|
if end_marker:
|
|
|
|
qs += '&end_marker=%s' % quote(end_marker)
|
|
|
|
if path:
|
|
|
|
qs += '&path=%s' % quote(path)
|
2012-05-09 15:50:17 +01:00
|
|
|
headers = {'X-Auth-Token': token}
|
|
|
|
method = 'GET'
|
2013-05-02 00:15:11 +09:00
|
|
|
conn.request(method, '%s?%s' % (cont_path, qs), '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
2012-05-09 15:50:17 +01:00
|
|
|
body = resp.read()
|
2013-11-29 20:46:22 -05:00
|
|
|
http_log(('%(url)s%(cont_path)s?%(qs)s' %
|
|
|
|
{'url': url.replace(parsed.path, ''),
|
|
|
|
'cont_path': cont_path,
|
|
|
|
'qs': qs}, method,),
|
|
|
|
{'headers': headers}, resp, body)
|
2012-05-09 15:50:17 +01:00
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Container GET failed',
|
2012-06-15 12:18:08 -07:00
|
|
|
http_scheme=parsed.scheme, http_host=conn.host,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_path=cont_path, http_query=qs,
|
|
|
|
http_status=resp.status, http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp_headers = {}
|
|
|
|
for header, value in resp.getheaders():
|
|
|
|
resp_headers[header.lower()] = value
|
|
|
|
if resp.status == 204:
|
|
|
|
return resp_headers, []
|
2012-05-09 15:50:17 +01:00
|
|
|
return resp_headers, json_loads(body)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
def head_container(url, token, container, http_conn=None, headers=None):
|
|
|
|
"""
|
|
|
|
Get container stats.
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name to get stats for
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:returns: a dict containing the response's headers (all header names will
|
|
|
|
be lowercase)
|
|
|
|
:raises ClientException: HTTP HEAD request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
|
|
|
path = '%s/%s' % (parsed.path, quote(container))
|
2012-05-09 15:50:17 +01:00
|
|
|
method = 'HEAD'
|
2012-05-08 11:17:04 +01:00
|
|
|
req_headers = {'X-Auth-Token': token}
|
|
|
|
if headers:
|
|
|
|
req_headers.update(headers)
|
2012-05-09 15:50:17 +01:00
|
|
|
conn.request(method, path, '', req_headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': req_headers}, resp, body)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Container HEAD failed',
|
2012-06-15 12:18:08 -07:00
|
|
|
http_scheme=parsed.scheme, http_host=conn.host,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_path=path, http_status=resp.status,
|
|
|
|
http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp_headers = {}
|
|
|
|
for header, value in resp.getheaders():
|
|
|
|
resp_headers[header.lower()] = value
|
|
|
|
return resp_headers
|
|
|
|
|
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def put_container(url, token, container, headers=None, http_conn=None,
|
|
|
|
response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Create a container
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name to create
|
|
|
|
:param headers: additional headers to include in the request
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
2012-05-08 11:17:04 +01:00
|
|
|
:raises ClientException: HTTP PUT request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
|
|
|
path = '%s/%s' % (parsed.path, quote(container))
|
2012-05-09 15:50:17 +01:00
|
|
|
method = 'PUT'
|
2012-05-08 11:17:04 +01:00
|
|
|
if not headers:
|
|
|
|
headers = {}
|
|
|
|
headers['X-Auth-Token'] = token
|
2014-05-13 23:57:24 +02:00
|
|
|
if 'content-length' not in (k.lower() for k in headers):
|
2013-07-07 16:45:56 +08:00
|
|
|
headers['Content-Length'] = '0'
|
2012-05-09 15:50:17 +01:00
|
|
|
conn.request(method, path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
store_response(resp, response_dict)
|
|
|
|
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Container PUT failed',
|
2012-06-15 12:18:08 -07:00
|
|
|
http_scheme=parsed.scheme, http_host=conn.host,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_path=path, http_status=resp.status,
|
|
|
|
http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def post_container(url, token, container, headers, http_conn=None,
|
|
|
|
response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Update a container's metadata.
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name to update
|
|
|
|
:param headers: additional headers to include in the request
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
2012-05-08 11:17:04 +01:00
|
|
|
:raises ClientException: HTTP POST request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
|
|
|
path = '%s/%s' % (parsed.path, quote(container))
|
2012-05-09 15:50:17 +01:00
|
|
|
method = 'POST'
|
2012-05-08 11:17:04 +01:00
|
|
|
headers['X-Auth-Token'] = token
|
2014-05-13 23:57:24 +02:00
|
|
|
if 'content-length' not in (k.lower() for k in headers):
|
2013-07-07 16:45:56 +08:00
|
|
|
headers['Content-Length'] = '0'
|
2012-05-09 15:50:17 +01:00
|
|
|
conn.request(method, path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
store_response(resp, response_dict)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Container POST failed',
|
2012-06-15 12:18:08 -07:00
|
|
|
http_scheme=parsed.scheme, http_host=conn.host,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_path=path, http_status=resp.status,
|
|
|
|
http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def delete_container(url, token, container, http_conn=None,
|
|
|
|
response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Delete a container
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name to delete
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
2012-05-08 11:17:04 +01:00
|
|
|
:raises ClientException: HTTP DELETE request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
|
|
|
path = '%s/%s' % (parsed.path, quote(container))
|
2012-05-09 15:50:17 +01:00
|
|
|
headers = {'X-Auth-Token': token}
|
|
|
|
method = 'DELETE'
|
|
|
|
conn.request(method, path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
store_response(resp, response_dict)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Container DELETE failed',
|
2012-06-15 12:18:08 -07:00
|
|
|
http_scheme=parsed.scheme, http_host=conn.host,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_path=path, http_status=resp.status,
|
|
|
|
http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
def get_object(url, token, container, name, http_conn=None,
|
2013-06-12 12:02:02 +00:00
|
|
|
resp_chunk_size=None, query_string=None,
|
2013-07-27 17:06:28 +10:00
|
|
|
response_dict=None, headers=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Get an object
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name that the object is in
|
|
|
|
:param name: object name to get
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:param resp_chunk_size: if defined, chunk size of data to read. NOTE: If
|
|
|
|
you specify a resp_chunk_size you must fully read
|
|
|
|
the object's contents before making another
|
|
|
|
request.
|
2013-03-05 15:12:04 -08:00
|
|
|
:param query_string: if set will be appended with '?' to generated path
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
2013-07-27 17:06:28 +10:00
|
|
|
:param headers: an optional dictionary with additional headers to include
|
|
|
|
in the request
|
2012-05-08 11:17:04 +01:00
|
|
|
:returns: a tuple of (response headers, the object's contents) The response
|
|
|
|
headers will be a dict and all header names will be lowercase.
|
|
|
|
:raises ClientException: HTTP GET request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
|
|
|
path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
|
2013-03-05 15:12:04 -08:00
|
|
|
if query_string:
|
|
|
|
path += '?' + query_string
|
2012-05-09 15:50:17 +01:00
|
|
|
method = 'GET'
|
2013-07-27 17:06:28 +10:00
|
|
|
headers = headers.copy() if headers else {}
|
|
|
|
headers['X-Auth-Token'] = token
|
2012-05-09 15:50:17 +01:00
|
|
|
conn.request(method, path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
parsed_response = {}
|
|
|
|
store_response(resp, parsed_response)
|
|
|
|
if response_dict is not None:
|
|
|
|
response_dict.update(parsed_response)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2012-05-08 11:17:04 +01:00
|
|
|
raise ClientException('Object GET failed', http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=path,
|
|
|
|
http_status=resp.status,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_reason=resp.reason,
|
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp_chunk_size:
|
|
|
|
|
|
|
|
def _object_body():
|
|
|
|
buf = resp.read(resp_chunk_size)
|
|
|
|
while buf:
|
|
|
|
yield buf
|
|
|
|
buf = resp.read(resp_chunk_size)
|
|
|
|
object_body = _object_body()
|
|
|
|
else:
|
|
|
|
object_body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
|
|
|
|
{'headers': headers}, resp, None)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
return parsed_response['headers'], object_body
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
def head_object(url, token, container, name, http_conn=None):
|
|
|
|
"""
|
|
|
|
Get object info
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name that the object is in
|
|
|
|
:param name: object name to get info for
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:returns: a dict containing the response's headers (all header names will
|
|
|
|
be lowercase)
|
|
|
|
:raises ClientException: HTTP HEAD request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
|
|
|
path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
|
2012-05-09 15:50:17 +01:00
|
|
|
method = 'HEAD'
|
|
|
|
headers = {'X-Auth-Token': token}
|
|
|
|
conn.request(method, path, '', headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Object HEAD failed', http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=path,
|
|
|
|
http_status=resp.status, http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
resp_headers = {}
|
|
|
|
for header, value in resp.getheaders():
|
|
|
|
resp_headers[header.lower()] = value
|
|
|
|
return resp_headers
|
|
|
|
|
|
|
|
|
|
|
|
def put_object(url, token=None, container=None, name=None, contents=None,
|
2013-03-05 16:56:02 +00:00
|
|
|
content_length=None, etag=None, chunk_size=None,
|
2013-03-05 15:12:04 -08:00
|
|
|
content_type=None, headers=None, http_conn=None, proxy=None,
|
2013-06-12 12:02:02 +00:00
|
|
|
query_string=None, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Put an object
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token; if None, no token will be sent
|
|
|
|
:param container: container name that the object is in; if None, the
|
|
|
|
container name is expected to be part of the url
|
|
|
|
:param name: object name to put; if None, the object name is expected to be
|
|
|
|
part of the url
|
2014-10-29 10:11:29 +00:00
|
|
|
:param contents: a string, a file like object or an iterable
|
|
|
|
to read object data from;
|
2012-05-08 11:17:04 +01:00
|
|
|
if None, a zero-byte put will be done
|
|
|
|
:param content_length: value to send as content-length header; also limits
|
|
|
|
the amount read from contents; if None, it will be
|
|
|
|
computed via the contents or chunked transfer
|
|
|
|
encoding will be used
|
|
|
|
:param etag: etag of contents; if None, no etag will be sent
|
2013-03-05 16:56:02 +00:00
|
|
|
:param chunk_size: chunk size of data to write; it defaults to 65536;
|
2014-04-25 11:26:28 -07:00
|
|
|
used only if the contents object has a 'read'
|
2014-05-02 18:39:14 +02:00
|
|
|
method, e.g. file-like objects, ignored otherwise
|
2014-05-20 11:41:03 +00:00
|
|
|
:param content_type: value to send as content-type header; if None, an
|
|
|
|
empty string value will be sent
|
2012-05-08 11:17:04 +01:00
|
|
|
:param headers: additional headers to include in the request, if any
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
|
|
|
format 'http://127.0.0.1:8888' to set one
|
2013-03-05 15:12:04 -08:00
|
|
|
:param query_string: if set will be appended with '?' to generated path
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
|
|
|
:returns: etag
|
2012-05-08 11:17:04 +01:00
|
|
|
:raises ClientException: HTTP PUT request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url, proxy=proxy)
|
|
|
|
path = parsed.path
|
|
|
|
if container:
|
|
|
|
path = '%s/%s' % (path.rstrip('/'), quote(container))
|
|
|
|
if name:
|
|
|
|
path = '%s/%s' % (path.rstrip('/'), quote(name))
|
2013-03-05 15:12:04 -08:00
|
|
|
if query_string:
|
|
|
|
path += '?' + query_string
|
2012-05-08 11:17:04 +01:00
|
|
|
if headers:
|
|
|
|
headers = dict(headers)
|
|
|
|
else:
|
|
|
|
headers = {}
|
|
|
|
if token:
|
|
|
|
headers['X-Auth-Token'] = token
|
|
|
|
if etag:
|
|
|
|
headers['ETag'] = etag.strip('"')
|
|
|
|
if content_length is not None:
|
|
|
|
headers['Content-Length'] = str(content_length)
|
|
|
|
else:
|
2014-03-24 18:21:34 +01:00
|
|
|
for n, v in headers.items():
|
2012-05-08 11:17:04 +01:00
|
|
|
if n.lower() == 'content-length':
|
|
|
|
content_length = int(v)
|
|
|
|
if content_type is not None:
|
|
|
|
headers['Content-Type'] = content_type
|
2014-05-20 11:41:03 +00:00
|
|
|
else: # python-requests sets application/x-www-form-urlencoded otherwise
|
|
|
|
headers['Content-Type'] = ''
|
2012-05-08 11:17:04 +01:00
|
|
|
if not contents:
|
|
|
|
headers['Content-Length'] = '0'
|
2014-10-29 10:11:29 +00:00
|
|
|
|
|
|
|
if isinstance(contents, (ReadableToIterable, LengthWrapper)):
|
|
|
|
conn.putrequest(path, headers=headers, data=contents)
|
|
|
|
elif hasattr(contents, 'read'):
|
2013-03-05 16:56:02 +00:00
|
|
|
if chunk_size is None:
|
|
|
|
chunk_size = 65536
|
2014-10-29 10:11:29 +00:00
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if content_length is None:
|
2014-10-29 10:11:29 +00:00
|
|
|
data = ReadableToIterable(contents, chunk_size, md5=False)
|
2012-05-08 11:17:04 +01:00
|
|
|
else:
|
2014-10-29 10:11:29 +00:00
|
|
|
data = LengthWrapper(contents, content_length, md5=False)
|
|
|
|
|
|
|
|
conn.putrequest(path, headers=headers, data=data)
|
2012-05-08 11:17:04 +01:00
|
|
|
else:
|
2013-03-05 16:56:02 +00:00
|
|
|
if chunk_size is not None:
|
2014-10-29 10:11:29 +00:00
|
|
|
warn_msg = ('%s object has no "read" method, ignoring chunk_size'
|
|
|
|
% type(contents).__name__)
|
2013-03-05 16:56:02 +00:00
|
|
|
warnings.warn(warn_msg, stacklevel=2)
|
2012-05-08 11:17:04 +01:00
|
|
|
conn.request('PUT', path, contents, headers)
|
2014-10-29 10:11:29 +00:00
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-05-09 15:50:17 +01:00
|
|
|
headers = {'X-Auth-Token': token}
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), 'PUT',),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
store_response(resp, response_dict)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Object PUT failed', http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=path,
|
|
|
|
http_status=resp.status, http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
2014-10-29 10:11:29 +00:00
|
|
|
etag = resp.getheader('etag', '').strip('"')
|
|
|
|
return etag
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def post_object(url, token, container, name, headers, http_conn=None,
|
|
|
|
response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Update object metadata
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token
|
|
|
|
:param container: container name that the object is in
|
|
|
|
:param name: name of the object to update
|
|
|
|
:param headers: additional headers to include in the request
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
2012-05-08 11:17:04 +01:00
|
|
|
:raises ClientException: HTTP POST request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url)
|
|
|
|
path = '%s/%s/%s' % (parsed.path, quote(container), quote(name))
|
|
|
|
headers['X-Auth-Token'] = token
|
|
|
|
conn.request('POST', path, '', headers)
|
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), 'POST',),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
store_response(resp, response_dict)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Object POST failed', http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=path,
|
|
|
|
http_status=resp.status, http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
def delete_object(url, token=None, container=None, name=None, http_conn=None,
|
2013-06-12 12:02:02 +00:00
|
|
|
headers=None, proxy=None, query_string=None,
|
|
|
|
response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
Delete object
|
|
|
|
|
|
|
|
:param url: storage URL
|
|
|
|
:param token: auth token; if None, no token will be sent
|
|
|
|
:param container: container name that the object is in; if None, the
|
|
|
|
container name is expected to be part of the url
|
|
|
|
:param name: object name to delete; if None, the object name is expected to
|
|
|
|
be part of the url
|
|
|
|
:param http_conn: HTTP connection object (If None, it will create the
|
|
|
|
conn object)
|
|
|
|
:param headers: additional headers to include in the request
|
|
|
|
:param proxy: proxy to connect through, if any; None by default; str of the
|
|
|
|
format 'http://127.0.0.1:8888' to set one
|
2013-03-05 15:12:04 -08:00
|
|
|
:param query_string: if set will be appended with '?' to generated path
|
2013-06-12 12:02:02 +00:00
|
|
|
:param response_dict: an optional dictionary into which to place
|
|
|
|
the response - status, reason and headers
|
2012-05-08 11:17:04 +01:00
|
|
|
:raises ClientException: HTTP DELETE request failed
|
|
|
|
"""
|
|
|
|
if http_conn:
|
|
|
|
parsed, conn = http_conn
|
|
|
|
else:
|
|
|
|
parsed, conn = http_connection(url, proxy=proxy)
|
|
|
|
path = parsed.path
|
|
|
|
if container:
|
|
|
|
path = '%s/%s' % (path.rstrip('/'), quote(container))
|
|
|
|
if name:
|
|
|
|
path = '%s/%s' % (path.rstrip('/'), quote(name))
|
2013-03-05 15:12:04 -08:00
|
|
|
if query_string:
|
|
|
|
path += '?' + query_string
|
2012-05-08 11:17:04 +01:00
|
|
|
if headers:
|
|
|
|
headers = dict(headers)
|
|
|
|
else:
|
|
|
|
headers = {}
|
|
|
|
if token:
|
|
|
|
headers['X-Auth-Token'] = token
|
|
|
|
conn.request('DELETE', path, '', headers)
|
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
2012-11-16 15:23:25 +10:00
|
|
|
http_log(('%s%s' % (url.replace(parsed.path, ''), path), 'DELETE',),
|
2012-05-09 15:50:17 +01:00
|
|
|
{'headers': headers}, resp, body)
|
2013-06-12 12:02:02 +00:00
|
|
|
|
|
|
|
store_response(resp, response_dict)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Object DELETE failed',
|
2012-06-15 12:18:08 -07:00
|
|
|
http_scheme=parsed.scheme, http_host=conn.host,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_path=path, http_status=resp.status,
|
|
|
|
http_reason=resp.reason,
|
2012-06-15 12:18:08 -07:00
|
|
|
http_response_content=body)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
|
2014-01-13 22:39:28 +01:00
|
|
|
def get_capabilities(http_conn):
|
|
|
|
"""
|
|
|
|
Get cluster capability infos.
|
|
|
|
|
|
|
|
:param http_conn: HTTP connection
|
|
|
|
:returns: a dict containing the cluster capabilities
|
|
|
|
:raises ClientException: HTTP Capabilities GET failed
|
|
|
|
"""
|
|
|
|
parsed, conn = http_conn
|
|
|
|
conn.request('GET', parsed.path, '')
|
|
|
|
resp = conn.getresponse()
|
|
|
|
body = resp.read()
|
|
|
|
http_log((parsed.geturl(), 'GET',), {'headers': {}}, resp, body)
|
|
|
|
if resp.status < 200 or resp.status >= 300:
|
|
|
|
raise ClientException('Capabilities GET failed',
|
|
|
|
http_scheme=parsed.scheme,
|
2014-03-06 22:02:10 +01:00
|
|
|
http_host=conn.host, http_path=parsed.path,
|
|
|
|
http_status=resp.status, http_reason=resp.reason,
|
2014-01-13 22:39:28 +01:00
|
|
|
http_response_content=body)
|
|
|
|
return json_loads(body)
|
|
|
|
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
class Connection(object):
|
|
|
|
"""Convenience class to make requests that will also retry the request"""
|
|
|
|
|
2012-10-23 09:18:29 +01:00
|
|
|
def __init__(self, authurl=None, user=None, key=None, retries=5,
|
|
|
|
preauthurl=None, preauthtoken=None, snet=False,
|
2013-07-10 10:52:02 -07:00
|
|
|
starting_backoff=1, max_backoff=64, tenant_name=None,
|
|
|
|
os_options=None, auth_version="1", cacert=None,
|
2013-12-23 13:49:46 -08:00
|
|
|
insecure=False, ssl_compression=True,
|
|
|
|
retry_on_ratelimit=False):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
:param authurl: authentication URL
|
|
|
|
:param user: user name to authenticate as
|
|
|
|
:param key: key/password to authenticate with
|
|
|
|
:param retries: Number of times to retry the request before failing
|
|
|
|
:param preauthurl: storage URL (if you have already authenticated)
|
|
|
|
:param preauthtoken: authentication token (if you have already
|
2012-10-23 09:18:29 +01:00
|
|
|
authenticated) note authurl/user/key/tenant_name
|
|
|
|
are not required when specifying preauthtoken
|
2012-05-08 11:17:04 +01:00
|
|
|
:param snet: use SERVICENET internal network default is False
|
2013-07-12 17:26:45 -07:00
|
|
|
:param starting_backoff: initial delay between retries (seconds)
|
|
|
|
:param max_backoff: maximum delay between retries (seconds)
|
2012-05-08 11:17:04 +01:00
|
|
|
:param auth_version: OpenStack auth version, default is 1.0
|
|
|
|
:param tenant_name: The tenant/account name, required when connecting
|
2014-05-02 18:39:14 +02:00
|
|
|
to an auth 2.0 system.
|
2012-07-04 21:46:02 +02:00
|
|
|
:param os_options: The OpenStack options which can have tenant_id,
|
2012-08-28 10:25:42 -04:00
|
|
|
auth_token, service_type, endpoint_type,
|
2012-09-05 15:55:53 +01:00
|
|
|
tenant_name, object_storage_url, region_name
|
2014-02-13 23:33:01 -08:00
|
|
|
:param insecure: Allow to access servers without checking SSL certs.
|
|
|
|
The server's certificate will not be verified.
|
2013-01-18 14:17:21 +00:00
|
|
|
:param ssl_compression: Whether to enable compression at the SSL layer.
|
|
|
|
If set to 'False' and the pyOpenSSL library is
|
|
|
|
present an attempt to disable SSL compression
|
|
|
|
will be made. This may provide a performance
|
|
|
|
increase for https upload/download operations.
|
2013-12-23 13:49:46 -08:00
|
|
|
:param retry_on_ratelimit: by default, a ratelimited connection will
|
|
|
|
raise an exception to the caller. Setting
|
|
|
|
this parameter to True will cause a retry
|
|
|
|
after a backoff.
|
2012-05-08 11:17:04 +01:00
|
|
|
"""
|
|
|
|
self.authurl = authurl
|
|
|
|
self.user = user
|
|
|
|
self.key = key
|
|
|
|
self.retries = retries
|
|
|
|
self.http_conn = None
|
|
|
|
self.attempts = 0
|
|
|
|
self.snet = snet
|
|
|
|
self.starting_backoff = starting_backoff
|
2013-07-10 10:52:02 -07:00
|
|
|
self.max_backoff = max_backoff
|
2012-05-08 11:17:04 +01:00
|
|
|
self.auth_version = auth_version
|
2012-09-13 05:35:26 +00:00
|
|
|
self.os_options = os_options or {}
|
2012-07-04 21:46:02 +02:00
|
|
|
if tenant_name:
|
2012-09-13 05:35:26 +00:00
|
|
|
self.os_options['tenant_name'] = tenant_name
|
2014-10-24 01:02:53 -07:00
|
|
|
if preauthurl:
|
|
|
|
self.os_options['object_storage_url'] = preauthurl
|
|
|
|
self.url = preauthurl or self.os_options.get('object_storage_url')
|
|
|
|
self.token = preauthtoken or self.os_options.get('auth_token')
|
2012-12-19 09:52:54 -06:00
|
|
|
self.cacert = cacert
|
2012-12-05 13:18:27 +09:00
|
|
|
self.insecure = insecure
|
2013-01-18 14:17:21 +00:00
|
|
|
self.ssl_compression = ssl_compression
|
2013-09-04 19:32:09 +01:00
|
|
|
self.auth_end_time = 0
|
2013-12-23 13:49:46 -08:00
|
|
|
self.retry_on_ratelimit = retry_on_ratelimit
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-10-17 11:12:06 +08:00
|
|
|
def close(self):
|
2014-11-06 14:10:13 +00:00
|
|
|
if (self.http_conn and isinstance(self.http_conn, tuple)
|
|
|
|
and len(self.http_conn) > 1):
|
2013-10-17 11:12:06 +08:00
|
|
|
conn = self.http_conn[1]
|
|
|
|
if hasattr(conn, 'close') and callable(conn.close):
|
2014-10-24 01:02:53 -07:00
|
|
|
# XXX: Our HTTPConnection object has no close, should be
|
|
|
|
# trying to close the requests.Session here?
|
2013-10-17 11:12:06 +08:00
|
|
|
conn.close()
|
|
|
|
self.http_conn = None
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
def get_auth(self):
|
2013-06-28 21:26:54 -07:00
|
|
|
return get_auth(self.authurl, self.user, self.key,
|
2012-07-04 21:46:02 +02:00
|
|
|
snet=self.snet,
|
|
|
|
auth_version=self.auth_version,
|
2012-12-05 13:18:27 +09:00
|
|
|
os_options=self.os_options,
|
2012-12-19 09:52:54 -06:00
|
|
|
cacert=self.cacert,
|
2012-12-05 13:18:27 +09:00
|
|
|
insecure=self.insecure)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2014-10-17 13:38:01 -06:00
|
|
|
def http_connection(self, url=None):
|
|
|
|
return http_connection(url if url else self.url,
|
2014-01-24 17:40:16 +01:00
|
|
|
cacert=self.cacert,
|
|
|
|
insecure=self.insecure,
|
2013-01-18 14:17:21 +00:00
|
|
|
ssl_compression=self.ssl_compression)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def _add_response_dict(self, target_dict, kwargs):
|
2014-10-15 18:56:58 +01:00
|
|
|
if target_dict is not None and 'response_dict' in kwargs:
|
2013-06-12 12:02:02 +00:00
|
|
|
response_dict = kwargs['response_dict']
|
|
|
|
if 'response_dicts' in target_dict:
|
|
|
|
target_dict['response_dicts'].append(response_dict)
|
|
|
|
else:
|
|
|
|
target_dict['response_dicts'] = [response_dict]
|
|
|
|
target_dict.update(response_dict)
|
|
|
|
|
2012-05-08 11:17:04 +01:00
|
|
|
def _retry(self, reset_func, func, *args, **kwargs):
|
|
|
|
self.attempts = 0
|
2013-06-12 17:43:01 -05:00
|
|
|
retried_auth = False
|
2012-05-08 11:17:04 +01:00
|
|
|
backoff = self.starting_backoff
|
2013-06-12 12:02:02 +00:00
|
|
|
caller_response_dict = kwargs.pop('response_dict', None)
|
2012-05-08 11:17:04 +01:00
|
|
|
while self.attempts <= self.retries:
|
|
|
|
self.attempts += 1
|
|
|
|
try:
|
|
|
|
if not self.url or not self.token:
|
|
|
|
self.url, self.token = self.get_auth()
|
|
|
|
self.http_conn = None
|
2013-09-04 19:32:09 +01:00
|
|
|
self.auth_end_time = time()
|
2012-05-08 11:17:04 +01:00
|
|
|
if not self.http_conn:
|
|
|
|
self.http_conn = self.http_connection()
|
|
|
|
kwargs['http_conn'] = self.http_conn
|
2013-06-12 12:02:02 +00:00
|
|
|
if caller_response_dict is not None:
|
|
|
|
kwargs['response_dict'] = {}
|
2012-05-08 11:17:04 +01:00
|
|
|
rv = func(self.url, self.token, *args, **kwargs)
|
2013-06-12 12:02:02 +00:00
|
|
|
self._add_response_dict(caller_response_dict, kwargs)
|
2012-05-08 11:17:04 +01:00
|
|
|
return rv
|
2014-01-24 17:40:16 +01:00
|
|
|
except SSLError:
|
|
|
|
raise
|
|
|
|
except (socket.error, RequestException) as e:
|
2013-06-12 12:02:02 +00:00
|
|
|
self._add_response_dict(caller_response_dict, kwargs)
|
2012-05-08 11:17:04 +01:00
|
|
|
if self.attempts > self.retries:
|
2013-07-16 21:22:32 +00:00
|
|
|
logger.exception(e)
|
2012-05-08 11:17:04 +01:00
|
|
|
raise
|
|
|
|
self.http_conn = None
|
2013-04-25 23:19:52 +02:00
|
|
|
except ClientException as err:
|
2013-06-12 12:02:02 +00:00
|
|
|
self._add_response_dict(caller_response_dict, kwargs)
|
2012-05-08 11:17:04 +01:00
|
|
|
if self.attempts > self.retries:
|
2013-07-16 21:22:32 +00:00
|
|
|
logger.exception(err)
|
2012-05-08 11:17:04 +01:00
|
|
|
raise
|
|
|
|
if err.http_status == 401:
|
|
|
|
self.url = self.token = None
|
2013-06-12 17:43:01 -05:00
|
|
|
if retried_auth or not all((self.authurl,
|
|
|
|
self.user,
|
|
|
|
self.key)):
|
2013-07-16 21:22:32 +00:00
|
|
|
logger.exception(err)
|
2012-05-08 11:17:04 +01:00
|
|
|
raise
|
2013-06-12 17:43:01 -05:00
|
|
|
retried_auth = True
|
2012-05-08 11:17:04 +01:00
|
|
|
elif err.http_status == 408:
|
|
|
|
self.http_conn = None
|
|
|
|
elif 500 <= err.http_status <= 599:
|
|
|
|
pass
|
2013-12-23 13:49:46 -08:00
|
|
|
elif self.retry_on_ratelimit and err.http_status == 498:
|
|
|
|
pass
|
2012-05-08 11:17:04 +01:00
|
|
|
else:
|
2013-07-16 21:22:32 +00:00
|
|
|
logger.exception(err)
|
2012-05-08 11:17:04 +01:00
|
|
|
raise
|
|
|
|
sleep(backoff)
|
2013-07-10 10:52:02 -07:00
|
|
|
backoff = min(backoff * 2, self.max_backoff)
|
2012-05-08 11:17:04 +01:00
|
|
|
if reset_func:
|
|
|
|
reset_func(func, *args, **kwargs)
|
|
|
|
|
|
|
|
def head_account(self):
|
|
|
|
"""Wrapper for :func:`head_account`"""
|
|
|
|
return self._retry(None, head_account)
|
|
|
|
|
|
|
|
def get_account(self, marker=None, limit=None, prefix=None,
|
2013-05-02 00:15:11 +09:00
|
|
|
end_marker=None, full_listing=False):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`get_account`"""
|
|
|
|
# TODO(unknown): With full_listing=True this will restart the entire
|
|
|
|
# listing with each retry. Need to make a better version that just
|
|
|
|
# retries where it left off.
|
|
|
|
return self._retry(None, get_account, marker=marker, limit=limit,
|
2013-05-02 00:15:11 +09:00
|
|
|
prefix=prefix, end_marker=end_marker,
|
|
|
|
full_listing=full_listing)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def post_account(self, headers, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`post_account`"""
|
2013-06-12 12:02:02 +00:00
|
|
|
return self._retry(None, post_account, headers,
|
|
|
|
response_dict=response_dict)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
def head_container(self, container):
|
|
|
|
"""Wrapper for :func:`head_container`"""
|
|
|
|
return self._retry(None, head_container, container)
|
|
|
|
|
|
|
|
def get_container(self, container, marker=None, limit=None, prefix=None,
|
2013-05-02 00:15:11 +09:00
|
|
|
delimiter=None, end_marker=None, path=None,
|
|
|
|
full_listing=False):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`get_container`"""
|
|
|
|
# TODO(unknown): With full_listing=True this will restart the entire
|
|
|
|
# listing with each retry. Need to make a better version that just
|
|
|
|
# retries where it left off.
|
|
|
|
return self._retry(None, get_container, container, marker=marker,
|
|
|
|
limit=limit, prefix=prefix, delimiter=delimiter,
|
2013-05-02 00:15:11 +09:00
|
|
|
end_marker=end_marker, path=path,
|
2012-05-08 11:17:04 +01:00
|
|
|
full_listing=full_listing)
|
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def put_container(self, container, headers=None, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`put_container`"""
|
2013-06-12 12:02:02 +00:00
|
|
|
return self._retry(None, put_container, container, headers=headers,
|
|
|
|
response_dict=response_dict)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def post_container(self, container, headers, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`post_container`"""
|
2013-06-12 12:02:02 +00:00
|
|
|
return self._retry(None, post_container, container, headers,
|
|
|
|
response_dict=response_dict)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def delete_container(self, container, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`delete_container`"""
|
2013-06-12 12:02:02 +00:00
|
|
|
return self._retry(None, delete_container, container,
|
|
|
|
response_dict=response_dict)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
def head_object(self, container, obj):
|
|
|
|
"""Wrapper for :func:`head_object`"""
|
|
|
|
return self._retry(None, head_object, container, obj)
|
|
|
|
|
2013-03-05 15:12:04 -08:00
|
|
|
def get_object(self, container, obj, resp_chunk_size=None,
|
2013-07-27 17:06:28 +10:00
|
|
|
query_string=None, response_dict=None, headers=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`get_object`"""
|
|
|
|
return self._retry(None, get_object, container, obj,
|
2013-03-05 15:12:04 -08:00
|
|
|
resp_chunk_size=resp_chunk_size,
|
2013-06-12 12:02:02 +00:00
|
|
|
query_string=query_string,
|
2013-07-27 17:06:28 +10:00
|
|
|
response_dict=response_dict, headers=headers)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
def put_object(self, container, obj, contents, content_length=None,
|
2013-03-05 16:56:02 +00:00
|
|
|
etag=None, chunk_size=None, content_type=None,
|
2013-06-12 12:02:02 +00:00
|
|
|
headers=None, query_string=None, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`put_object`"""
|
|
|
|
|
|
|
|
def _default_reset(*args, **kwargs):
|
|
|
|
raise ClientException('put_object(%r, %r, ...) failure and no '
|
2012-06-15 12:18:08 -07:00
|
|
|
'ability to reset contents for reupload.'
|
|
|
|
% (container, obj))
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-09-04 15:20:08 +08:00
|
|
|
if isinstance(contents, str) or not contents:
|
|
|
|
# if its a str or None then you can retry as much as you want
|
2013-03-05 15:12:04 -08:00
|
|
|
reset_func = None
|
|
|
|
else:
|
|
|
|
reset_func = _default_reset
|
2013-09-04 15:20:08 +08:00
|
|
|
if self.retries > 0:
|
|
|
|
tell = getattr(contents, 'tell', None)
|
|
|
|
seek = getattr(contents, 'seek', None)
|
|
|
|
if tell and seek:
|
|
|
|
orig_pos = tell()
|
|
|
|
reset_func = lambda *a, **k: seek(orig_pos)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
|
|
|
return self._retry(reset_func, put_object, container, obj, contents,
|
2012-06-15 12:18:08 -07:00
|
|
|
content_length=content_length, etag=etag,
|
|
|
|
chunk_size=chunk_size, content_type=content_type,
|
2013-06-12 12:02:02 +00:00
|
|
|
headers=headers, query_string=query_string,
|
|
|
|
response_dict=response_dict)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def post_object(self, container, obj, headers, response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`post_object`"""
|
2013-06-12 12:02:02 +00:00
|
|
|
return self._retry(None, post_object, container, obj, headers,
|
|
|
|
response_dict=response_dict)
|
2012-05-08 11:17:04 +01:00
|
|
|
|
2013-06-12 12:02:02 +00:00
|
|
|
def delete_object(self, container, obj, query_string=None,
|
|
|
|
response_dict=None):
|
2012-05-08 11:17:04 +01:00
|
|
|
"""Wrapper for :func:`delete_object`"""
|
2013-03-05 15:12:04 -08:00
|
|
|
return self._retry(None, delete_object, container, obj,
|
2013-06-12 12:02:02 +00:00
|
|
|
query_string=query_string,
|
|
|
|
response_dict=response_dict)
|
2014-01-13 22:39:28 +01:00
|
|
|
|
|
|
|
def get_capabilities(self, url=None):
|
2014-10-24 01:02:53 -07:00
|
|
|
url = url or self.url
|
2014-01-13 22:39:28 +01:00
|
|
|
if not url:
|
|
|
|
url, _ = self.get_auth()
|
|
|
|
scheme = urlparse(url).scheme
|
|
|
|
netloc = urlparse(url).netloc
|
|
|
|
url = scheme + '://' + netloc + '/info'
|
2014-10-17 13:38:01 -06:00
|
|
|
http_conn = self.http_connection(url)
|
2014-01-13 22:39:28 +01:00
|
|
|
return get_capabilities(http_conn)
|