Moving away from novaclient and adding all the missing pieces into reddwarfclient.

- Added parameters for the authentication instead of the arguments.
- Cleaned out the HttpClient and Authentication pieces.
This commit is contained in:
Nirmal Ranganathan 2012-07-03 10:21:43 -05:00
parent 5241529fa2
commit 199ded7eb5
22 changed files with 857 additions and 269 deletions

@ -15,7 +15,6 @@
from reddwarfclient.accounts import Accounts
from reddwarfclient.config import Configs
from reddwarfclient.databases import Databases
from reddwarfclient.flavors import Flavors
from reddwarfclient.instances import Instances

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
class Account(base.Resource):

178
reddwarfclient/auth.py Normal file

@ -0,0 +1,178 @@
# Copyright 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.
from reddwarfclient import exceptions
class Authenticator(object):
"""
Helper class to perform Keystone or other miscellaneous authentication.
"""
def __init__(self, client, type, url, username, password, tenant,
region=None, service_type=None, service_name=None,
service_url=None):
self.client = client
self.type = type
self.url = url
self.username = username
self.password = password
self.tenant = tenant
self.region = region
self.service_type = service_type
self.service_name = service_name
self.service_url = service_url
def _authenticate(self, url, body):
"""Authenticate and extract the service catalog."""
# Make sure we follow redirects when trying to reach Keystone
tmp_follow_all_redirects = self.client.follow_all_redirects
self.client.follow_all_redirects = True
try:
resp, body = self.client._time_request(url, "POST", body=body)
finally:
self.client.follow_all_redirects = tmp_follow_all_redirects
if resp.status == 200: # content must always present
try:
return ServiceCatalog(body, region=self.region,
service_type=self.service_type,
service_name=self.service_name,
service_url=self.service_url)
except exceptions.AmbiguousEndpoints:
print "Found more than one valid endpoint. Use a more "\
"restrictive filter"
raise
except KeyError:
raise exceptions.AuthorizationFailure()
except exceptions.EndpointNotFound:
print "Could not find any suitable endpoint. Correct region?"
raise
elif resp.status == 305:
return resp['location']
else:
raise exceptions.from_response(resp, body)
def authenticate(self):
if self.type == "keystone":
return self._v2_auth(self.url)
elif self.type == "rax":
return self._rax_auth(self.url)
def _v2_auth(self, url):
"""Authenticate against a v2.0 auth service."""
body = {"auth": {
"passwordCredentials": {
"username": self.username,
"password": self.password}
}
}
if self.tenant:
body['auth']['tenantName'] = self.tenant
return self._authenticate(url, body)
def _rax_auth(self, url):
"""Authenticate against the Rackspace auth service."""
body = {'auth': {
'RAX-KSKEY:apiKeyCredentials': {
'username': self.username,
'apiKey': self.password,
'tenantName': self.tenant}
}
}
return self._authenticate(self.url, body)
class ServiceCatalog(object):
"""Helper methods for dealing with a Keystone Service Catalog."""
def __init__(self, resource_dict, region=None, service_type=None,
service_name=None, service_url=None):
self.catalog = resource_dict
self.region = region
self.service_type = service_type
self.service_name = service_name
self.service_url = service_url
self.management_url = None
self.public_url = None
self._load()
def _load(self):
if not self.service_url:
self.public_url = self._url_for(attr='region',
filter_value=self.region,
endpoint_type="publicURL")
self.management_url = self._url_for(attr='region',
filter_value=self.region,
endpoint_type="adminURL")
else:
self.public_url = self.service_url
self.management_url = self.service_url
def get_token(self):
return self.catalog['access']['token']['id']
def get_management_url(self):
return self.management_url
def get_public_url(self):
return self.public_url
def _url_for(self, attr=None, filter_value=None,
endpoint_type='publicURL'):
"""
Fetch the public URL from the Reddwarf service for a particular
endpoint attribute. If none given, return the first.
"""
matching_endpoints = []
if 'endpoints' in self.catalog:
# We have a bastardized service catalog. Treat it special. :/
for endpoint in self.catalog['endpoints']:
if not filter_value or endpoint[attr] == filter_value:
matching_endpoints.append(endpoint)
if not matching_endpoints:
raise exceptions.EndpointNotFound()
# We don't always get a service catalog back ...
if not 'serviceCatalog' in self.catalog['access']:
raise exceptions.EndpointNotFound()
# Full catalog ...
catalog = self.catalog['access']['serviceCatalog']
for service in catalog:
if service.get("type") != self.service_type:
continue
if (self.service_name and self.service_type == 'reddwarf' and
service.get('name') != self.service_name):
continue
endpoints = service['endpoints']
for endpoint in endpoints:
if not filter_value or endpoint.get(attr) == filter_value:
endpoint["serviceName"] = service.get("name")
matching_endpoints.append(endpoint)
if not matching_endpoints:
raise exceptions.EndpointNotFound()
elif len(matching_endpoints) > 1:
raise exceptions.AmbiguousEndpoints(endpoints=matching_endpoints)
else:
return matching_endpoints[0].get(endpoint_type, None)

@ -1,14 +1,293 @@
def isid(obj):
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# 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.
"""
Base utilities to build API operation managers and objects on top of.
"""
import contextlib
import hashlib
import os
from reddwarfclient import exceptions
from reddwarfclient import utils
# Python 2.4 compat
try:
all
except NameError:
def all(iterable):
return True not in (not x for x in iterable)
def getid(obj):
"""
Returns true if the given object can be converted to an ID,
false otherwise.
Abstracts the common pattern of allowing both an object or an object's ID
as a parameter when dealing with relationships.
"""
if hasattr(obj, "id"):
return True
else:
try:
int(obj)
except ValueError:
return False
try:
return obj.id
except AttributeError:
return obj
class Manager(utils.HookableMixin):
"""
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key, obj_class=None, body=None):
resp = None
if body:
resp, body = self.api.client.post(url, body=body)
else:
return True
resp, body = self.api.client.get(url)
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
if isinstance(data, dict):
try:
data = data['values']
except KeyError:
pass
with self.completion_cache('human_id', obj_class, mode="w"):
with self.completion_cache('uuid', obj_class, mode="w"):
return [obj_class(self, res, loaded=True)
for res in data if res]
@contextlib.contextmanager
def completion_cache(self, cache_type, obj_class, mode):
"""
The completion cache store items that can be used for bash
autocompletion, like UUIDs or human-friendly IDs.
A resource listing will clear and repopulate the cache.
A resource create will append to the cache.
Delete is not handled because listings are assumed to be performed
often enough to keep the cache reasonably up-to-date.
"""
base_dir = utils.env('REDDWARFCLIENT_ID_CACHE_DIR',
default="~/.reddwarfclient")
# NOTE(sirp): Keep separate UUID caches for each username + endpoint
# pair
username = utils.env('OS_USERNAME', 'USERNAME')
url = utils.env('OS_URL', 'SERVICE_URL')
uniqifier = hashlib.md5(username + url).hexdigest()
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
try:
os.makedirs(cache_dir, 0755)
except OSError:
# NOTE(kiall): This is typicaly either permission denied while
# attempting to create the directory, or the directory
# already exists. Either way, don't fail.
pass
resource = obj_class.__name__.lower()
filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
path = os.path.join(cache_dir, filename)
cache_attr = "_%s_cache" % cache_type
try:
setattr(self, cache_attr, open(path, mode))
except IOError:
# NOTE(kiall): This is typicaly a permission denied while
# attempting to write the cache file.
pass
try:
yield
finally:
cache = getattr(self, cache_attr, None)
if cache:
cache.close()
delattr(self, cache_attr)
def write_to_completion_cache(self, cache_type, val):
cache = getattr(self, "_%s_cache" % cache_type, None)
if cache:
cache.write("%s\n" % val)
def _get(self, url, response_key=None):
resp, body = self.api.client.get(url)
if response_key:
return self.resource_class(self, body[response_key], loaded=True)
else:
return self.resource_class(self, body, loaded=True)
def _create(self, url, body, response_key, return_raw=False, **kwargs):
self.run_hooks('modify_body_for_create', body, **kwargs)
resp, body = self.api.client.post(url, body=body)
if return_raw:
return body[response_key]
with self.completion_cache('human_id', self.resource_class, mode="a"):
with self.completion_cache('uuid', self.resource_class, mode="a"):
return self.resource_class(self, body[response_key])
def _delete(self, url):
resp, body = self.api.client.delete(url)
def _update(self, url, body, **kwargs):
self.run_hooks('modify_body_for_update', body, **kwargs)
resp, body = self.api.client.put(url, body=body)
return body
class ManagerWithFind(Manager):
"""
Like a `Manager`, but with additional `find()`/`findall()` methods.
"""
def find(self, **kwargs):
"""
Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(404, msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch
else:
return matches[0]
def findall(self, **kwargs):
"""
Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
def list(self):
raise NotImplementedError
class Resource(object):
"""
A resource represents a particular instance of an object (server, flavor,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
HUMAN_ID = False
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
# NOTE(sirp): ensure `id` is already present because if it isn't we'll
# enter an infinite loop of __getattr__ -> get -> __init__ ->
# __getattr__ -> ...
if 'id' in self.__dict__ and len(str(self.id)) == 36:
self.manager.write_to_completion_cache('uuid', self.id)
human_id = self.human_id
if human_id:
self.manager.write_to_completion_cache('human_id', human_id)
@property
def human_id(self):
"""Subclasses may override this provide a pretty ID which can be used
for bash completion.
"""
if 'name' in self.__dict__ and self.HUMAN_ID:
return utils.slugify(self.name)
return None
def _add_details(self, info):
for (k, v) in info.iteritems():
try:
setattr(self, k, v)
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val

@ -32,16 +32,11 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
if os.path.exists(os.path.join(possible_topdir, 'reddwarfclient',
'__init__.py')):
sys.path.insert(0, possible_topdir)
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
sys.path.insert(0, possible_topdir)
from reddwarfclient import common
oparser = None
def _pretty_print(info):
print json.dumps(info, sort_keys=True, indent=4)
@ -261,11 +256,29 @@ class VersionCommands(object):
print sys.exc_info()[1]
def config_options():
global oparser
oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1",
def config_options(oparser):
oparser.add_option("--auth_url", default="http://localhost:5000/v2.0",
help="Auth API endpoint URL with port and version. \
Default: http://localhost:5000/v1.1")
Default: http://localhost:5000/v2.0")
oparser.add_option("--username", help="Login username")
oparser.add_option("--apikey", help="Api key")
oparser.add_option("--tenant_id",
help="Tenant Id associated with the account")
oparser.add_option("--auth_type", default="keystone",
help="Auth type to support different auth environments, \
Supported values are 'keystone', 'rax'.")
oparser.add_option("--service_type", default="reddwarf",
help="Service type is a name associated for the catalog")
oparser.add_option("--service_name", default="Reddwarf",
help="Service name as provided in the service catalog")
oparser.add_option("--service_url", default="",
help="Service endpoint to use if the catalog doesn't \
have one")
oparser.add_option("--region", default="RegionOne",
help="Region the service is located in")
oparser.add_option("-i", "--insecure", action="store_true",
dest="insecure", default=False,
help="Run in insecure mode for https endpoints.")
COMMANDS = {'auth': common.Auth,
@ -280,10 +293,9 @@ COMMANDS = {'auth': common.Auth,
def main():
# Parse arguments
global oparser
oparser = optparse.OptionParser("%prog [options] <cmd> <action> <args>",
version='1.0')
config_options()
config_options(oparser)
(options, args) = oparser.parse_args()
if not args:
@ -307,7 +319,12 @@ def main():
fn = actions.get(action)
try:
fn(*args)
# TODO(rnirmal): Fix when we have proper argument parsing for
# the rest of the commands.
if fn.__name__ == "login":
fn(*args, options=options)
else:
fn(*args)
sys.exit(0)
except TypeError as err:
print "Possible wrong number of arguments supplied."

@ -13,6 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import httplib2
import logging
import os
import time
import urlparse
@ -21,137 +24,88 @@ try:
except ImportError:
import simplejson as json
# Python 2.5 compat fix
if not hasattr(urlparse, 'parse_qsl'):
import cgi
urlparse.parse_qsl = cgi.parse_qsl
from novaclient.client import HTTPClient
from novaclient.v1_1.client import Client
from novaclient import exceptions as nova_exceptions
from reddwarfclient import auth
from reddwarfclient import exceptions
class ReddwarfHTTPClient(HTTPClient):
"""
Class for overriding the HTTP authenticate call and making it specific to
reddwarf
"""
_logger = logging.getLogger(__name__)
if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']:
ch = logging.StreamHandler()
_logger.setLevel(logging.DEBUG)
_logger.addHandler(ch)
def __init__(self, user, apikey, tenant, auth_url, service_name,
class ReddwarfHTTPClient(httplib2.Http):
USER_AGENT = 'python-reddwarfclient'
def __init__(self, user, password, tenant, auth_url, service_name,
service_url=None,
auth_strategy=None, **kwargs):
super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant,
auth_url,
**kwargs)
self.api_key = apikey
auth_strategy=None, insecure=False,
timeout=None, proxy_tenant_id=None,
proxy_token=None, region_name=None,
endpoint_type='publicURL', service_type=None,
timings=False):
super(ReddwarfHTTPClient, self).__init__(timeout=timeout)
self.username = user
self.password = password
self.tenant = tenant
self.service = service_name
self.management_url = service_url
if auth_strategy == "basic":
self.auth_strategy = self.basic_auth
elif auth_strategy == "rax":
self.auth_strategy = self._rax_auth
else:
self.auth_strategy = super(ReddwarfHTTPClient, self).authenticate
self.auth_url = auth_url.rstrip('/')
self.region_name = region_name
self.endpoint_type = endpoint_type
self.service_url = service_url
self.service_type = service_type
self.service_name = service_name
self.timings = timings
def authenticate(self):
self.auth_strategy()
self.times = [] # [("item", starttime, endtime), ...]
def _authenticate_without_tokens(self, url, body):
"""Authenticate and extract the service catalog."""
#TODO(tim.simpson): Copy pasta from Nova client's "_authenticate" but
# does not append "tokens" to the url.
self.auth_token = None
self.proxy_token = proxy_token
self.proxy_tenant_id = proxy_tenant_id
# Make sure we follow redirects when trying to reach Keystone
tmp_follow_all_redirects = self.follow_all_redirects
self.follow_all_redirects = True
# httplib2 overrides
self.force_exception_to_status_code = True
self.disable_ssl_certificate_validation = insecure
self.authenticator = auth.Authenticator(self, auth_strategy,
self.auth_url, self.username,
self.password, self.tenant,
region=region_name,
service_type=service_type,
service_name=service_name,
service_url=service_url)
try:
resp, body = self.request(url, "POST", body=body)
finally:
self.follow_all_redirects = tmp_follow_all_redirects
def get_timings(self):
return self.times
return resp, body
def http_log(self, args, kwargs, resp, body):
if not _logger.isEnabledFor(logging.DEBUG):
return
def basic_auth(self):
"""Authenticate against a v2.0 auth service."""
auth_url = self.auth_url
body = {"credentials": {"username": self.user,
"key": self.password}}
resp, resp_body = self._authenticate_without_tokens(auth_url, body)
string_parts = ['curl -i']
for element in args:
if element in ('GET', 'POST'):
string_parts.append(' -X %s' % element)
else:
string_parts.append(' %s' % element)
try:
self.auth_token = resp_body['auth']['token']['id']
except KeyError:
raise nova_exceptions.AuthorizationFailure()
catalog = resp_body['auth']['serviceCatalog']
if 'cloudDatabases' not in catalog:
raise nova_exceptions.EndpointNotFound()
endpoints = catalog['cloudDatabases']
for endpoint in endpoints:
if self.region_name is None or \
endpoint['region'] == self.region_name:
self.management_url = endpoint['publicURL']
return
raise nova_exceptions.EndpointNotFound()
for element in kwargs['headers']:
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
string_parts.append(header)
def _rax_auth(self):
"""Authenticate against the Rackspace auth service."""
body = {'auth': {
'RAX-KSKEY:apiKeyCredentials': {
'username': self.user,
'apiKey': self.password,
'tenantName': self.projectid}}}
resp, resp_body = self._authenticate_without_tokens(self.auth_url, body)
try:
self.auth_token = resp_body['access']['token']['id']
except KeyError:
raise nova_exceptions.AuthorizationFailure()
if not self.management_url:
catalogs = resp_body['access']['serviceCatalog']
for catalog in catalogs:
if catalog['name'] == "cloudDatabases":
endpoints = catalog['endpoints']
for endpoint in endpoints:
if self.region_name is None or \
endpoint['region'] == self.region_name:
self.management_url = endpoint['publicURL']
return
raise nova_exceptions.EndpointNotFound()
def _get_token(self, path, req_body):
"""Set the management url and auth token"""
token_url = urlparse.urljoin(self.auth_url, path)
resp, body = self.request(token_url, "POST", body=req_body)
if 'access' in body:
if not self.management_url:
# Assume the new Keystone lite:
catalog = body['access']['serviceCatalog']
for service in catalog:
if service['name'] == self.service:
self.management_url = service['adminURL']
self.auth_token = body['access']['token']['id']
else:
# Assume pre-Keystone Light:
try:
if not self.management_url:
keys = ['auth',
'serviceCatalog',
self.service,
0,
'publicURL']
url = body
for key in keys:
url = url[key]
self.management_url = url
self.auth_token = body['auth']['token']['id']
except KeyError:
raise NotImplementedError("Service: %s is not available"
% self.service)
_logger.debug("REQ: %s\n" % "".join(string_parts))
if 'body' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['body']))
_logger.debug("RESP:%s %s\n", resp, body)
def request(self, *args, **kwargs):
#TODO(tim.simpson): Copy and pasted from novaclient, since we raise
# extra exception subclasses not raised there.
kwargs.setdefault('headers', kwargs.get('headers', {}))
kwargs['headers']['User-Agent'] = self.USER_AGENT
kwargs['headers']['Accept'] = 'application/json'
@ -159,11 +113,10 @@ class ReddwarfHTTPClient(HTTPClient):
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['body'] = json.dumps(kwargs['body'])
resp, body = super(HTTPClient, self).request(*args, **kwargs)
resp, body = super(ReddwarfHTTPClient, self).request(*args, **kwargs)
# Save this in case anyone wants it.
self.last_response = (resp, body)
self.http_log(args, kwargs, resp, body)
if body:
@ -179,8 +132,60 @@ class ReddwarfHTTPClient(HTTPClient):
return resp, body
def _time_request(self, url, method, **kwargs):
start_time = time.time()
resp, body = self.request(url, method, **kwargs)
self.times.append(("%s %s" % (method, url),
start_time, time.time()))
return resp, body
class Dbaas(Client):
def _cs_request(self, url, method, **kwargs):
if not self.auth_token or not self.service_url:
self.authenticate()
# Perform the request once. If we get a 401 back then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
if self.tenant:
kwargs['headers']['X-Auth-Project-Id'] = self.tenant
resp, body = self._time_request(self.service_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized, ex:
try:
self.authenticate()
resp, body = self._time_request(self.service_url + url,
method, **kwargs)
return resp, body
except exceptions.Unauthorized:
raise ex
def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self._cs_request(url, 'PUT', **kwargs)
def delete(self, url, **kwargs):
return self._cs_request(url, 'DELETE', **kwargs)
def authenticate(self):
catalog = self.authenticator.authenticate()
self.auth_token = catalog.get_token()
if not self.service_url:
if self.endpoint_type == "publicURL":
self.service_url = catalog.get_public_url()
elif self.endpoint_type == "adminURL":
self.service_url = catalog.get_management_url()
class Dbaas(object):
"""
Top-level object to access the Rackspace Database as a Service API.
@ -200,8 +205,8 @@ class Dbaas(Client):
"""
def __init__(self, username, api_key, tenant=None, auth_url=None,
service_type='reddwarf', service_name='Reddwarf Service',
service_url=None, insecure=False, auth_strategy=None,
service_type='reddwarf', service_name='Reddwarf',
service_url=None, insecure=False, auth_strategy='keystone',
region_name=None):
from reddwarfclient.versions import Versions
from reddwarfclient.databases import Databases
@ -213,10 +218,8 @@ class Dbaas(Client):
from reddwarfclient.storage import StorageInfo
from reddwarfclient.management import Management
from reddwarfclient.accounts import Accounts
from reddwarfclient.config import Configs
from reddwarfclient.diagnostics import Interrogator
super(Dbaas, self).__init__(username, api_key, tenant, auth_url)
self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url,
service_type=service_type,
service_name=service_name,
@ -234,5 +237,21 @@ class Dbaas(Client):
self.storage = StorageInfo(self)
self.management = Management(self)
self.accounts = Accounts(self)
self.configs = Configs(self)
self.diagnostics = Interrogator(self)
def set_management_url(self, url):
self.client.management_url = url
def get_timings(self):
return self.client.get_timings()
def authenticate(self):
"""
Authenticate against the server.
This is called to perform an authentication to retrieve a token.
Returns on success; raises :exc:`exceptions.Unauthorized` if the
credentials are wrong.
"""
self.client.authenticate()

@ -17,7 +17,7 @@ import pickle
import sys
from reddwarfclient.client import Dbaas
import exceptions
from reddwarfclient import exceptions
APITOKEN = os.path.expanduser("~/.apitoken")
@ -31,9 +31,11 @@ def get_client():
dbaas = Dbaas(apitoken._user, apitoken._apikey,
tenant=apitoken._tenant, auth_url=apitoken._auth_url,
auth_strategy=apitoken._auth_strategy,
service_type=apitoken._service_type,
service_name=apitoken._service_name,
service_url=apitoken._service_url,
insecure=apitoken._insecure)
insecure=apitoken._insecure,
region_name=apitoken._region_name)
dbaas.client.auth_token = apitoken._token
return dbaas
except IOError:
@ -94,13 +96,15 @@ class APIToken(object):
is pickleable."""
def __init__(self, user, apikey, tenant, token, auth_url, auth_strategy,
service_name, service_url, region_name, insecure):
service_type, service_name, service_url, region_name,
insecure):
self._user = user
self._apikey = apikey
self._tenant = tenant
self._token = token
self._auth_url = auth_url
self._auth_strategy = auth_strategy
self._service_type = service_type
self._service_name = service_name
self._service_url = service_url
self._region_name = region_name
@ -113,20 +117,24 @@ class Auth(object):
def __init__(self):
pass
def login(self, user, apikey, tenant="dbaas",
auth_url="http://localhost:5000/v1.1",
auth_strategy=None, service_name="reddwarf",
region_name="default", service_url=None, insecure=True):
def login(self, options=None):
"""Login to retrieve an auth token to use for other api calls"""
try:
dbaas = Dbaas(user, apikey, tenant, auth_url=auth_url,
auth_strategy=auth_strategy,
service_name=service_name, region_name=None,
service_url=service_url, insecure=insecure)
dbaas = Dbaas(options.username, options.apikey, options.tenant_id,
auth_url=options.auth_url,
auth_strategy=options.auth_type,
service_type=options.service_type,
service_name=options.service_name,
region_name=options.region,
service_url=options.service_url,
insecure=options.insecure)
dbaas.authenticate()
apitoken = APIToken(user, apikey, tenant, dbaas.client.auth_token,
auth_url, auth_strategy, service_name,
service_url, region_name, insecure)
apitoken = APIToken(options.username, options.apikey,
options.tenant_id, dbaas.client.auth_token,
options.auth_url, options.auth_type,
options.service_type, options.service_name,
options.service_url, options.region,
options.insecure)
with open(APITOKEN, 'wb') as token:
pickle.dump(apitoken, token, protocol=2)

@ -1,73 +0,0 @@
# Copyright (c) 2011 OpenStack, LLC.
# All Rights Reserved.
#
# 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.
from novaclient import base
class Config(base.Resource):
"""
A configuration entry
"""
def __repr__(self):
return "<Config: %s>" % self.key
class Configs(base.ManagerWithFind):
"""
Manage :class:`Configs` resources.
"""
resource_class = Config
def create(self, configs):
"""
Create the configuration entries
"""
body = {"configs": configs}
url = "/mgmt/configs"
resp, body = self.api.client.post(url, body=body)
def delete(self, config):
"""
Delete an existing configuration
"""
url = "/mgmt/configs/%s" % config
self._delete(url)
def list(self):
"""
Get a list of all configuration entries
"""
resp, body = self.api.client.get("/mgmt/configs")
if not body:
raise Exception("Call to /mgmt/configs did not return a body.")
return [self.resource_class(self, res) for res in body['configs']]
def get(self, config):
"""
Get the specified configuration entry
"""
url = "/mgmt/configs/%s" % config
resp, body = self.api.client.get(url)
if not body:
raise Exception("Call to %s did not return a body." % url)
return self.resource_class(self, body['config'])
def update(self, config):
"""
Update the configuration entries
"""
body = {"config": config}
url = "/mgmt/configs/%s" % config['key']
resp, body = self.api.client.put(url, body=body)

@ -1,4 +1,4 @@
from novaclient import base
from reddwarfclient import base
from reddwarfclient.common import check_for_exceptions
from reddwarfclient.common import limit_url
from reddwarfclient.common import Paginated

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
import exceptions

@ -12,24 +12,113 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import exceptions
from novaclient.exceptions import UnsupportedVersion
from novaclient.exceptions import CommandError
from novaclient.exceptions import AuthorizationFailure
from novaclient.exceptions import NoUniqueMatch
from novaclient.exceptions import NoTokenLookupException
from novaclient.exceptions import EndpointNotFound
from novaclient.exceptions import AmbiguousEndpoints
from novaclient.exceptions import ClientException
from novaclient.exceptions import BadRequest
from novaclient.exceptions import Unauthorized
from novaclient.exceptions import Forbidden
from novaclient.exceptions import NotFound
from novaclient.exceptions import OverLimit
from novaclient.exceptions import HTTPNotImplemented
class UnsupportedVersion(Exception):
"""Indicates that the user is trying to use an unsupported
version of the API"""
pass
class UnprocessableEntity(exceptions.ClientException):
class CommandError(Exception):
pass
class AuthorizationFailure(Exception):
pass
class NoUniqueMatch(Exception):
pass
class NoTokenLookupException(Exception):
"""This form of authentication does not support looking up
endpoints from an existing token."""
pass
class EndpointNotFound(Exception):
"""Could not find Service or Region in Service Catalog."""
pass
class AmbiguousEndpoints(Exception):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
self.endpoints = endpoints
def __str__(self):
return "AmbiguousEndpoints: %s" % repr(self.endpoints)
class ClientException(Exception):
"""
The base exception class for all exceptions this library raises.
"""
def __init__(self, code, message=None, details=None, request_id=None):
self.code = code
self.message = message or self.__class__.message
self.details = details
self.request_id = request_id
def __str__(self):
formatted_string = "%s (HTTP %s)" % (self.message, self.code)
if self.request_id:
formatted_string += " (Request-ID: %s)" % self.request_id
return formatted_string
class BadRequest(ClientException):
"""
HTTP 400 - Bad request: you sent some malformed data.
"""
http_status = 400
message = "Bad request"
class Unauthorized(ClientException):
"""
HTTP 401 - Unauthorized: bad credentials.
"""
http_status = 401
message = "Unauthorized"
class Forbidden(ClientException):
"""
HTTP 403 - Forbidden: your credentials don't give you access to this
resource.
"""
http_status = 403
message = "Forbidden"
class NotFound(ClientException):
"""
HTTP 404 - Not found
"""
http_status = 404
message = "Not found"
class OverLimit(ClientException):
"""
HTTP 413 - Over limit: you're over the API limits for this time period.
"""
http_status = 413
message = "Over limit"
# NotImplemented is a python keyword.
class HTTPNotImplemented(ClientException):
"""
HTTP 501 - Not Implemented: the server does not support this operation.
"""
http_status = 501
message = "Not Implemented"
class UnprocessableEntity(ClientException):
"""
HTTP 422 - Unprocessable Entity: The request cannot be processed.
"""
@ -37,7 +126,16 @@ class UnprocessableEntity(exceptions.ClientException):
message = "Unprocessable Entity"
_code_map = dict((c.http_status, c) for c in [UnprocessableEntity])
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
# so we can do this:
# _code_map = dict((c.http_status, c)
# for c in ClientException.__subclasses__())
#
# Instead, we have to hardcode it:
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
Forbidden, NotFound, OverLimit,
HTTPNotImplemented,
UnprocessableEntity])
def from_response(response, body):
@ -51,10 +149,7 @@ def from_response(response, body):
if resp.status != 200:
raise exception_from_response(resp, body)
"""
cls = _code_map.get(response.status, None)
if not cls:
cls = exceptions._code_map.get(response.status,
exceptions.ClientException)
cls = _code_map.get(response.status, ClientException)
if body:
message = "n/a"
details = "n/a"
@ -64,4 +159,4 @@ def from_response(response, body):
details = error.get('details', None)
return cls(code=response.status, message=message, details=details)
else:
return cls(code=response.status)
return cls(code=response.status, request_id=request_id)

@ -14,7 +14,7 @@
# under the License.
from novaclient import base
from reddwarfclient import base
import exceptions

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
class Host(base.Resource):

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
import exceptions
import urlparse

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
import urlparse
from reddwarfclient.common import check_for_exceptions

@ -32,8 +32,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
if os.path.exists(os.path.join(possible_topdir, 'reddwarfclient',
'__init__.py')):
sys.path.insert(0, possible_topdir)
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
sys.path.insert(0, possible_topdir)
from reddwarfclient import common

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
from reddwarfclient import users
from reddwarfclient.common import check_for_exceptions

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
class Device(base.Resource):

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
from reddwarfclient.common import check_for_exceptions
from reddwarfclient.common import limit_url
from reddwarfclient.common import Paginated

68
reddwarfclient/utils.py Normal file

@ -0,0 +1,68 @@
# Copyright 2012 OpenStack LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import re
import sys
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
def env(*vars, **kwargs):
"""
returns the first environment variable set
if none are non-empty, defaults to '' or keyword arg default
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')
_slugify_strip_re = re.compile(r'[^\w\s-]')
_slugify_hyphenate_re = re.compile(r'[-\s]+')
# http://code.activestate.com/recipes/
# 577257-slugify-make-a-string-usable-in-a-url-or-filename/
def slugify(value):
"""
Normalizes string, converts to lowercase, removes non-alpha characters,
and converts spaces to hyphens.
From Django's "django/template/defaultfilters.py".
"""
import unicodedata
if not isinstance(value, unicode):
value = unicode(value)
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(_slugify_strip_re.sub('', value).strip().lower())
return _slugify_hyphenate_re.sub('-', value)

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import base
from reddwarfclient import base
class Version(base.Resource):

@ -21,7 +21,7 @@ import setuptools
import sys
requirements = ["python-novaclient"]
requirements = []
def read_file(file_name):