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:
parent
5241529fa2
commit
199ded7eb5
@ -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
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
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):
|
||||
|
2
setup.py
2
setup.py
@ -21,7 +21,7 @@ import setuptools
|
||||
import sys
|
||||
|
||||
|
||||
requirements = ["python-novaclient"]
|
||||
requirements = []
|
||||
|
||||
|
||||
def read_file(file_name):
|
||||
|
Loading…
x
Reference in New Issue
Block a user