Massive refactoring to the troveclient
The new client adheres to the standards of the other clients now. It prints out tables, uses ENVVAR's for auth, no longer stores pickled json in a login token, uses openstack common, and moves the cli operations into a v1 module for the future of trove when it has a v2 api. Please note for compatibility, the troveclient.compat module has the old cli. In order to deploy it, amend the setup.cfg to include the compat module. implements blueprint cli-compliance-upgrade Change-Id: Ie69d9dbc75ce90496da316244c97acca1877a327
This commit is contained in:
parent
12a577f15a
commit
fd43cbd73b
openstack-common.confrequirements.txtsetup.cfgsetup.py
tools
troveclient
9
openstack-common.conf
Normal file
9
openstack-common.conf
Normal file
@ -0,0 +1,9 @@
|
||||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from openstack-common
|
||||
module=apiclient
|
||||
module=strutils
|
||||
module=install_venv_common
|
||||
|
||||
# The base module to hold the copy of openstack.common
|
||||
base=troveclient
|
@ -1,5 +1,10 @@
|
||||
pbr>=0.5.16,<0.6
|
||||
argparse
|
||||
httplib2
|
||||
lxml>=2.3
|
||||
PrettyTable>=0.6,<0.8
|
||||
requests>=1.1
|
||||
simplejson>=2.0.9
|
||||
Babel>=1.3
|
||||
six>=1.4.1
|
||||
# Compat
|
||||
httplib2
|
||||
|
21
setup.cfg
21
setup.cfg
@ -17,15 +17,22 @@ classifier =
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 2.6
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
trove-cli = troveclient.cli:main
|
||||
trove-mgmt-cli = troveclient.mcli:main
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
||||
|
||||
[files]
|
||||
packages =
|
||||
troveclient
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
trove = troveclient.shell:main
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
2
setup.py
2
setup.py
@ -18,5 +18,5 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=0.5.20'],
|
||||
setup_requires=['pbr>=0.5.21,<1.0'],
|
||||
pbr=True)
|
||||
|
213
tools/install_venv_common.py
Normal file
213
tools/install_venv_common.py
Normal file
@ -0,0 +1,213 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Provides methods needed by installation script for OpenStack development
|
||||
virtual environments.
|
||||
|
||||
Since this script is used to bootstrap a virtualenv from the system's Python
|
||||
environment, it should be kept strictly compatible with Python 2.6.
|
||||
|
||||
Synced in from openstack-common
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
class InstallVenv(object):
|
||||
|
||||
def __init__(self, root, venv, requirements,
|
||||
test_requirements, py_version,
|
||||
project):
|
||||
self.root = root
|
||||
self.venv = venv
|
||||
self.requirements = requirements
|
||||
self.test_requirements = test_requirements
|
||||
self.py_version = py_version
|
||||
self.project = project
|
||||
|
||||
def die(self, message, *args):
|
||||
print(message % args, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def check_python_version(self):
|
||||
if sys.version_info < (2, 6):
|
||||
self.die("Need Python Version >= 2.6")
|
||||
|
||||
def run_command_with_code(self, cmd, redirect_output=True,
|
||||
check_exit_code=True):
|
||||
"""Runs a command in an out-of-process shell.
|
||||
|
||||
Returns the output of that command. Working directory is self.root.
|
||||
"""
|
||||
if redirect_output:
|
||||
stdout = subprocess.PIPE
|
||||
else:
|
||||
stdout = None
|
||||
|
||||
proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
|
||||
output = proc.communicate()[0]
|
||||
if check_exit_code and proc.returncode != 0:
|
||||
self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
|
||||
return (output, proc.returncode)
|
||||
|
||||
def run_command(self, cmd, redirect_output=True, check_exit_code=True):
|
||||
return self.run_command_with_code(cmd, redirect_output,
|
||||
check_exit_code)[0]
|
||||
|
||||
def get_distro(self):
|
||||
if (os.path.exists('/etc/fedora-release') or
|
||||
os.path.exists('/etc/redhat-release')):
|
||||
return Fedora(
|
||||
self.root, self.venv, self.requirements,
|
||||
self.test_requirements, self.py_version, self.project)
|
||||
else:
|
||||
return Distro(
|
||||
self.root, self.venv, self.requirements,
|
||||
self.test_requirements, self.py_version, self.project)
|
||||
|
||||
def check_dependencies(self):
|
||||
self.get_distro().install_virtualenv()
|
||||
|
||||
def create_virtualenv(self, no_site_packages=True):
|
||||
"""Creates the virtual environment and installs PIP.
|
||||
|
||||
Creates the virtual environment and installs PIP only into the
|
||||
virtual environment.
|
||||
"""
|
||||
if not os.path.isdir(self.venv):
|
||||
print('Creating venv...', end=' ')
|
||||
if no_site_packages:
|
||||
self.run_command(['virtualenv', '-q', '--no-site-packages',
|
||||
self.venv])
|
||||
else:
|
||||
self.run_command(['virtualenv', '-q', self.venv])
|
||||
print('done.')
|
||||
else:
|
||||
print("venv already exists...")
|
||||
pass
|
||||
|
||||
def pip_install(self, *args):
|
||||
self.run_command(['tools/with_venv.sh',
|
||||
'pip', 'install', '--upgrade'] + list(args),
|
||||
redirect_output=False)
|
||||
|
||||
def install_dependencies(self):
|
||||
print('Installing dependencies with pip (this can take a while)...')
|
||||
|
||||
# First things first, make sure our venv has the latest pip and
|
||||
# setuptools and pbr
|
||||
self.pip_install('pip>=1.4')
|
||||
self.pip_install('setuptools')
|
||||
self.pip_install('pbr')
|
||||
|
||||
self.pip_install('-r', self.requirements, '-r', self.test_requirements)
|
||||
|
||||
def post_process(self):
|
||||
self.get_distro().post_process()
|
||||
|
||||
def parse_args(self, argv):
|
||||
"""Parses command-line arguments."""
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-n', '--no-site-packages',
|
||||
action='store_true',
|
||||
help="Do not inherit packages from global Python "
|
||||
"install")
|
||||
return parser.parse_args(argv[1:])[0]
|
||||
|
||||
|
||||
class Distro(InstallVenv):
|
||||
|
||||
def check_cmd(self, cmd):
|
||||
return bool(self.run_command(['which', cmd],
|
||||
check_exit_code=False).strip())
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if self.check_cmd('easy_install'):
|
||||
print('Installing virtualenv via easy_install...', end=' ')
|
||||
if self.run_command(['easy_install', 'virtualenv']):
|
||||
print('Succeeded')
|
||||
return
|
||||
else:
|
||||
print('Failed')
|
||||
|
||||
self.die('ERROR: virtualenv not found.\n\n%s development'
|
||||
' requires virtualenv, please install it using your'
|
||||
' favorite package management tool' % self.project)
|
||||
|
||||
def post_process(self):
|
||||
"""Any distribution-specific post-processing gets done here.
|
||||
|
||||
In particular, this is useful for applying patches to code inside
|
||||
the venv.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Fedora(Distro):
|
||||
"""This covers all Fedora-based distributions.
|
||||
|
||||
Includes: Fedora, RHEL, CentOS, Scientific Linux
|
||||
"""
|
||||
|
||||
def check_pkg(self, pkg):
|
||||
return self.run_command_with_code(['rpm', '-q', pkg],
|
||||
check_exit_code=False)[1] == 0
|
||||
|
||||
def apply_patch(self, originalfile, patchfile):
|
||||
self.run_command(['patch', '-N', originalfile, patchfile],
|
||||
check_exit_code=False)
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if not self.check_pkg('python-virtualenv'):
|
||||
self.die("Please install 'python-virtualenv'.")
|
||||
|
||||
super(Fedora, self).install_virtualenv()
|
||||
|
||||
def post_process(self):
|
||||
"""Workaround for a bug in eventlet.
|
||||
|
||||
This currently affects RHEL6.1, but the fix can safely be
|
||||
applied to all RHEL and Fedora distributions.
|
||||
|
||||
This can be removed when the fix is applied upstream.
|
||||
|
||||
Nova: https://bugs.launchpad.net/nova/+bug/884915
|
||||
Upstream: https://bitbucket.org/eventlet/eventlet/issue/89
|
||||
RHEL: https://bugzilla.redhat.com/958868
|
||||
"""
|
||||
|
||||
if os.path.exists('contrib/redhat-eventlet.patch'):
|
||||
# Install "patch" program if it's not there
|
||||
if not self.check_pkg('patch'):
|
||||
self.die("Please install 'patch'.")
|
||||
|
||||
# Apply the eventlet patch
|
||||
self.apply_patch(os.path.join(self.venv, 'lib', self.py_version,
|
||||
'site-packages',
|
||||
'eventlet/green/subprocess.py'),
|
||||
'contrib/redhat-eventlet.patch')
|
@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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
|
||||
@ -13,20 +14,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
__all__ = ['__version__']
|
||||
|
||||
from troveclient.accounts import Accounts # noqa
|
||||
from troveclient.databases import Databases # noqa
|
||||
from troveclient.flavors import Flavors # noqa
|
||||
from troveclient.instances import Instances # noqa
|
||||
from troveclient.hosts import Hosts # noqa
|
||||
from troveclient.management import Management # noqa
|
||||
from troveclient.management import RootHistory # noqa
|
||||
from troveclient.management import MgmtFlavors # noqa
|
||||
from troveclient.root import Root # noqa
|
||||
from troveclient.storage import StorageInfo # noqa
|
||||
from troveclient.users import Users # noqa
|
||||
from troveclient.versions import Versions # noqa
|
||||
from troveclient.diagnostics import DiagnosticsInterrogator # noqa
|
||||
from troveclient.diagnostics import HwInfoInterrogator # noqa
|
||||
from troveclient.client import Dbaas # noqa
|
||||
from troveclient.client import TroveHTTPClient # noqa
|
||||
import pbr.version
|
||||
|
||||
version_info = pbr.version.VersionInfo('python-troveclient')
|
||||
# We have a circular import problem when we first run python setup.py sdist
|
||||
# It's harmless, so deflect it.
|
||||
try:
|
||||
__version__ = version_info.version_string()
|
||||
except AttributeError:
|
||||
__version__ = None
|
||||
|
@ -18,11 +18,14 @@
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
import hashlib
|
||||
import os
|
||||
from troveclient import exceptions
|
||||
|
||||
import six
|
||||
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
from troveclient import utils
|
||||
|
||||
|
||||
@ -92,14 +95,15 @@ class Manager(utils.HookableMixin):
|
||||
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',
|
||||
base_dir = utils.env('TROVECLIENT_UUID_CACHE_DIR',
|
||||
default="~/.troveclient")
|
||||
|
||||
# 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()
|
||||
username = utils.env('OS_USERNAME', 'TROVE_USERNAME')
|
||||
url = utils.env('OS_URL', 'TROVE_URL')
|
||||
uniqifier = hashlib.md5(username.encode('utf-8') +
|
||||
url.encode('utf-8')).hexdigest()
|
||||
|
||||
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
|
||||
|
||||
@ -163,11 +167,15 @@ class Manager(utils.HookableMixin):
|
||||
return body
|
||||
|
||||
|
||||
class ManagerWithFind(Manager):
|
||||
class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)):
|
||||
"""
|
||||
Like a `Manager`, but with additional `find()`/`findall()` methods.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def find(self, **kwargs):
|
||||
"""
|
||||
Find a single item with attributes matching ``**kwargs``.
|
||||
@ -193,7 +201,7 @@ class ManagerWithFind(Manager):
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
searches = list(kwargs.items())
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
@ -205,9 +213,6 @@ class ManagerWithFind(Manager):
|
||||
|
||||
return found
|
||||
|
||||
def list(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
@ -246,7 +251,7 @@ class Resource(object):
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
for (k, v) in six.iteritems(info):
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
except AttributeError:
|
||||
@ -265,8 +270,8 @@ class Resource(object):
|
||||
return self.__dict__[k]
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys()
|
||||
if k[0] != '_' and k != 'manager')
|
||||
reprkeys = sorted(k for k in list(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)
|
||||
|
||||
|
@ -13,12 +13,24 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import httplib2
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import urlparse
|
||||
import sys
|
||||
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
try:
|
||||
from eventlet import sleep
|
||||
except ImportError:
|
||||
from time import sleep
|
||||
|
||||
try:
|
||||
import json
|
||||
@ -30,92 +42,67 @@ if not hasattr(urlparse, 'parse_qsl'):
|
||||
import cgi
|
||||
urlparse.parse_qsl = cgi.parse_qsl
|
||||
|
||||
from troveclient import auth
|
||||
from troveclient import exceptions
|
||||
import requests
|
||||
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
from troveclient import service_catalog
|
||||
from troveclient import utils
|
||||
from troveclient.openstack.common.apiclient import client
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
RDC_PP = os.environ.get("RDC_PP", "False") == "True"
|
||||
|
||||
|
||||
expected_errors = (400, 401, 403, 404, 408, 409, 413, 422, 500, 501)
|
||||
|
||||
|
||||
def log_to_streamhandler(stream=None):
|
||||
stream = stream or sys.stderr
|
||||
ch = logging.StreamHandler(stream)
|
||||
_logger.setLevel(logging.DEBUG)
|
||||
_logger.addHandler(ch)
|
||||
|
||||
|
||||
if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']:
|
||||
log_to_streamhandler()
|
||||
|
||||
|
||||
class TroveHTTPClient(httplib2.Http):
|
||||
class HTTPClient(object):
|
||||
|
||||
USER_AGENT = 'python-troveclient'
|
||||
|
||||
def __init__(self, user, password, tenant, auth_url, service_name,
|
||||
service_url=None,
|
||||
auth_strategy=None, insecure=False,
|
||||
timeout=None, proxy_tenant_id=None,
|
||||
def __init__(self, user, password, projectid, auth_url, insecure=False,
|
||||
timeout=None, tenant_id=None, proxy_tenant_id=None,
|
||||
proxy_token=None, region_name=None,
|
||||
endpoint_type='publicURL', service_type=None,
|
||||
timings=False):
|
||||
|
||||
super(TroveHTTPClient, self).__init__(timeout=timeout)
|
||||
|
||||
self.username = user
|
||||
service_name=None, database_service_name=None, retries=None,
|
||||
http_log_debug=False, cacert=None):
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.tenant = tenant
|
||||
if auth_url:
|
||||
self.auth_url = auth_url.rstrip('/')
|
||||
else:
|
||||
self.auth_url = None
|
||||
self.projectid = projectid
|
||||
self.tenant_id = tenant_id
|
||||
self.auth_url = auth_url.rstrip('/')
|
||||
self.version = 'v1'
|
||||
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
|
||||
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
self.database_service_name = database_service_name
|
||||
self.retries = int(retries or 0)
|
||||
self.http_log_debug = http_log_debug
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
self.proxy_token = proxy_token
|
||||
self.proxy_tenant_id = proxy_tenant_id
|
||||
self.timeout = timeout
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
self.disable_ssl_certificate_validation = insecure
|
||||
|
||||
auth_cls = auth.get_authenticator_cls(auth_strategy)
|
||||
|
||||
self.authenticator = auth_cls(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)
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not RDC_PP:
|
||||
self.simple_log(args, kwargs, resp, body)
|
||||
if insecure:
|
||||
self.verify_cert = False
|
||||
else:
|
||||
self.pretty_log(args, kwargs, resp, body)
|
||||
if cacert:
|
||||
self.verify_cert = cacert
|
||||
else:
|
||||
self.verify_cert = True
|
||||
|
||||
def simple_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
if self.http_log_debug and not self._logger.handlers:
|
||||
ch = logging.StreamHandler()
|
||||
self._logger.setLevel(logging.DEBUG)
|
||||
self._logger.addHandler(ch)
|
||||
if hasattr(requests, 'logging'):
|
||||
requests.logging.getLogger(requests.__name__).addHandler(ch)
|
||||
|
||||
def http_log_req(self, args, kwargs):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST'):
|
||||
if element in ('GET', 'POST', 'DELETE', 'PUT'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
@ -124,117 +111,96 @@ class TroveHTTPClient(httplib2.Http):
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
_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)
|
||||
if 'data' in kwargs:
|
||||
string_parts.append(" -d '%s'" % (kwargs['data']))
|
||||
self._logger.debug("\nREQ: %s\n" % "".join(string_parts))
|
||||
|
||||
def pretty_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
def http_log_resp(self, resp):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
self._logger.debug(
|
||||
"RESP: [%s] %s\nRESP BODY: %s\n",
|
||||
resp.status_code,
|
||||
resp.headers,
|
||||
resp.text)
|
||||
|
||||
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)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
curl_cmd = "".join(string_parts)
|
||||
_logger.debug("REQUEST:")
|
||||
if 'body' in kwargs:
|
||||
_logger.debug("%s -d '%s'" % (curl_cmd, kwargs['body']))
|
||||
try:
|
||||
req_body = json.dumps(json.loads(kwargs['body']),
|
||||
sort_keys=True, indent=4)
|
||||
except:
|
||||
req_body = kwargs['body']
|
||||
_logger.debug("BODY: %s\n" % (req_body))
|
||||
else:
|
||||
_logger.debug(curl_cmd)
|
||||
|
||||
try:
|
||||
resp_body = json.dumps(json.loads(body), sort_keys=True, indent=4)
|
||||
except:
|
||||
resp_body = body
|
||||
_logger.debug("RESPONSE HEADERS: %s" % resp)
|
||||
_logger.debug("RESPONSE BODY : %s" % resp_body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
def request(self, url, method, **kwargs):
|
||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
self.morph_request(kwargs)
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = json.dumps(kwargs['body'])
|
||||
del kwargs['body']
|
||||
|
||||
resp, body = super(TroveHTTPClient, self).request(*args, **kwargs)
|
||||
if self.timeout:
|
||||
kwargs.setdefault('timeout', self.timeout)
|
||||
self.http_log_req((url, method,), kwargs)
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
verify=self.verify_cert,
|
||||
**kwargs)
|
||||
self.http_log_resp(resp)
|
||||
|
||||
# Save this in case anyone wants it.
|
||||
self.last_response = (resp, body)
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
if resp.text:
|
||||
try:
|
||||
body = self.morph_response_body(body)
|
||||
except exceptions.ResponseFormatError:
|
||||
# Acceptable only if the response status is an error code.
|
||||
# Otherwise its the API or client misbehaving.
|
||||
self.raise_error_from_status(resp, None)
|
||||
raise # Not accepted!
|
||||
body = json.loads(resp.text)
|
||||
except ValueError:
|
||||
pass
|
||||
body = None
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in expected_errors:
|
||||
raise exceptions.from_response(resp, body)
|
||||
if resp.status_code >= 400:
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
return resp, body
|
||||
|
||||
def raise_error_from_status(self, resp, body):
|
||||
if resp.status in expected_errors:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
def morph_request(self, kwargs):
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
if 'body' in kwargs:
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
def morph_response_body(self, body_string):
|
||||
try:
|
||||
return json.loads(body_string)
|
||||
except ValueError:
|
||||
raise exceptions.ResponseFormatError()
|
||||
|
||||
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
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
def request():
|
||||
auth_attempts = 0
|
||||
attempts = 0
|
||||
backoff = 1
|
||||
while True:
|
||||
attempts += 1
|
||||
if not self.management_url or not self.auth_token:
|
||||
self.authenticate()
|
||||
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
|
||||
|
||||
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:
|
||||
return request()
|
||||
except exceptions.Unauthorized, ex:
|
||||
self.authenticate()
|
||||
return request()
|
||||
if self.projectid:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
try:
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.BadRequest as e:
|
||||
if attempts > self.retries:
|
||||
raise
|
||||
except exceptions.Unauthorized:
|
||||
if auth_attempts > 0:
|
||||
raise
|
||||
self._logger.debug("Unauthorized, reauthenticating.")
|
||||
self.management_url = self.auth_token = None
|
||||
# First reauth. Discount this attempt.
|
||||
attempts -= 1
|
||||
auth_attempts += 1
|
||||
continue
|
||||
except exceptions.ClientException as e:
|
||||
if attempts > self.retries:
|
||||
raise
|
||||
if 500 <= e.code <= 599:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
# Catch a connection refused from requests.request
|
||||
self._logger.debug("Connection refused: %s" % e)
|
||||
msg = 'Unable to establish connection: %s' % e
|
||||
raise exceptions.ConnectionError(msg)
|
||||
self._logger.debug(
|
||||
"Failed attempt(%s of %s), retrying in %s seconds" %
|
||||
(attempts, self.retries, backoff))
|
||||
sleep(backoff)
|
||||
backoff *= 2
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
@ -248,124 +214,192 @@ class TroveHTTPClient(httplib2.Http):
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def authenticate(self):
|
||||
"""Auths the client and gets a token. May optionally set a service url.
|
||||
|
||||
The client will get auth errors until the authentication step
|
||||
occurs. Additionally, if a service_url was not explicitly given in
|
||||
the clients __init__ method, one will be obtained from the auth
|
||||
service.
|
||||
|
||||
def _extract_service_catalog(self, url, resp, body, extract_token=True):
|
||||
"""See what the auth service told us and process the response.
|
||||
We may get redirected to another site, fail or actually get
|
||||
back a service catalog with a token and our endpoints.
|
||||
"""
|
||||
catalog = self.authenticator.authenticate()
|
||||
if self.service_url:
|
||||
possible_service_url = None
|
||||
|
||||
if resp.status_code == 200: # content must always present
|
||||
try:
|
||||
self.auth_url = url
|
||||
self.service_catalog = \
|
||||
service_catalog.ServiceCatalog(body)
|
||||
|
||||
if extract_token:
|
||||
self.auth_token = self.service_catalog.get_token()
|
||||
|
||||
management_url = self.service_catalog.url_for(
|
||||
attr='region',
|
||||
filter_value=self.region_name,
|
||||
endpoint_type=self.endpoint_type,
|
||||
service_type=self.service_type,
|
||||
service_name=self.service_name,
|
||||
database_service_name=self.database_service_name)
|
||||
self.management_url = management_url.rstrip('/')
|
||||
return None
|
||||
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_code == 305:
|
||||
return resp['location']
|
||||
else:
|
||||
if self.endpoint_type == "publicURL":
|
||||
possible_service_url = catalog.get_public_url()
|
||||
elif self.endpoint_type == "adminURL":
|
||||
possible_service_url = catalog.get_management_url()
|
||||
self.authenticate_with_token(catalog.get_token(), possible_service_url)
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
def authenticate_with_token(self, token, service_url=None):
|
||||
self.auth_token = token
|
||||
if not self.service_url:
|
||||
if not service_url:
|
||||
raise exceptions.ServiceUrlNotGiven()
|
||||
else:
|
||||
self.service_url = service_url
|
||||
def _fetch_endpoints_from_auth(self, url):
|
||||
"""We have a token, but don't know the final endpoint for
|
||||
the region. We have to go back to the auth service and
|
||||
ask again. This request requires an admin-level token
|
||||
to work. The proxy token supplied could be from a low-level enduser.
|
||||
|
||||
We can't get this from the keystone service endpoint, we have to use
|
||||
the admin endpoint.
|
||||
|
||||
class Dbaas(object):
|
||||
"""
|
||||
Top-level object to access the Rackspace Database as a Service API.
|
||||
This will overwrite our admin token with the user token.
|
||||
"""
|
||||
|
||||
Create an instance with your creds::
|
||||
|
||||
>>> red = Dbaas(USERNAME, API_KEY, TENANT, AUTH_URL, SERVICE_NAME, \
|
||||
SERVICE_URL)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>>> red.instances.list()
|
||||
...
|
||||
>>> red.flavors.list()
|
||||
...
|
||||
|
||||
&c.
|
||||
"""
|
||||
|
||||
def __init__(self, username, api_key, tenant=None, auth_url=None,
|
||||
service_type='database', service_name=None,
|
||||
service_url=None, insecure=False, auth_strategy='keystone',
|
||||
region_name=None, client_cls=TroveHTTPClient):
|
||||
from troveclient.versions import Versions
|
||||
from troveclient.databases import Databases
|
||||
from troveclient.flavors import Flavors
|
||||
from troveclient.instances import Instances
|
||||
from troveclient.limits import Limits
|
||||
from troveclient.users import Users
|
||||
from troveclient.root import Root
|
||||
from troveclient.hosts import Hosts
|
||||
from troveclient.quota import Quotas
|
||||
from troveclient.backups import Backups
|
||||
from troveclient.security_groups import SecurityGroups
|
||||
from troveclient.security_groups import SecurityGroupRules
|
||||
from troveclient.storage import StorageInfo
|
||||
from troveclient.management import Management
|
||||
from troveclient.management import MgmtFlavors
|
||||
from troveclient.accounts import Accounts
|
||||
from troveclient.diagnostics import DiagnosticsInterrogator
|
||||
from troveclient.diagnostics import HwInfoInterrogator
|
||||
|
||||
self.client = client_cls(username, api_key, tenant, auth_url,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
service_url=service_url,
|
||||
insecure=insecure,
|
||||
auth_strategy=auth_strategy,
|
||||
region_name=region_name)
|
||||
self.versions = Versions(self)
|
||||
self.databases = Databases(self)
|
||||
self.flavors = Flavors(self)
|
||||
self.instances = Instances(self)
|
||||
self.limits = Limits(self)
|
||||
self.users = Users(self)
|
||||
self.root = Root(self)
|
||||
self.hosts = Hosts(self)
|
||||
self.quota = Quotas(self)
|
||||
self.backups = Backups(self)
|
||||
self.security_groups = SecurityGroups(self)
|
||||
self.security_group_rules = SecurityGroupRules(self)
|
||||
self.storage = StorageInfo(self)
|
||||
self.management = Management(self)
|
||||
self.mgmt_flavor = MgmtFlavors(self)
|
||||
self.accounts = Accounts(self)
|
||||
self.diagnostics = DiagnosticsInterrogator(self)
|
||||
self.hwinfo = HwInfoInterrogator(self)
|
||||
|
||||
class Mgmt(object):
|
||||
def __init__(self, dbaas):
|
||||
self.instances = dbaas.management
|
||||
self.hosts = dbaas.hosts
|
||||
self.accounts = dbaas.accounts
|
||||
self.storage = dbaas.storage
|
||||
|
||||
self.mgmt = Mgmt(self)
|
||||
|
||||
def set_management_url(self, url):
|
||||
self.client.management_url = url
|
||||
|
||||
def get_timings(self):
|
||||
return self.client.get_timings()
|
||||
# GET ...:5001/v2.0/tokens/#####/endpoints
|
||||
url = '/'.join([url, 'tokens', '%s?belongsTo=%s'
|
||||
% (self.proxy_token, self.proxy_tenant_id)])
|
||||
self._logger.debug("Using Endpoint URL: %s" % url)
|
||||
resp, body = self.request(url, "GET",
|
||||
headers={'X-Auth-Token': self.auth_token})
|
||||
return self._extract_service_catalog(url, resp, body,
|
||||
extract_token=False)
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Authenticate against the server.
|
||||
magic_tuple = urlparse.urlsplit(self.auth_url)
|
||||
scheme, netloc, path, query, frag = magic_tuple
|
||||
port = magic_tuple.port
|
||||
if port is None:
|
||||
port = 80
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
This is called to perform an authentication to retrieve a token.
|
||||
# TODO(sandy): Assume admin endpoint is 35357 for now.
|
||||
# Ideally this is going to have to be provided by the service catalog.
|
||||
new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,))
|
||||
admin_url = urlparse.urlunsplit((scheme, new_netloc,
|
||||
path, query, frag))
|
||||
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
||||
auth_url = self.auth_url
|
||||
if self.version == "v2.0":
|
||||
while auth_url:
|
||||
if "TROVE_RAX_AUTH" in os.environ:
|
||||
auth_url = self._rax_auth(auth_url)
|
||||
else:
|
||||
auth_url = self._v2_auth(auth_url)
|
||||
|
||||
# Are we acting on behalf of another user via an
|
||||
# existing token? If so, our actual endpoints may
|
||||
# be different than that of the admin token.
|
||||
if self.proxy_token:
|
||||
self._fetch_endpoints_from_auth(admin_url)
|
||||
# Since keystone no longer returns the user token
|
||||
# with the endpoints any more, we need to replace
|
||||
# our service account token with the user token.
|
||||
self.auth_token = self.proxy_token
|
||||
else:
|
||||
try:
|
||||
while auth_url:
|
||||
auth_url = self._v1_auth(auth_url)
|
||||
# In some configurations trove makes redirection to
|
||||
# v2.0 keystone endpoint. Also, new location does not contain
|
||||
# real endpoint, only hostname and port.
|
||||
except exceptions.AuthorizationFailure:
|
||||
if auth_url.find('v2.0') < 0:
|
||||
auth_url = auth_url + '/v2.0'
|
||||
self._v2_auth(auth_url)
|
||||
|
||||
def _v1_auth(self, url):
|
||||
if self.proxy_token:
|
||||
raise exceptions.NoTokenLookupException()
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.password}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
resp, body = self.request(url, 'GET', headers=headers)
|
||||
if resp.status_code in (200, 204): # in some cases we get No Content
|
||||
try:
|
||||
mgmt_header = 'x-server-management-url'
|
||||
self.management_url = resp.headers[mgmt_header].rstrip('/')
|
||||
self.auth_token = resp.headers['x-auth-token']
|
||||
self.auth_url = url
|
||||
except (KeyError, TypeError):
|
||||
raise exceptions.AuthorizationFailure()
|
||||
elif resp.status_code == 305:
|
||||
return resp.headers['location']
|
||||
else:
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
def _v2_auth(self, url):
|
||||
"""Authenticate against a v2.0 auth service."""
|
||||
body = {"auth": {
|
||||
"passwordCredentials": {"username": self.user,
|
||||
"password": self.password}}}
|
||||
|
||||
if self.projectid:
|
||||
body['auth']['tenantName'] = self.projectid
|
||||
elif self.tenant_id:
|
||||
body['auth']['tenantId'] = self.tenant_id
|
||||
|
||||
self._authenticate(url, body)
|
||||
|
||||
def _rax_auth(self, url):
|
||||
"""Authenticate against the Rackspace auth service."""
|
||||
body = {"auth": {
|
||||
"RAX-KSKEY:apiKeyCredentials": {
|
||||
"username": self.user,
|
||||
"apiKey": self.password,
|
||||
"tenantName": self.projectid}}}
|
||||
|
||||
self._authenticate(url, body)
|
||||
|
||||
def _authenticate(self, url, body):
|
||||
"""Authenticate and extract the service catalog."""
|
||||
token_url = url + "/tokens"
|
||||
|
||||
# Make sure we follow redirects when trying to reach Keystone
|
||||
resp, body = self.request(
|
||||
token_url,
|
||||
"POST",
|
||||
body=body,
|
||||
allow_redirects=True)
|
||||
|
||||
return self._extract_service_catalog(url, resp, body)
|
||||
|
||||
def get_database_api_version_from_endpoint(self):
|
||||
magic_tuple = urlparse.urlsplit(self.management_url)
|
||||
scheme, netloc, path, query, frag = magic_tuple
|
||||
v = path.split("/")[1]
|
||||
valid_versions = ['v1.0']
|
||||
if v not in valid_versions:
|
||||
msg = "Invalid client version '%s'. must be one of: %s" % (
|
||||
(v, ', '.join(valid_versions)))
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
return v[1:]
|
||||
|
||||
|
||||
def get_version_map():
|
||||
return {
|
||||
'1.0': 'troveclient.v1.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def Client(version, *args, **kwargs):
|
||||
version_map = get_version_map()
|
||||
client_class = client.BaseClient.get_class('database',
|
||||
version, version_map)
|
||||
return client_class(*args, **kwargs)
|
||||
|
@ -20,44 +20,17 @@ import pickle
|
||||
import sys
|
||||
|
||||
from troveclient import client
|
||||
from troveclient.xml import TroveXmlClient
|
||||
#from troveclient.xml import TroveXmlClient
|
||||
from troveclient import exceptions
|
||||
|
||||
from urllib import quote
|
||||
|
||||
|
||||
def methods_of(obj):
|
||||
"""Get all callable methods of an object that don't start with underscore
|
||||
returns a list of tuples of the form (method_name, method)"""
|
||||
result = {}
|
||||
for i in dir(obj):
|
||||
if callable(getattr(obj, i)) and not i.startswith('_'):
|
||||
result[i] = getattr(obj, i)
|
||||
return result
|
||||
|
||||
|
||||
def check_for_exceptions(resp, body):
|
||||
if resp.status in (400, 422, 500):
|
||||
if resp.status_code in (400, 422, 500):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
|
||||
def print_actions(cmd, actions):
|
||||
"""Print help for the command with list of options and description"""
|
||||
print ("Available actions for '%s' cmd:") % cmd
|
||||
for k, v in actions.iteritems():
|
||||
print "\t%-20s%s" % (k, v.__doc__)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def print_commands(commands):
|
||||
"""Print the list of available commands and description"""
|
||||
|
||||
print "Available commands"
|
||||
for k, v in commands.iteritems():
|
||||
print "\t%-20s%s" % (k, v.__doc__)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def limit_url(url, limit=None, marker=None):
|
||||
if not limit and not marker:
|
||||
return url
|
||||
@ -79,325 +52,6 @@ def quote_user_host(user, host):
|
||||
return quoted.replace('.', '%2e')
|
||||
|
||||
|
||||
class CliOptions(object):
|
||||
"""A token object containing the user, apikey and token which
|
||||
is pickleable."""
|
||||
|
||||
APITOKEN = os.path.expanduser("~/.apitoken")
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
'username': None,
|
||||
'apikey': None,
|
||||
'tenant_id': None,
|
||||
'auth_url': None,
|
||||
'auth_type': 'keystone',
|
||||
'service_type': 'database',
|
||||
'service_name': '',
|
||||
'region': 'RegionOne',
|
||||
'service_url': None,
|
||||
'insecure': False,
|
||||
'verbose': False,
|
||||
'debug': False,
|
||||
'token': None,
|
||||
'xml': None,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in self.DEFAULT_VALUES.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
kwargs = copy.deepcopy(cls.DEFAULT_VALUES)
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls):
|
||||
try:
|
||||
with open(cls.APITOKEN, 'rb') as token:
|
||||
return pickle.load(token)
|
||||
except IOError:
|
||||
pass # File probably not found.
|
||||
except:
|
||||
print("ERROR: Token file found at %s was corrupt." % cls.APITOKEN)
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def save_from_instance_fields(cls, instance):
|
||||
apitoken = cls.default()
|
||||
for key, default_value in cls.DEFAULT_VALUES.items():
|
||||
final_value = getattr(instance, key, default_value)
|
||||
setattr(apitoken, key, final_value)
|
||||
with open(cls.APITOKEN, 'wb') as token:
|
||||
pickle.dump(apitoken, token, protocol=2)
|
||||
|
||||
@classmethod
|
||||
def create_optparser(cls, load_file):
|
||||
oparser = optparse.OptionParser(
|
||||
usage="%prog [options] <cmd> <action> <args>",
|
||||
version='1.0', conflict_handler='resolve')
|
||||
if load_file:
|
||||
file = cls.load_from_file()
|
||||
else:
|
||||
file = cls.default()
|
||||
|
||||
def add_option(*args, **kwargs):
|
||||
if len(args) == 1:
|
||||
name = args[0]
|
||||
else:
|
||||
name = args[1]
|
||||
kwargs['default'] = getattr(file, name, cls.DEFAULT_VALUES[name])
|
||||
oparser.add_option("--%s" % name, **kwargs)
|
||||
|
||||
add_option("verbose", action="store_true",
|
||||
help="Show equivalent curl statement along "
|
||||
"with actual HTTP communication.")
|
||||
add_option("debug", action="store_true",
|
||||
help="Show the stack trace on errors.")
|
||||
add_option("auth_url", help="Auth API endpoint URL with port and "
|
||||
"version. Default: http://localhost:5000/v2.0")
|
||||
add_option("username", help="Login username")
|
||||
add_option("apikey", help="Api key")
|
||||
add_option("tenant_id",
|
||||
help="Tenant Id associated with the account")
|
||||
add_option("auth_type",
|
||||
help="Auth type to support different auth environments, \
|
||||
Supported values are 'keystone', 'rax'.")
|
||||
add_option("service_type",
|
||||
help="Service type is a name associated for the catalog")
|
||||
add_option("service_name",
|
||||
help="Service name as provided in the service catalog")
|
||||
add_option("service_url",
|
||||
help="Service endpoint to use "
|
||||
"if the catalog doesn't have one.")
|
||||
add_option("region", help="Region the service is located in")
|
||||
add_option("insecure", action="store_true",
|
||||
help="Run in insecure mode for https endpoints.")
|
||||
add_option("token", help="Token from a prior login.")
|
||||
add_option("xml", action="store_true", help="Changes format to XML.")
|
||||
|
||||
oparser.add_option("--secure", action="store_false", dest="insecure",
|
||||
help="Run in insecure mode for https endpoints.")
|
||||
oparser.add_option("--json", action="store_false", dest="xml",
|
||||
help="Changes format to JSON.")
|
||||
oparser.add_option("--terse", action="store_false", dest="verbose",
|
||||
help="Toggles verbose mode off.")
|
||||
oparser.add_option("--hide-debug", action="store_false", dest="debug",
|
||||
help="Toggles debug mode off.")
|
||||
return oparser
|
||||
|
||||
|
||||
class ArgumentRequired(Exception):
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
|
||||
def __str__(self):
|
||||
return 'Argument "--%s" required.' % self.param
|
||||
|
||||
|
||||
class ArgumentsRequired(ArgumentRequired):
|
||||
def __init__(self, *params):
|
||||
self.params = params
|
||||
|
||||
def __str__(self):
|
||||
returnstring = 'Specify at least one of these arguments: '
|
||||
for param in self.params:
|
||||
returnstring = returnstring + '"--%s" ' % param
|
||||
return returnstring
|
||||
|
||||
|
||||
class CommandsBase(object):
|
||||
params = []
|
||||
|
||||
def __init__(self, parser):
|
||||
self._parse_options(parser)
|
||||
|
||||
def _get_client(self):
|
||||
"""Creates the all important client object."""
|
||||
try:
|
||||
if self.xml:
|
||||
client_cls = TroveXmlClient
|
||||
else:
|
||||
client_cls = client.TroveHTTPClient
|
||||
if self.verbose:
|
||||
client.log_to_streamhandler(sys.stdout)
|
||||
client.RDC_PP = True
|
||||
return client.Dbaas(self.username, self.apikey, self.tenant_id,
|
||||
auth_url=self.auth_url,
|
||||
auth_strategy=self.auth_type,
|
||||
service_type=self.service_type,
|
||||
service_name=self.service_name,
|
||||
region_name=self.region,
|
||||
service_url=self.service_url,
|
||||
insecure=self.insecure,
|
||||
client_cls=client_cls)
|
||||
except:
|
||||
if self.debug:
|
||||
raise
|
||||
print sys.exc_info()[1]
|
||||
|
||||
def _safe_exec(self, func, *args, **kwargs):
|
||||
if not self.debug:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except:
|
||||
print(sys.exc_info()[1])
|
||||
return None
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _prepare_parser(cls, parser):
|
||||
for param in cls.params:
|
||||
parser.add_option("--%s" % param)
|
||||
|
||||
def _parse_options(self, parser):
|
||||
opts, args = parser.parse_args()
|
||||
for param in opts.__dict__:
|
||||
value = getattr(opts, param)
|
||||
setattr(self, param, value)
|
||||
|
||||
def _require(self, *params):
|
||||
for param in params:
|
||||
if not hasattr(self, param):
|
||||
raise ArgumentRequired(param)
|
||||
if not getattr(self, param):
|
||||
raise ArgumentRequired(param)
|
||||
|
||||
def _require_at_least_one_of(self, *params):
|
||||
# One or more of params is required to be present.
|
||||
argument_present = False
|
||||
for param in params:
|
||||
if hasattr(self, param):
|
||||
if getattr(self, param):
|
||||
argument_present = True
|
||||
if argument_present is False:
|
||||
raise ArgumentsRequired(*params)
|
||||
|
||||
def _make_list(self, *params):
|
||||
# Convert the listed params to lists.
|
||||
for param in params:
|
||||
raw = getattr(self, param)
|
||||
if isinstance(raw, list):
|
||||
return
|
||||
raw = [item.strip() for item in raw.split(',')]
|
||||
setattr(self, param, raw)
|
||||
|
||||
def _pretty_print(self, func, *args, **kwargs):
|
||||
if self.verbose:
|
||||
self._safe_exec(func, *args, **kwargs)
|
||||
return # Skip this, since the verbose stuff will show up anyway.
|
||||
|
||||
def wrapped_func():
|
||||
result = func(*args, **kwargs)
|
||||
if result:
|
||||
print(json.dumps(result._info, sort_keys=True, indent=4))
|
||||
else:
|
||||
print("OK")
|
||||
|
||||
self._safe_exec(wrapped_func)
|
||||
|
||||
def _dumps(self, item):
|
||||
return json.dumps(item, sort_keys=True, indent=4)
|
||||
|
||||
def _pretty_list(self, func, *args, **kwargs):
|
||||
result = self._safe_exec(func, *args, **kwargs)
|
||||
if self.verbose:
|
||||
return
|
||||
if result and len(result) > 0:
|
||||
for item in result:
|
||||
print(self._dumps(item._info))
|
||||
else:
|
||||
print("OK")
|
||||
|
||||
def _pretty_paged(self, func, *args, **kwargs):
|
||||
try:
|
||||
limit = self.limit
|
||||
if limit:
|
||||
limit = int(limit, 10)
|
||||
result = func(*args, limit=limit, marker=self.marker, **kwargs)
|
||||
if self.verbose:
|
||||
return # Verbose already shows the output, so skip this.
|
||||
if result and len(result) > 0:
|
||||
for item in result:
|
||||
print self._dumps(item._info)
|
||||
if result.links:
|
||||
print("Links:")
|
||||
for link in result.links:
|
||||
print self._dumps((link))
|
||||
else:
|
||||
print("OK")
|
||||
except:
|
||||
if self.debug:
|
||||
raise
|
||||
print sys.exc_info()[1]
|
||||
|
||||
|
||||
class Auth(CommandsBase):
|
||||
"""Authenticate with your username and api key"""
|
||||
params = [
|
||||
'apikey',
|
||||
'auth_strategy',
|
||||
'auth_type',
|
||||
'auth_url',
|
||||
'options',
|
||||
'region',
|
||||
'service_name',
|
||||
'service_type',
|
||||
'service_url',
|
||||
'tenant_id',
|
||||
'username',
|
||||
]
|
||||
|
||||
def __init__(self, parser):
|
||||
super(Auth, self).__init__(parser)
|
||||
self.dbaas = None
|
||||
|
||||
def login(self):
|
||||
"""Login to retrieve an auth token to use for other api calls"""
|
||||
self._require('username', 'apikey', 'tenant_id', 'auth_url')
|
||||
try:
|
||||
self.dbaas = self._get_client()
|
||||
self.dbaas.authenticate()
|
||||
self.token = self.dbaas.client.auth_token
|
||||
self.service_url = self.dbaas.client.service_url
|
||||
CliOptions.save_from_instance_fields(self)
|
||||
print("Token aquired! Saving to %s..." % CliOptions.APITOKEN)
|
||||
print(" service_url = %s" % self.service_url)
|
||||
print(" token = %s" % self.token)
|
||||
except:
|
||||
if self.debug:
|
||||
raise
|
||||
print sys.exc_info()[1]
|
||||
|
||||
|
||||
class AuthedCommandsBase(CommandsBase):
|
||||
"""Commands that work only with an authicated client."""
|
||||
|
||||
def __init__(self, parser):
|
||||
"""Makes sure a token is available somehow and logs in."""
|
||||
super(AuthedCommandsBase, self).__init__(parser)
|
||||
try:
|
||||
self._require('token')
|
||||
except ArgumentRequired:
|
||||
if self.debug:
|
||||
raise
|
||||
print('No token argument supplied. Use the "auth login" command '
|
||||
'to log in and get a token.\n')
|
||||
sys.exit(1)
|
||||
try:
|
||||
self._require('service_url')
|
||||
except ArgumentRequired:
|
||||
if self.debug:
|
||||
raise
|
||||
print('No service_url given.\n')
|
||||
sys.exit(1)
|
||||
self.dbaas = self._get_client()
|
||||
# Actually set the token to avoid a re-auth.
|
||||
self.dbaas.client.auth_token = self.token
|
||||
self.dbaas.client.authenticate_with_token(self.token, self.service_url)
|
||||
|
||||
|
||||
class Paginated(object):
|
||||
""" Pretends to be a list if you iterate over it, but also keeps a
|
||||
next property you can use to get the next page of data. """
|
||||
|
32
troveclient/compat/__init__.py
Normal file
32
troveclient/compat/__init__.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# 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 troveclient.v1.accounts import Accounts # noqa
|
||||
from troveclient.v1.databases import Databases # noqa
|
||||
from troveclient.v1.flavors import Flavors # noqa
|
||||
from troveclient.v1.instances import Instances # noqa
|
||||
from troveclient.v1.hosts import Hosts # noqa
|
||||
from troveclient.v1.management import Management # noqa
|
||||
from troveclient.v1.management import RootHistory # noqa
|
||||
from troveclient.v1.management import MgmtFlavors # noqa
|
||||
from troveclient.v1.root import Root # noqa
|
||||
from troveclient.v1.storage import StorageInfo # noqa
|
||||
from troveclient.v1.users import Users # noqa
|
||||
from troveclient.compat.versions import Versions # noqa
|
||||
from troveclient.v1.diagnostics import DiagnosticsInterrogator # noqa
|
||||
from troveclient.v1.diagnostics import HwInfoInterrogator # noqa
|
||||
from troveclient.compat.client import Dbaas # noqa
|
||||
from troveclient.compat.client import TroveHTTPClient # noqa
|
252
troveclient/compat/auth.py
Normal file
252
troveclient/compat/auth.py
Normal file
@ -0,0 +1,252 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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 troveclient.compat import exceptions
|
||||
|
||||
|
||||
def get_authenticator_cls(cls_or_name):
|
||||
"""Factory method to retrieve Authenticator class."""
|
||||
if isinstance(cls_or_name, type):
|
||||
return cls_or_name
|
||||
elif isinstance(cls_or_name, basestring):
|
||||
if cls_or_name == "keystone":
|
||||
return KeyStoneV2Authenticator
|
||||
elif cls_or_name == "rax":
|
||||
return RaxAuthenticator
|
||||
elif cls_or_name == "auth1.1":
|
||||
return Auth1_1
|
||||
elif cls_or_name == "fake":
|
||||
return FakeAuth
|
||||
|
||||
raise ValueError("Could not determine authenticator class from the given "
|
||||
"value %r." % cls_or_name)
|
||||
|
||||
|
||||
class Authenticator(object):
|
||||
"""
|
||||
Helper class to perform Keystone or other miscellaneous authentication.
|
||||
|
||||
The "authenticate" method returns a ServiceCatalog, which can be used
|
||||
to obtain a token.
|
||||
|
||||
"""
|
||||
|
||||
URL_REQUIRED = True
|
||||
|
||||
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, root_key='access'):
|
||||
"""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,
|
||||
root_key=root_key)
|
||||
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):
|
||||
raise NotImplementedError("Missing authenticate method.")
|
||||
|
||||
|
||||
class KeyStoneV2Authenticator(Authenticator):
|
||||
def authenticate(self):
|
||||
if self.url is None:
|
||||
raise exceptions.AuthUrlNotGiven()
|
||||
return self._v2_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)
|
||||
|
||||
|
||||
class Auth1_1(Authenticator):
|
||||
def authenticate(self):
|
||||
"""Authenticate against a v2.0 auth service."""
|
||||
if self.url is None:
|
||||
raise exceptions.AuthUrlNotGiven()
|
||||
auth_url = self.url
|
||||
body = {
|
||||
"credentials": {
|
||||
"username": self.username,
|
||||
"key": self.password
|
||||
}}
|
||||
return self._authenticate(auth_url, body, root_key='auth')
|
||||
|
||||
|
||||
class RaxAuthenticator(Authenticator):
|
||||
def authenticate(self):
|
||||
if self.url is None:
|
||||
raise exceptions.AuthUrlNotGiven()
|
||||
return self._rax_auth(self.url)
|
||||
|
||||
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 FakeAuth(Authenticator):
|
||||
"""Useful for faking auth."""
|
||||
|
||||
def authenticate(self):
|
||||
class FakeCatalog(object):
|
||||
def __init__(self, auth):
|
||||
self.auth = auth
|
||||
|
||||
def get_public_url(self):
|
||||
return "%s/%s" % ('http://localhost:8779/v1.0',
|
||||
self.auth.tenant)
|
||||
|
||||
def get_token(self):
|
||||
return self.auth.tenant
|
||||
|
||||
return FakeCatalog(self)
|
||||
|
||||
|
||||
class ServiceCatalog(object):
|
||||
"""Represents a Keystone Service Catalog which describes a service.
|
||||
|
||||
This class has methods to obtain a valid token as well as a public service
|
||||
url and a management url.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, resource_dict, region=None, service_type=None,
|
||||
service_name=None, service_url=None, root_key='access'):
|
||||
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.root_key = root_key
|
||||
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[self.root_key]['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 Trove 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 'serviceCatalog' not in self.catalog[self.root_key]:
|
||||
raise exceptions.EndpointNotFound()
|
||||
|
||||
# Full catalog ...
|
||||
catalog = self.catalog[self.root_key]['serviceCatalog']
|
||||
|
||||
for service in catalog:
|
||||
if service.get("type") != self.service_type:
|
||||
continue
|
||||
|
||||
if (self.service_name and self.service_type == 'database' 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)
|
294
troveclient/compat/base.py
Normal file
294
troveclient/compat/base.py
Normal file
@ -0,0 +1,294 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# 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 troveclient.compat import exceptions
|
||||
from troveclient.compat 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):
|
||||
"""
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
as a parameter when dealing with relationships.
|
||||
"""
|
||||
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:
|
||||
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="~/.troveclient")
|
||||
|
||||
# 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
|
@ -31,7 +31,7 @@ if os.path.exists(os.path.join(possible_topdir, 'troveclient',
|
||||
'__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from troveclient import common
|
||||
from troveclient.compat import common
|
||||
|
||||
|
||||
class InstanceCommands(common.AuthedCommandsBase):
|
||||
@ -342,6 +342,7 @@ COMMANDS = {
|
||||
|
||||
def main():
|
||||
# Parse arguments
|
||||
import pdb
|
||||
load_file = True
|
||||
for index, arg in enumerate(sys.argv):
|
||||
if (arg == "auth" and len(sys.argv) > (index + 1)
|
373
troveclient/compat/client.py
Normal file
373
troveclient/compat/client.py
Normal file
@ -0,0 +1,373 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
import httplib2
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import urlparse
|
||||
import sys
|
||||
|
||||
try:
|
||||
import json
|
||||
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 troveclient.compat import auth
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
RDC_PP = os.environ.get("RDC_PP", "False") == "True"
|
||||
|
||||
|
||||
expected_errors = (400, 401, 403, 404, 408, 409, 413, 422, 500, 501)
|
||||
|
||||
|
||||
def log_to_streamhandler(stream=None):
|
||||
stream = stream or sys.stderr
|
||||
ch = logging.StreamHandler(stream)
|
||||
_logger.setLevel(logging.DEBUG)
|
||||
_logger.addHandler(ch)
|
||||
|
||||
|
||||
if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']:
|
||||
log_to_streamhandler()
|
||||
|
||||
|
||||
class TroveHTTPClient(httplib2.Http):
|
||||
|
||||
USER_AGENT = 'python-troveclient'
|
||||
|
||||
def __init__(self, user, password, tenant, auth_url, service_name,
|
||||
service_url=None,
|
||||
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(TroveHTTPClient, self).__init__(timeout=timeout)
|
||||
|
||||
self.username = user
|
||||
self.password = password
|
||||
self.tenant = tenant
|
||||
if auth_url:
|
||||
self.auth_url = auth_url.rstrip('/')
|
||||
else:
|
||||
self.auth_url = None
|
||||
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
|
||||
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
|
||||
self.auth_token = None
|
||||
self.proxy_token = proxy_token
|
||||
self.proxy_tenant_id = proxy_tenant_id
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
self.disable_ssl_certificate_validation = insecure
|
||||
|
||||
auth_cls = auth.get_authenticator_cls(auth_strategy)
|
||||
|
||||
self.authenticator = auth_cls(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)
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not RDC_PP:
|
||||
self.simple_log(args, kwargs, resp, body)
|
||||
else:
|
||||
self.pretty_log(args, kwargs, resp, body)
|
||||
|
||||
def simple_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
_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 pretty_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
curl_cmd = "".join(string_parts)
|
||||
_logger.debug("REQUEST:")
|
||||
if 'body' in kwargs:
|
||||
_logger.debug("%s -d '%s'" % (curl_cmd, kwargs['body']))
|
||||
try:
|
||||
req_body = json.dumps(json.loads(kwargs['body']),
|
||||
sort_keys=True, indent=4)
|
||||
except:
|
||||
req_body = kwargs['body']
|
||||
_logger.debug("BODY: %s\n" % (req_body))
|
||||
else:
|
||||
_logger.debug(curl_cmd)
|
||||
|
||||
try:
|
||||
resp_body = json.dumps(json.loads(body), sort_keys=True, indent=4)
|
||||
except:
|
||||
resp_body = body
|
||||
_logger.debug("RESPONSE HEADERS: %s" % resp)
|
||||
_logger.debug("RESPONSE BODY : %s" % resp_body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
self.morph_request(kwargs)
|
||||
|
||||
resp, body = super(TroveHTTPClient, self).request(*args, **kwargs)
|
||||
# compat between requests and httplib2
|
||||
resp.status_code = resp.status
|
||||
|
||||
# Save this in case anyone wants it.
|
||||
self.last_response = (resp, body)
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = self.morph_response_body(body)
|
||||
except exceptions.ResponseFormatError:
|
||||
# Acceptable only if the response status is an error code.
|
||||
# Otherwise its the API or client misbehaving.
|
||||
self.raise_error_from_status(resp, None)
|
||||
raise # Not accepted!
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in expected_errors:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def raise_error_from_status(self, resp, body):
|
||||
if resp.status in expected_errors:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
def morph_request(self, kwargs):
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
if 'body' in kwargs:
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
def morph_response_body(self, body_string):
|
||||
try:
|
||||
return json.loads(body_string)
|
||||
except ValueError:
|
||||
raise exceptions.ResponseFormatError()
|
||||
|
||||
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
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
def request():
|
||||
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
|
||||
|
||||
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:
|
||||
return request()
|
||||
except exceptions.Unauthorized, ex:
|
||||
self.authenticate()
|
||||
return request()
|
||||
|
||||
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):
|
||||
"""Auths the client and gets a token. May optionally set a service url.
|
||||
|
||||
The client will get auth errors until the authentication step
|
||||
occurs. Additionally, if a service_url was not explicitly given in
|
||||
the clients __init__ method, one will be obtained from the auth
|
||||
service.
|
||||
|
||||
"""
|
||||
catalog = self.authenticator.authenticate()
|
||||
if self.service_url:
|
||||
possible_service_url = None
|
||||
else:
|
||||
if self.endpoint_type == "publicURL":
|
||||
possible_service_url = catalog.get_public_url()
|
||||
elif self.endpoint_type == "adminURL":
|
||||
possible_service_url = catalog.get_management_url()
|
||||
self.authenticate_with_token(catalog.get_token(), possible_service_url)
|
||||
|
||||
def authenticate_with_token(self, token, service_url=None):
|
||||
self.auth_token = token
|
||||
if not self.service_url:
|
||||
if not service_url:
|
||||
raise exceptions.ServiceUrlNotGiven()
|
||||
else:
|
||||
self.service_url = service_url
|
||||
|
||||
|
||||
class Dbaas(object):
|
||||
"""
|
||||
Top-level object to access the Rackspace Database as a Service API.
|
||||
|
||||
Create an instance with your creds::
|
||||
|
||||
>>> red = Dbaas(USERNAME, API_KEY, TENANT, AUTH_URL, SERVICE_NAME, \
|
||||
SERVICE_URL)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>>> red.instances.list()
|
||||
...
|
||||
>>> red.flavors.list()
|
||||
...
|
||||
|
||||
&c.
|
||||
"""
|
||||
|
||||
def __init__(self, username, api_key, tenant=None, auth_url=None,
|
||||
service_type='database', service_name=None,
|
||||
service_url=None, insecure=False, auth_strategy='keystone',
|
||||
region_name=None, client_cls=TroveHTTPClient):
|
||||
from troveclient.compat.versions import Versions
|
||||
from troveclient.v1.databases import Databases
|
||||
from troveclient.v1.flavors import Flavors
|
||||
from troveclient.v1.instances import Instances
|
||||
from troveclient.v1.limits import Limits
|
||||
from troveclient.v1.users import Users
|
||||
from troveclient.v1.root import Root
|
||||
from troveclient.v1.hosts import Hosts
|
||||
from troveclient.v1.quota import Quotas
|
||||
from troveclient.v1.backups import Backups
|
||||
from troveclient.v1.security_groups import SecurityGroups
|
||||
from troveclient.v1.security_groups import SecurityGroupRules
|
||||
from troveclient.v1.storage import StorageInfo
|
||||
from troveclient.v1.management import Management
|
||||
from troveclient.v1.management import MgmtFlavors
|
||||
from troveclient.v1.accounts import Accounts
|
||||
from troveclient.v1.diagnostics import DiagnosticsInterrogator
|
||||
from troveclient.v1.diagnostics import HwInfoInterrogator
|
||||
|
||||
self.client = client_cls(username, api_key, tenant, auth_url,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
service_url=service_url,
|
||||
insecure=insecure,
|
||||
auth_strategy=auth_strategy,
|
||||
region_name=region_name)
|
||||
self.versions = Versions(self)
|
||||
self.databases = Databases(self)
|
||||
self.flavors = Flavors(self)
|
||||
self.instances = Instances(self)
|
||||
self.limits = Limits(self)
|
||||
self.users = Users(self)
|
||||
self.root = Root(self)
|
||||
self.hosts = Hosts(self)
|
||||
self.quota = Quotas(self)
|
||||
self.backups = Backups(self)
|
||||
self.security_groups = SecurityGroups(self)
|
||||
self.security_group_rules = SecurityGroupRules(self)
|
||||
self.storage = StorageInfo(self)
|
||||
self.management = Management(self)
|
||||
self.mgmt_flavor = MgmtFlavors(self)
|
||||
self.accounts = Accounts(self)
|
||||
self.diagnostics = DiagnosticsInterrogator(self)
|
||||
self.hwinfo = HwInfoInterrogator(self)
|
||||
|
||||
class Mgmt(object):
|
||||
def __init__(self, dbaas):
|
||||
self.instances = dbaas.management
|
||||
self.hosts = dbaas.hosts
|
||||
self.accounts = dbaas.accounts
|
||||
self.storage = dbaas.storage
|
||||
|
||||
self.mgmt = Mgmt(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()
|
429
troveclient/compat/common.py
Normal file
429
troveclient/compat/common.py
Normal file
@ -0,0 +1,429 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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 copy
|
||||
import json
|
||||
import optparse
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
from troveclient.compat import client
|
||||
from troveclient.compat.xml import TroveXmlClient
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
from urllib import quote
|
||||
|
||||
|
||||
def methods_of(obj):
|
||||
"""Get all callable methods of an object that don't start with underscore
|
||||
returns a list of tuples of the form (method_name, method)"""
|
||||
result = {}
|
||||
for i in dir(obj):
|
||||
if callable(getattr(obj, i)) and not i.startswith('_'):
|
||||
result[i] = getattr(obj, i)
|
||||
return result
|
||||
|
||||
|
||||
def check_for_exceptions(resp, body):
|
||||
if resp.status in (400, 422, 500):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
|
||||
def print_actions(cmd, actions):
|
||||
"""Print help for the command with list of options and description"""
|
||||
print ("Available actions for '%s' cmd:") % cmd
|
||||
for k, v in actions.iteritems():
|
||||
print "\t%-20s%s" % (k, v.__doc__)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def print_commands(commands):
|
||||
"""Print the list of available commands and description"""
|
||||
|
||||
print "Available commands"
|
||||
for k, v in commands.iteritems():
|
||||
print "\t%-20s%s" % (k, v.__doc__)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def limit_url(url, limit=None, marker=None):
|
||||
if not limit and not marker:
|
||||
return url
|
||||
query = []
|
||||
if marker:
|
||||
query.append("marker=%s" % marker)
|
||||
if limit:
|
||||
query.append("limit=%s" % limit)
|
||||
query = '?' + '&'.join(query)
|
||||
return url + query
|
||||
|
||||
|
||||
def quote_user_host(user, host):
|
||||
quoted = ''
|
||||
if host:
|
||||
quoted = quote("%s@%s" % (user, host))
|
||||
else:
|
||||
quoted = quote("%s" % user)
|
||||
return quoted.replace('.', '%2e')
|
||||
|
||||
|
||||
class CliOptions(object):
|
||||
"""A token object containing the user, apikey and token which
|
||||
is pickleable."""
|
||||
|
||||
APITOKEN = os.path.expanduser("~/.apitoken")
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
'username': None,
|
||||
'apikey': None,
|
||||
'tenant_id': None,
|
||||
'auth_url': None,
|
||||
'auth_type': 'keystone',
|
||||
'service_type': 'database',
|
||||
'service_name': '',
|
||||
'region': 'RegionOne',
|
||||
'service_url': None,
|
||||
'insecure': False,
|
||||
'verbose': False,
|
||||
'debug': False,
|
||||
'token': None,
|
||||
'xml': None,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in self.DEFAULT_VALUES.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
kwargs = copy.deepcopy(cls.DEFAULT_VALUES)
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls):
|
||||
try:
|
||||
with open(cls.APITOKEN, 'rb') as token:
|
||||
return pickle.load(token)
|
||||
except IOError:
|
||||
pass # File probably not found.
|
||||
except:
|
||||
print("ERROR: Token file found at %s was corrupt." % cls.APITOKEN)
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def save_from_instance_fields(cls, instance):
|
||||
apitoken = cls.default()
|
||||
for key, default_value in cls.DEFAULT_VALUES.items():
|
||||
final_value = getattr(instance, key, default_value)
|
||||
setattr(apitoken, key, final_value)
|
||||
with open(cls.APITOKEN, 'wb') as token:
|
||||
pickle.dump(apitoken, token, protocol=2)
|
||||
|
||||
@classmethod
|
||||
def create_optparser(cls, load_file):
|
||||
oparser = optparse.OptionParser(
|
||||
usage="%prog [options] <cmd> <action> <args>",
|
||||
version='1.0', conflict_handler='resolve')
|
||||
if load_file:
|
||||
file = cls.load_from_file()
|
||||
else:
|
||||
file = cls.default()
|
||||
|
||||
def add_option(*args, **kwargs):
|
||||
if len(args) == 1:
|
||||
name = args[0]
|
||||
else:
|
||||
name = args[1]
|
||||
kwargs['default'] = getattr(file, name, cls.DEFAULT_VALUES[name])
|
||||
oparser.add_option("--%s" % name, **kwargs)
|
||||
|
||||
add_option("verbose", action="store_true",
|
||||
help="Show equivalent curl statement along "
|
||||
"with actual HTTP communication.")
|
||||
add_option("debug", action="store_true",
|
||||
help="Show the stack trace on errors.")
|
||||
add_option("auth_url", help="Auth API endpoint URL with port and "
|
||||
"version. Default: http://localhost:5000/v2.0")
|
||||
add_option("username", help="Login username")
|
||||
add_option("apikey", help="Api key")
|
||||
add_option("tenant_id",
|
||||
help="Tenant Id associated with the account")
|
||||
add_option("auth_type",
|
||||
help="Auth type to support different auth environments, \
|
||||
Supported values are 'keystone', 'rax'.")
|
||||
add_option("service_type",
|
||||
help="Service type is a name associated for the catalog")
|
||||
add_option("service_name",
|
||||
help="Service name as provided in the service catalog")
|
||||
add_option("service_url",
|
||||
help="Service endpoint to use "
|
||||
"if the catalog doesn't have one.")
|
||||
add_option("region", help="Region the service is located in")
|
||||
add_option("insecure", action="store_true",
|
||||
help="Run in insecure mode for https endpoints.")
|
||||
add_option("token", help="Token from a prior login.")
|
||||
add_option("xml", action="store_true", help="Changes format to XML.")
|
||||
|
||||
oparser.add_option("--secure", action="store_false", dest="insecure",
|
||||
help="Run in insecure mode for https endpoints.")
|
||||
oparser.add_option("--json", action="store_false", dest="xml",
|
||||
help="Changes format to JSON.")
|
||||
oparser.add_option("--terse", action="store_false", dest="verbose",
|
||||
help="Toggles verbose mode off.")
|
||||
oparser.add_option("--hide-debug", action="store_false", dest="debug",
|
||||
help="Toggles debug mode off.")
|
||||
return oparser
|
||||
|
||||
|
||||
class ArgumentRequired(Exception):
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
|
||||
def __str__(self):
|
||||
return 'Argument "--%s" required.' % self.param
|
||||
|
||||
|
||||
class ArgumentsRequired(ArgumentRequired):
|
||||
def __init__(self, *params):
|
||||
self.params = params
|
||||
|
||||
def __str__(self):
|
||||
returnstring = 'Specify at least one of these arguments: '
|
||||
for param in self.params:
|
||||
returnstring = returnstring + '"--%s" ' % param
|
||||
return returnstring
|
||||
|
||||
|
||||
class CommandsBase(object):
|
||||
params = []
|
||||
|
||||
def __init__(self, parser):
|
||||
self._parse_options(parser)
|
||||
|
||||
def _get_client(self):
|
||||
"""Creates the all important client object."""
|
||||
try:
|
||||
if self.xml:
|
||||
client_cls = TroveXmlClient
|
||||
else:
|
||||
client_cls = client.TroveHTTPClient
|
||||
if self.verbose:
|
||||
client.log_to_streamhandler(sys.stdout)
|
||||
client.RDC_PP = True
|
||||
return client.Dbaas(self.username, self.apikey, self.tenant_id,
|
||||
auth_url=self.auth_url,
|
||||
auth_strategy=self.auth_type,
|
||||
service_type=self.service_type,
|
||||
service_name=self.service_name,
|
||||
region_name=self.region,
|
||||
service_url=self.service_url,
|
||||
insecure=self.insecure,
|
||||
client_cls=client_cls)
|
||||
except:
|
||||
if self.debug:
|
||||
raise
|
||||
print sys.exc_info()[1]
|
||||
|
||||
def _safe_exec(self, func, *args, **kwargs):
|
||||
if not self.debug:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except:
|
||||
print(sys.exc_info()[1])
|
||||
return None
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _prepare_parser(cls, parser):
|
||||
for param in cls.params:
|
||||
parser.add_option("--%s" % param)
|
||||
|
||||
def _parse_options(self, parser):
|
||||
opts, args = parser.parse_args()
|
||||
for param in opts.__dict__:
|
||||
value = getattr(opts, param)
|
||||
setattr(self, param, value)
|
||||
|
||||
def _require(self, *params):
|
||||
for param in params:
|
||||
if not hasattr(self, param):
|
||||
raise ArgumentRequired(param)
|
||||
if not getattr(self, param):
|
||||
raise ArgumentRequired(param)
|
||||
|
||||
def _require_at_least_one_of(self, *params):
|
||||
# One or more of params is required to be present.
|
||||
argument_present = False
|
||||
for param in params:
|
||||
if hasattr(self, param):
|
||||
if getattr(self, param):
|
||||
argument_present = True
|
||||
if argument_present is False:
|
||||
raise ArgumentsRequired(*params)
|
||||
|
||||
def _make_list(self, *params):
|
||||
# Convert the listed params to lists.
|
||||
for param in params:
|
||||
raw = getattr(self, param)
|
||||
if isinstance(raw, list):
|
||||
return
|
||||
raw = [item.strip() for item in raw.split(',')]
|
||||
setattr(self, param, raw)
|
||||
|
||||
def _pretty_print(self, func, *args, **kwargs):
|
||||
if self.verbose:
|
||||
self._safe_exec(func, *args, **kwargs)
|
||||
return # Skip this, since the verbose stuff will show up anyway.
|
||||
|
||||
def wrapped_func():
|
||||
result = func(*args, **kwargs)
|
||||
if result:
|
||||
print(json.dumps(result._info, sort_keys=True, indent=4))
|
||||
else:
|
||||
print("OK")
|
||||
|
||||
self._safe_exec(wrapped_func)
|
||||
|
||||
def _dumps(self, item):
|
||||
return json.dumps(item, sort_keys=True, indent=4)
|
||||
|
||||
def _pretty_list(self, func, *args, **kwargs):
|
||||
result = self._safe_exec(func, *args, **kwargs)
|
||||
if self.verbose:
|
||||
return
|
||||
if result and len(result) > 0:
|
||||
for item in result:
|
||||
print(self._dumps(item._info))
|
||||
else:
|
||||
print("OK")
|
||||
|
||||
def _pretty_paged(self, func, *args, **kwargs):
|
||||
try:
|
||||
limit = self.limit
|
||||
if limit:
|
||||
limit = int(limit, 10)
|
||||
result = func(*args, limit=limit, marker=self.marker, **kwargs)
|
||||
if self.verbose:
|
||||
return # Verbose already shows the output, so skip this.
|
||||
if result and len(result) > 0:
|
||||
for item in result:
|
||||
print self._dumps(item._info)
|
||||
if result.links:
|
||||
print("Links:")
|
||||
for link in result.links:
|
||||
print self._dumps((link))
|
||||
else:
|
||||
print("OK")
|
||||
except:
|
||||
if self.debug:
|
||||
raise
|
||||
print sys.exc_info()[1]
|
||||
|
||||
|
||||
class Auth(CommandsBase):
|
||||
"""Authenticate with your username and api key"""
|
||||
params = [
|
||||
'apikey',
|
||||
'auth_strategy',
|
||||
'auth_type',
|
||||
'auth_url',
|
||||
'options',
|
||||
'region',
|
||||
'service_name',
|
||||
'service_type',
|
||||
'service_url',
|
||||
'tenant_id',
|
||||
'username',
|
||||
]
|
||||
|
||||
def __init__(self, parser):
|
||||
super(Auth, self).__init__(parser)
|
||||
self.dbaas = None
|
||||
|
||||
def login(self):
|
||||
"""Login to retrieve an auth token to use for other api calls"""
|
||||
self._require('username', 'apikey', 'tenant_id', 'auth_url')
|
||||
try:
|
||||
self.dbaas = self._get_client()
|
||||
self.dbaas.authenticate()
|
||||
self.token = self.dbaas.client.auth_token
|
||||
self.service_url = self.dbaas.client.service_url
|
||||
CliOptions.save_from_instance_fields(self)
|
||||
print("Token aquired! Saving to %s..." % CliOptions.APITOKEN)
|
||||
print(" service_url = %s" % self.service_url)
|
||||
print(" token = %s" % self.token)
|
||||
except:
|
||||
if self.debug:
|
||||
raise
|
||||
print sys.exc_info()[1]
|
||||
|
||||
|
||||
class AuthedCommandsBase(CommandsBase):
|
||||
"""Commands that work only with an authicated client."""
|
||||
|
||||
def __init__(self, parser):
|
||||
"""Makes sure a token is available somehow and logs in."""
|
||||
super(AuthedCommandsBase, self).__init__(parser)
|
||||
try:
|
||||
self._require('token')
|
||||
except ArgumentRequired:
|
||||
if self.debug:
|
||||
raise
|
||||
print('No token argument supplied. Use the "auth login" command '
|
||||
'to log in and get a token.\n')
|
||||
sys.exit(1)
|
||||
try:
|
||||
self._require('service_url')
|
||||
except ArgumentRequired:
|
||||
if self.debug:
|
||||
raise
|
||||
print('No service_url given.\n')
|
||||
sys.exit(1)
|
||||
self.dbaas = self._get_client()
|
||||
# Actually set the token to avoid a re-auth.
|
||||
self.dbaas.client.auth_token = self.token
|
||||
self.dbaas.client.authenticate_with_token(self.token, self.service_url)
|
||||
|
||||
|
||||
class Paginated(object):
|
||||
""" Pretends to be a list if you iterate over it, but also keeps a
|
||||
next property you can use to get the next page of data. """
|
||||
|
||||
def __init__(self, items=[], next_marker=None, links=[]):
|
||||
self.items = items
|
||||
self.next = next_marker
|
||||
self.links = links
|
||||
|
||||
def __len__(self):
|
||||
return len(self.items)
|
||||
|
||||
def __iter__(self):
|
||||
return self.items.__iter__()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.items[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.items[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.items[key]
|
||||
|
||||
def __reversed__(self):
|
||||
return reversed(self.items)
|
||||
|
||||
def __contains__(self, needle):
|
||||
return needle in self.items
|
179
troveclient/compat/exceptions.py
Normal file
179
troveclient/compat/exceptions.py
Normal file
@ -0,0 +1,179 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
class UnsupportedVersion(Exception):
|
||||
"""Indicates that the user is trying to use an unsupported
|
||||
version of the API"""
|
||||
pass
|
||||
|
||||
|
||||
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 AuthUrlNotGiven(EndpointNotFound):
|
||||
"""The auth url was not given."""
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUrlNotGiven(EndpointNotFound):
|
||||
"""The service url was not given."""
|
||||
pass
|
||||
|
||||
|
||||
class ResponseFormatError(Exception):
|
||||
"""Could not parse the response format."""
|
||||
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.
|
||||
"""
|
||||
http_status = 422
|
||||
message = "Unprocessable Entity"
|
||||
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Return an instance of an ClientException or subclass
|
||||
based on an httplib2 response.
|
||||
|
||||
Usage::
|
||||
|
||||
resp, body = http.request(...)
|
||||
if resp.status != 200:
|
||||
raise exception_from_response(resp, body)
|
||||
"""
|
||||
cls = _code_map.get(response.status, ClientException)
|
||||
if body:
|
||||
message = "n/a"
|
||||
details = "n/a"
|
||||
if hasattr(body, 'keys'):
|
||||
error = body[body.keys()[0]]
|
||||
message = error.get('message', None)
|
||||
details = error.get('details', None)
|
||||
return cls(code=response.status, message=message, details=details)
|
||||
else:
|
||||
request_id = response.get('x-compute-request-id')
|
||||
return cls(code=response.status, request_id=request_id)
|
@ -28,11 +28,11 @@ import sys
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'troveclient',
|
||||
if os.path.exists(os.path.join(possible_topdir, 'troveclient.compat',
|
||||
'__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from troveclient import common
|
||||
from troveclient.compat import common
|
||||
|
||||
|
||||
oparser = None
|
67
troveclient/compat/utils.py
Normal file
67
troveclient/compat/utils.py
Normal file
@ -0,0 +1,67 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
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 troveclient import base
|
||||
from troveclient.compat import base
|
||||
|
||||
|
||||
class Version(base.Resource):
|
292
troveclient/compat/xml.py
Normal file
292
troveclient/compat/xml.py
Normal file
@ -0,0 +1,292 @@
|
||||
from lxml import etree
|
||||
from numbers import Number
|
||||
|
||||
from troveclient.compat import exceptions
|
||||
from troveclient.compat.client import TroveHTTPClient
|
||||
|
||||
XML_NS = {None: "http://docs.openstack.org/database/api/v1.0"}
|
||||
|
||||
# If XML element is listed here then this searches through the ancestors.
|
||||
LISTIFY = {
|
||||
"accounts": [[]],
|
||||
"databases": [[]],
|
||||
"flavors": [[]],
|
||||
"instances": [[]],
|
||||
"links": [[]],
|
||||
"hosts": [[]],
|
||||
"devices": [[]],
|
||||
"users": [[]],
|
||||
"versions": [[]],
|
||||
"attachments": [[]],
|
||||
"limits": [[]],
|
||||
"security_groups": [[]],
|
||||
"backups": [[]]
|
||||
}
|
||||
|
||||
|
||||
class IntDict(object):
|
||||
pass
|
||||
|
||||
|
||||
TYPE_MAP = {
|
||||
"instance": {
|
||||
"volume": {
|
||||
"used": float,
|
||||
"size": int,
|
||||
},
|
||||
"deleted": bool,
|
||||
"server": {
|
||||
"local_id": int,
|
||||
"deleted": bool,
|
||||
},
|
||||
},
|
||||
"instances": {
|
||||
"deleted": bool,
|
||||
},
|
||||
"deleted": bool,
|
||||
"flavor": {
|
||||
"ram": int,
|
||||
},
|
||||
"diagnostics": {
|
||||
"vmHwm": int,
|
||||
"vmPeak": int,
|
||||
"vmSize": int,
|
||||
"threads": int,
|
||||
"vmRss": int,
|
||||
"fdSize": int,
|
||||
},
|
||||
"security_group_rule": {
|
||||
"from_port": int,
|
||||
"to_port": int,
|
||||
},
|
||||
"quotas": IntDict,
|
||||
}
|
||||
TYPE_MAP["flavors"] = TYPE_MAP["flavor"]
|
||||
|
||||
REQUEST_AS_LIST = set(['databases', 'users'])
|
||||
|
||||
|
||||
def element_ancestors_match_list(element, list):
|
||||
"""
|
||||
For element root at <foo><blah><root></blah></foo> matches against
|
||||
list ["blah", "foo"].
|
||||
"""
|
||||
itr_elem = element.getparent()
|
||||
for name in list:
|
||||
if itr_elem is None:
|
||||
break
|
||||
if name != normalize_tag(itr_elem):
|
||||
return False
|
||||
itr_elem = itr_elem.getparent()
|
||||
return True
|
||||
|
||||
|
||||
def element_must_be_list(parent_element, name):
|
||||
"""Determines if an element to be created should be a dict or list."""
|
||||
if name in LISTIFY:
|
||||
list_of_lists = LISTIFY[name]
|
||||
for tag_list in list_of_lists:
|
||||
if element_ancestors_match_list(parent_element, tag_list):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def element_to_json(name, element):
|
||||
if element_must_be_list(element, name):
|
||||
return element_to_list(element)
|
||||
else:
|
||||
return element_to_dict(element)
|
||||
|
||||
|
||||
def root_element_to_json(name, element):
|
||||
"""Returns a tuple of the root JSON value, plus the links if found."""
|
||||
if name == "rootEnabled": # Why oh why were we inconsistent here? :'(
|
||||
if element.text.strip() == "False":
|
||||
return False, None
|
||||
elif element.text.strip() == "True":
|
||||
return True, None
|
||||
if element_must_be_list(element, name):
|
||||
return element_to_list(element, True)
|
||||
else:
|
||||
return element_to_dict(element), None
|
||||
|
||||
|
||||
def element_to_list(element, check_for_links=False):
|
||||
"""
|
||||
For element "foo" in <foos><foo/><foo/></foos>
|
||||
Returns [{}, {}]
|
||||
"""
|
||||
links = None
|
||||
result = []
|
||||
for child_element in element:
|
||||
# The "links" element gets jammed into the root element.
|
||||
if check_for_links and normalize_tag(child_element) == "links":
|
||||
links = element_to_list(child_element)
|
||||
else:
|
||||
result.append(element_to_dict(child_element))
|
||||
if check_for_links:
|
||||
return result, links
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
def element_to_dict(element):
|
||||
result = {}
|
||||
for name, value in element.items():
|
||||
result[name] = value
|
||||
for child_element in element:
|
||||
name = normalize_tag(child_element)
|
||||
result[name] = element_to_json(name, child_element)
|
||||
if len(result) == 0 and element.text:
|
||||
string_value = element.text.strip()
|
||||
if len(string_value):
|
||||
if string_value == 'None':
|
||||
return None
|
||||
return string_value
|
||||
return result
|
||||
|
||||
|
||||
def standardize_json_lists(json_dict):
|
||||
"""
|
||||
In XML, we might see something like {'instances':{'instances':[...]}},
|
||||
which we must change to just {'instances':[...]} to be compatable with
|
||||
the true JSON format.
|
||||
|
||||
If any items are dictionaries with only one item which is a list,
|
||||
simply remove the dictionary and insert its list directly.
|
||||
"""
|
||||
found_items = []
|
||||
for key, value in json_dict.items():
|
||||
value = json_dict[key]
|
||||
if isinstance(value, dict):
|
||||
if len(value) == 1 and isinstance(value.values()[0], list):
|
||||
found_items.append(key)
|
||||
else:
|
||||
standardize_json_lists(value)
|
||||
for key in found_items:
|
||||
json_dict[key] = json_dict[key].values()[0]
|
||||
|
||||
|
||||
def normalize_tag(elem):
|
||||
"""Given an element, returns the tag minus the XMLNS junk.
|
||||
|
||||
IOW, .tag may sometimes return the XML namespace at the start of the
|
||||
string. This gets rids of that.
|
||||
"""
|
||||
try:
|
||||
prefix = "{" + elem.nsmap[None] + "}"
|
||||
if elem.tag.startswith(prefix):
|
||||
return elem.tag[len(prefix):]
|
||||
except KeyError:
|
||||
pass
|
||||
return elem.tag
|
||||
|
||||
|
||||
def create_root_xml_element(name, value):
|
||||
"""Create the first element using a name and a dictionary."""
|
||||
element = etree.Element(name, nsmap=XML_NS)
|
||||
if name in REQUEST_AS_LIST:
|
||||
add_subelements_from_list(element, name, value)
|
||||
else:
|
||||
populate_element_from_dict(element, value)
|
||||
return element
|
||||
|
||||
|
||||
def create_subelement(parent_element, name, value):
|
||||
"""Attaches a new element onto the parent element."""
|
||||
if isinstance(value, dict):
|
||||
create_subelement_from_dict(parent_element, name, value)
|
||||
elif isinstance(value, list):
|
||||
create_subelement_from_list(parent_element, name, value)
|
||||
else:
|
||||
raise TypeError("Can't handle type %s." % type(value))
|
||||
|
||||
|
||||
def create_subelement_from_dict(parent_element, name, dict):
|
||||
element = etree.SubElement(parent_element, name)
|
||||
populate_element_from_dict(element, dict)
|
||||
|
||||
|
||||
def create_subelement_from_list(parent_element, name, list):
|
||||
element = etree.SubElement(parent_element, name)
|
||||
add_subelements_from_list(element, name, list)
|
||||
|
||||
|
||||
def add_subelements_from_list(element, name, list):
|
||||
if name.endswith("s"):
|
||||
item_name = name[:len(name) - 1]
|
||||
else:
|
||||
item_name = name
|
||||
for item in list:
|
||||
create_subelement(element, item_name, item)
|
||||
|
||||
|
||||
def populate_element_from_dict(element, dict):
|
||||
for key, value in dict.items():
|
||||
if isinstance(value, basestring):
|
||||
element.set(key, value)
|
||||
elif isinstance(value, Number):
|
||||
element.set(key, str(value))
|
||||
elif isinstance(value, None.__class__):
|
||||
element.set(key, '')
|
||||
else:
|
||||
create_subelement(element, key, value)
|
||||
|
||||
|
||||
def modify_response_types(value, type_translator):
|
||||
"""
|
||||
This will convert some string in response dictionary to ints or bool
|
||||
so that our respose is compatiable with code expecting JSON style responses
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
if value == 'True':
|
||||
return True
|
||||
elif value == 'False':
|
||||
return False
|
||||
else:
|
||||
return type_translator(value)
|
||||
elif isinstance(value, dict):
|
||||
for k, v in value.iteritems():
|
||||
if type_translator is not IntDict:
|
||||
if v.__class__ is dict and v.__len__() == 0:
|
||||
value[k] = None
|
||||
elif k in type_translator:
|
||||
value[k] = modify_response_types(value[k],
|
||||
type_translator[k])
|
||||
else:
|
||||
value[k] = int(value[k])
|
||||
return value
|
||||
elif isinstance(value, list):
|
||||
return [modify_response_types(element, type_translator)
|
||||
for element in value]
|
||||
|
||||
|
||||
class TroveXmlClient(TroveHTTPClient):
|
||||
|
||||
@classmethod
|
||||
def morph_request(self, kwargs):
|
||||
kwargs['headers']['Accept'] = 'application/xml'
|
||||
kwargs['headers']['Content-Type'] = 'application/xml'
|
||||
if 'body' in kwargs:
|
||||
body = kwargs['body']
|
||||
root_name = body.keys()[0]
|
||||
xml = create_root_xml_element(root_name, body[root_name])
|
||||
xml_string = etree.tostring(xml, pretty_print=True)
|
||||
kwargs['body'] = xml_string
|
||||
|
||||
@classmethod
|
||||
def morph_response_body(self, body_string):
|
||||
# The root XML element always becomes a dictionary with a single
|
||||
# field, which has the same key as the elements name.
|
||||
result = {}
|
||||
try:
|
||||
root_element = etree.XML(body_string)
|
||||
except etree.XMLSyntaxError:
|
||||
raise exceptions.ResponseFormatError()
|
||||
root_name = normalize_tag(root_element)
|
||||
root_value, links = root_element_to_json(root_name, root_element)
|
||||
result = {root_name: root_value}
|
||||
if links:
|
||||
result['links'] = links
|
||||
modify_response_types(result, TYPE_MAP)
|
||||
return result
|
@ -157,7 +157,7 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
def from_response(response, body):
|
||||
"""
|
||||
Return an instance of an ClientException or subclass
|
||||
based on an httplib2 response.
|
||||
based on an request's response.
|
||||
|
||||
Usage::
|
||||
|
||||
|
0
troveclient/openstack/__init__.py
Normal file
0
troveclient/openstack/__init__.py
Normal file
0
troveclient/openstack/common/__init__.py
Normal file
0
troveclient/openstack/common/__init__.py
Normal file
16
troveclient/openstack/common/apiclient/__init__.py
Normal file
16
troveclient/openstack/common/apiclient/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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.
|
227
troveclient/openstack/common/apiclient/auth.py
Normal file
227
troveclient/openstack/common/apiclient/auth.py
Normal file
@ -0,0 +1,227 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 Spanish National Research Council.
|
||||
# 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.
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
import abc
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_discovered_plugins = {}
|
||||
|
||||
|
||||
def discover_auth_systems():
|
||||
"""Discover the available auth-systems.
|
||||
|
||||
This won't take into account the old style auth-systems.
|
||||
"""
|
||||
global _discovered_plugins
|
||||
_discovered_plugins = {}
|
||||
|
||||
def add_plugin(ext):
|
||||
_discovered_plugins[ext.name] = ext.plugin
|
||||
|
||||
ep_namespace = "troveclient.openstack.common.apiclient.auth"
|
||||
mgr = extension.ExtensionManager(ep_namespace)
|
||||
mgr.map(add_plugin)
|
||||
|
||||
|
||||
def load_auth_system_opts(parser):
|
||||
"""Load options needed by the available auth-systems into a parser.
|
||||
|
||||
This function will try to populate the parser with options from the
|
||||
available plugins.
|
||||
"""
|
||||
group = parser.add_argument_group("Common auth options")
|
||||
BaseAuthPlugin.add_common_opts(group)
|
||||
for name, auth_plugin in _discovered_plugins.iteritems():
|
||||
group = parser.add_argument_group(
|
||||
"Auth-system '%s' options" % name,
|
||||
conflict_handler="resolve")
|
||||
auth_plugin.add_opts(group)
|
||||
|
||||
|
||||
def load_plugin(auth_system):
|
||||
try:
|
||||
plugin_class = _discovered_plugins[auth_system]
|
||||
except KeyError:
|
||||
raise exceptions.AuthSystemNotFound(auth_system)
|
||||
return plugin_class(auth_system=auth_system)
|
||||
|
||||
|
||||
def load_plugin_from_args(args):
|
||||
"""Load requred plugin and populate it with options.
|
||||
|
||||
Try to guess auth system if it is not specified. Systems are tried in
|
||||
alphabetical order.
|
||||
|
||||
:type args: argparse.Namespace
|
||||
:raises: AuthorizationFailure
|
||||
"""
|
||||
auth_system = args.os_auth_system
|
||||
if auth_system:
|
||||
plugin = load_plugin(auth_system)
|
||||
plugin.parse_opts(args)
|
||||
plugin.sufficient_options()
|
||||
return plugin
|
||||
|
||||
for plugin_auth_system in sorted(_discovered_plugins.iterkeys()):
|
||||
plugin_class = _discovered_plugins[plugin_auth_system]
|
||||
plugin = plugin_class()
|
||||
plugin.parse_opts(args)
|
||||
try:
|
||||
plugin.sufficient_options()
|
||||
except exceptions.AuthPluginOptionsMissing:
|
||||
continue
|
||||
return plugin
|
||||
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
|
||||
|
||||
|
||||
class BaseAuthPlugin(object):
|
||||
"""Base class for authentication plugins.
|
||||
|
||||
An authentication plugin needs to override at least the authenticate
|
||||
method to be a valid plugin.
|
||||
"""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
auth_system = None
|
||||
opt_names = []
|
||||
common_opt_names = [
|
||||
"auth_system",
|
||||
"username",
|
||||
"password",
|
||||
"tenant_name",
|
||||
"token",
|
||||
"auth_url",
|
||||
]
|
||||
|
||||
def __init__(self, auth_system=None, **kwargs):
|
||||
self.auth_system = auth_system or self.auth_system
|
||||
self.opts = dict((name, kwargs.get(name))
|
||||
for name in self.opt_names)
|
||||
|
||||
@staticmethod
|
||||
def _parser_add_opt(parser, opt):
|
||||
"""Add an option to parser in two variants.
|
||||
|
||||
:param opt: option name (with underscores)
|
||||
"""
|
||||
dashed_opt = opt.replace("_", "-")
|
||||
env_var = "OS_%s" % opt.upper()
|
||||
arg_default = os.environ.get(env_var, "")
|
||||
arg_help = "Defaults to env[%s]." % env_var
|
||||
parser.add_argument(
|
||||
"--os-%s" % dashed_opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
default=arg_default,
|
||||
help=arg_help)
|
||||
parser.add_argument(
|
||||
"--os_%s" % opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
@classmethod
|
||||
def add_opts(cls, parser):
|
||||
"""Populate the parser with the options for this plugin.
|
||||
"""
|
||||
for opt in cls.opt_names:
|
||||
# use `BaseAuthPlugin.common_opt_names` since it is never
|
||||
# changed in child classes
|
||||
if opt not in BaseAuthPlugin.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@classmethod
|
||||
def add_common_opts(cls, parser):
|
||||
"""Add options that are common for several plugins.
|
||||
"""
|
||||
for opt in cls.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@staticmethod
|
||||
def get_opt(opt_name, args):
|
||||
"""Return option name and value.
|
||||
|
||||
:param opt_name: name of the option, e.g., "username"
|
||||
:param args: parsed arguments
|
||||
"""
|
||||
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
||||
|
||||
def parse_opts(self, args):
|
||||
"""Parse the actual auth-system options if any.
|
||||
|
||||
This method is expected to populate the attribute `self.opts` with a
|
||||
dict containing the options and values needed to make authentication.
|
||||
"""
|
||||
self.opts.update(dict(self.get_opt(opt_name, args)
|
||||
for opt_name in self.opt_names))
|
||||
|
||||
def authenticate(self, http_client):
|
||||
"""Authenticate using plugin defined method.
|
||||
|
||||
The method usually analyses `self.opts` and performs
|
||||
a request to authentication server.
|
||||
|
||||
:param http_client: client object that needs authentication
|
||||
:type http_client: HTTPClient
|
||||
:raises: AuthorizationFailure
|
||||
"""
|
||||
self.sufficient_options()
|
||||
self._do_authenticate(http_client)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _do_authenticate(self, http_client):
|
||||
"""Protected method for authentication.
|
||||
"""
|
||||
|
||||
def sufficient_options(self):
|
||||
"""Check if all required options are present.
|
||||
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
missing = [opt
|
||||
for opt in self.opt_names
|
||||
if not self.opts.get(opt)]
|
||||
if missing:
|
||||
raise exceptions.AuthPluginOptionsMissing(missing)
|
||||
|
||||
@abc.abstractmethod
|
||||
def token_and_endpoint(self, endpoint_type, service_type):
|
||||
"""Return token and endpoint.
|
||||
|
||||
:param service_type: Service type of the endpoint
|
||||
:type service_type: string
|
||||
:param endpoint_type: Type of endpoint.
|
||||
Possible values: public or publicURL,
|
||||
internal or internalURL,
|
||||
admin or adminURL
|
||||
:type endpoint_type: string
|
||||
:returns: tuple of token and endpoint strings
|
||||
:raises: EndpointException
|
||||
"""
|
492
troveclient/openstack/common/apiclient/base.py
Normal file
492
troveclient/openstack/common/apiclient/base.py
Normal file
@ -0,0 +1,492 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2012 Grid Dynamics
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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.
|
||||
"""
|
||||
|
||||
# E1102: %s is not callable
|
||||
# pylint: disable=E1102
|
||||
|
||||
import abc
|
||||
import urllib
|
||||
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
from troveclient.openstack.common import strutils
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Return id if argument is a Resource.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
||||
class HookableMixin(object):
|
||||
"""Mixin so classes can register and run hooks."""
|
||||
_hooks_map = {}
|
||||
|
||||
@classmethod
|
||||
def add_hook(cls, hook_type, hook_func):
|
||||
"""Add a new hook of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param hook_func: hook function
|
||||
"""
|
||||
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):
|
||||
"""Run all hooks of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param **args: args to be passed to every hook function
|
||||
:param **kwargs: kwargs to be passed to every hook function
|
||||
"""
|
||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||
for hook_func in hook_funcs:
|
||||
hook_func(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseManager(HookableMixin):
|
||||
"""Basic manager type providing common operations.
|
||||
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initializes BaseManager with `client`.
|
||||
|
||||
:param client: instance of BaseClient descendant for HTTP requests
|
||||
"""
|
||||
super(BaseManager, self).__init__()
|
||||
self.client = client
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, json=None):
|
||||
"""List the collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
:param obj_class: class for constructing the returned objects
|
||||
(self.resource_class will be used by default)
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
"""
|
||||
if json:
|
||||
body = self.client.post(url, json=json).json()
|
||||
else:
|
||||
body = self.client.get(url).json()
|
||||
|
||||
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...
|
||||
try:
|
||||
data = data['values']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||
|
||||
def _get(self, url, response_key):
|
||||
"""Get an object from collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'
|
||||
"""
|
||||
body = self.client.get(url).json()
|
||||
return self.resource_class(self, body[response_key], loaded=True)
|
||||
|
||||
def _head(self, url):
|
||||
"""Retrieve request headers for an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
"""
|
||||
resp = self.client.head(url)
|
||||
return resp.status_code == 204
|
||||
|
||||
def _post(self, url, json, response_key, return_raw=False):
|
||||
"""Create an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
:param return_raw: flag to force returning raw JSON instead of
|
||||
Python object of self.resource_class
|
||||
"""
|
||||
body = self.client.post(url, json=json).json()
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _put(self, url, json=None, response_key=None):
|
||||
"""Update an object with PUT method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
"""
|
||||
resp = self.client.put(url, json=json)
|
||||
# PUT requests may not return a body
|
||||
if resp.content:
|
||||
body = resp.json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _patch(self, url, json=None, response_key=None):
|
||||
"""Update an object with PATCH method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
"""
|
||||
body = self.client.patch(url, json=json).json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _delete(self, url):
|
||||
"""Delete an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers/my-server'
|
||||
"""
|
||||
return self.client.delete(url)
|
||||
|
||||
|
||||
class ManagerWithFind(BaseManager):
|
||||
"""Manager with additional `find()`/`findall()` methods."""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
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(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
|
||||
|
||||
|
||||
class CrudManager(BaseManager):
|
||||
"""Base manager class for manipulating entities.
|
||||
|
||||
Children of this class are expected to define a `collection_key` and `key`.
|
||||
|
||||
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
||||
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
||||
objects containing a list of member resources (e.g. `{'entities': [{},
|
||||
{}, {}]}`).
|
||||
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
||||
refer to an individual member of the collection.
|
||||
|
||||
"""
|
||||
collection_key = None
|
||||
key = None
|
||||
|
||||
def build_url(self, base_url=None, **kwargs):
|
||||
"""Builds a resource URL for the given kwargs.
|
||||
|
||||
Given an example collection where `collection_key = 'entities'` and
|
||||
`key = 'entity'`, the following URL's could be generated.
|
||||
|
||||
By default, the URL will represent a collection of entities, e.g.::
|
||||
|
||||
/entities
|
||||
|
||||
If kwargs contains an `entity_id`, then the URL will represent a
|
||||
specific member, e.g.::
|
||||
|
||||
/entities/{entity_id}
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
url = base_url if base_url is not None else ''
|
||||
|
||||
url += '/%s' % self.collection_key
|
||||
|
||||
# do we have a specific entity?
|
||||
entity_id = kwargs.get('%s_id' % self.key)
|
||||
if entity_id is not None:
|
||||
url += '/%s' % entity_id
|
||||
|
||||
return url
|
||||
|
||||
def _filter_kwargs(self, kwargs):
|
||||
"""Drop null values and handle ids."""
|
||||
for key, ref in kwargs.copy().iteritems():
|
||||
if ref is None:
|
||||
kwargs.pop(key)
|
||||
else:
|
||||
if isinstance(ref, Resource):
|
||||
kwargs.pop(key)
|
||||
kwargs['%s_id' % key] = getid(ref)
|
||||
return kwargs
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._post(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: kwargs},
|
||||
self.key)
|
||||
|
||||
def get(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._get(
|
||||
self.build_url(**kwargs),
|
||||
self.key)
|
||||
|
||||
def head(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._head(self.build_url(**kwargs))
|
||||
|
||||
def list(self, base_url=None, **kwargs):
|
||||
"""List the collection.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
|
||||
def put(self, base_url=None, **kwargs):
|
||||
"""Update an element.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._put(self.build_url(base_url=base_url, **kwargs))
|
||||
|
||||
def update(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
params = kwargs.copy()
|
||||
params.pop('%s_id' % self.key)
|
||||
|
||||
return self._patch(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: params},
|
||||
self.key)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._delete(
|
||||
self.build_url(**kwargs))
|
||||
|
||||
def find(self, base_url=None, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
rl = self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
num = len(rl)
|
||||
|
||||
if num == 0:
|
||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||
raise exceptions.NotFound(404, msg)
|
||||
elif num > 1:
|
||||
raise exceptions.NoUniqueMatch
|
||||
else:
|
||||
return rl[0]
|
||||
|
||||
|
||||
class Extension(HookableMixin):
|
||||
"""Extension descriptor."""
|
||||
|
||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
||||
manager_class = None
|
||||
|
||||
def __init__(self, name, module):
|
||||
super(Extension, self).__init__()
|
||||
self.name = name
|
||||
self.module = module
|
||||
self._parse_extension_module()
|
||||
|
||||
def _parse_extension_module(self):
|
||||
self.manager_class = None
|
||||
for attr_name, attr_value in self.module.__dict__.items():
|
||||
if attr_name in self.SUPPORTED_HOOKS:
|
||||
self.add_hook(attr_name, attr_value)
|
||||
else:
|
||||
try:
|
||||
if issubclass(attr_value, BaseManager):
|
||||
self.manager_class = attr_value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension '%s'>" % self.name
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
HUMAN_ID = False
|
||||
NAME_ATTR = 'name'
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
"""Populate and bind to a manager.
|
||||
|
||||
:param manager: BaseManager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion.
|
||||
"""
|
||||
if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
|
||||
return strutils.to_slug(getattr(self, self.NAME_ATTR))
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
self._info[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 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, Resource):
|
||||
return NotImplemented
|
||||
# two resources of different types are not equal
|
||||
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
|
360
troveclient/openstack/common/apiclient/client.py
Normal file
360
troveclient/openstack/common/apiclient/client.py
Normal file
@ -0,0 +1,360 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 Grid Dynamics
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
from troveclient.openstack.common import importutils
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
"""This client handles sending HTTP requests to OpenStack servers.
|
||||
|
||||
Features:
|
||||
- share authentication information between several clients to different
|
||||
services (e.g., for compute and image clients);
|
||||
- reissue authentication request for expired tokens;
|
||||
- encode/decode JSON bodies;
|
||||
- raise exeptions on HTTP errors;
|
||||
- pluggable authentication;
|
||||
- store authentication information in a keyring;
|
||||
- store time spent for requests;
|
||||
- register clients for particular services, so one can use
|
||||
`http_client.identity` or `http_client.compute`;
|
||||
- log requests and responses in a format that is easy to copy-and-paste
|
||||
into terminal and send the same request with curl.
|
||||
"""
|
||||
|
||||
user_agent = "troveclient.openstack.common.apiclient"
|
||||
|
||||
def __init__(self,
|
||||
auth_plugin,
|
||||
region_name=None,
|
||||
endpoint_type="publicURL",
|
||||
original_ip=None,
|
||||
verify=True,
|
||||
cert=None,
|
||||
timeout=None,
|
||||
timings=False,
|
||||
keyring_saver=None,
|
||||
debug=False,
|
||||
user_agent=None,
|
||||
http=None):
|
||||
self.auth_plugin = auth_plugin
|
||||
|
||||
self.endpoint_type = endpoint_type
|
||||
self.region_name = region_name
|
||||
|
||||
self.original_ip = original_ip
|
||||
self.timeout = timeout
|
||||
self.verify = verify
|
||||
self.cert = cert
|
||||
|
||||
self.keyring_saver = keyring_saver
|
||||
self.debug = debug
|
||||
self.user_agent = user_agent or self.user_agent
|
||||
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
self.timings = timings
|
||||
|
||||
# requests within the same session can reuse TCP connections from pool
|
||||
self.http = http or requests.Session()
|
||||
|
||||
self.cached_token = None
|
||||
|
||||
def _http_log_req(self, method, url, kwargs):
|
||||
if not self.debug:
|
||||
return
|
||||
|
||||
string_parts = [
|
||||
"curl -i",
|
||||
"-X '%s'" % method,
|
||||
"'%s'" % url,
|
||||
]
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
_logger.debug("REQ: %s" % " ".join(string_parts))
|
||||
if 'data' in kwargs:
|
||||
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
|
||||
|
||||
def _http_log_resp(self, resp):
|
||||
if not self.debug:
|
||||
return
|
||||
_logger.debug(
|
||||
"RESP: [%s] %s\n",
|
||||
resp.status_code,
|
||||
resp.headers)
|
||||
if resp._content_consumed:
|
||||
_logger.debug(
|
||||
"RESP BODY: %s\n",
|
||||
resp.text)
|
||||
|
||||
def serialize(self, kwargs):
|
||||
if kwargs.get('json') is not None:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = json.dumps(kwargs['json'])
|
||||
try:
|
||||
del kwargs['json']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def reset_timings(self):
|
||||
self.times = []
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around `requests.Session.request` to handle tasks such as
|
||||
setting headers, JSON encoding/decoding, and error handling.
|
||||
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
' requests.Session.request (such as `headers`) or `json`
|
||||
that will be encoded as JSON and used as `data` argument
|
||||
"""
|
||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
||||
kwargs["headers"]["User-Agent"] = self.user_agent
|
||||
if self.original_ip:
|
||||
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
||||
self.original_ip, self.user_agent)
|
||||
if self.timeout is not None:
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
kwargs.setdefault("verify", self.verify)
|
||||
if self.cert is not None:
|
||||
kwargs.setdefault("cert", self.cert)
|
||||
self.serialize(kwargs)
|
||||
|
||||
self._http_log_req(method, url, kwargs)
|
||||
if self.timings:
|
||||
start_time = time.time()
|
||||
resp = self.http.request(method, url, **kwargs)
|
||||
if self.timings:
|
||||
self.times.append(("%s %s" % (method, url),
|
||||
start_time, time.time()))
|
||||
self._http_log_resp(resp)
|
||||
|
||||
if resp.status_code >= 400:
|
||||
_logger.debug(
|
||||
"Request returned failure status: %s",
|
||||
resp.status_code)
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def concat_url(endpoint, url):
|
||||
"""Concatenate endpoint and final URL.
|
||||
|
||||
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
||||
"http://keystone/v2.0/tokens".
|
||||
|
||||
:param endpoint: the base URL
|
||||
:param url: the final URL
|
||||
"""
|
||||
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
||||
|
||||
def client_request(self, client, method, url, **kwargs):
|
||||
"""Send an http request using `client`'s endpoint and specified `url`.
|
||||
|
||||
If request was rejected as unauthorized (possibly because the token is
|
||||
expired), issue one authorization attempt and send the request once
|
||||
again.
|
||||
|
||||
:param client: instance of BaseClient descendant
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
' `HTTPClient.request`
|
||||
"""
|
||||
|
||||
filter_args = {
|
||||
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
||||
"service_type": client.service_type,
|
||||
}
|
||||
token, endpoint = (self.cached_token, client.cached_endpoint)
|
||||
just_authenticated = False
|
||||
if not (token and endpoint):
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
pass
|
||||
if not (token and endpoint):
|
||||
self.authenticate()
|
||||
just_authenticated = True
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
if not (token and endpoint):
|
||||
raise exceptions.AuthorizationFailure(
|
||||
"Cannot find endpoint or token for request")
|
||||
|
||||
old_token_endpoint = (token, endpoint)
|
||||
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
# Perform the request once. If we get Unauthorized, then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
except exceptions.Unauthorized as unauth_ex:
|
||||
if just_authenticated:
|
||||
raise
|
||||
self.cached_token = None
|
||||
client.cached_endpoint = None
|
||||
self.authenticate()
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
raise unauth_ex
|
||||
if (not (token and endpoint) or
|
||||
old_token_endpoint == (token, endpoint)):
|
||||
raise unauth_ex
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
kwargs["headers"]["X-Auth-Token"] = token
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
|
||||
def add_client(self, base_client_instance):
|
||||
"""Add a new instance of :class:`BaseClient` descendant.
|
||||
|
||||
`self` will store a reference to `base_client_instance`.
|
||||
|
||||
Example:
|
||||
|
||||
>>> def test_clients():
|
||||
... from keystoneclient.auth import keystone
|
||||
... from openstack.common.apiclient import client
|
||||
... auth = keystone.KeystoneAuthPlugin(
|
||||
... username="user", password="pass", tenant_name="tenant",
|
||||
... auth_url="http://auth:5000/v2.0")
|
||||
... openstack_client = client.HTTPClient(auth)
|
||||
... # create nova client
|
||||
... from novaclient.v1_1 import client
|
||||
... client.Client(openstack_client)
|
||||
... # create keystone client
|
||||
... from keystoneclient.v2_0 import client
|
||||
... client.Client(openstack_client)
|
||||
... # use them
|
||||
... openstack_client.identity.tenants.list()
|
||||
... openstack_client.compute.servers.list()
|
||||
"""
|
||||
service_type = base_client_instance.service_type
|
||||
if service_type and not hasattr(self, service_type):
|
||||
setattr(self, service_type, base_client_instance)
|
||||
|
||||
def authenticate(self):
|
||||
self.auth_plugin.authenticate(self)
|
||||
# Store the authentication results in the keyring for later requests
|
||||
if self.keyring_saver:
|
||||
self.keyring_saver.save(self)
|
||||
|
||||
|
||||
class BaseClient(object):
|
||||
"""Top-level object to access the OpenStack API.
|
||||
|
||||
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
||||
will handle a bunch of issues such as authentication.
|
||||
"""
|
||||
|
||||
service_type = None
|
||||
endpoint_type = None # "publicURL" will be used
|
||||
cached_endpoint = None
|
||||
|
||||
def __init__(self, http_client, extensions=None):
|
||||
self.http_client = http_client
|
||||
http_client.add_client(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
for extension in extensions:
|
||||
if extension.manager_class:
|
||||
setattr(self, extension.name,
|
||||
extension.manager_class(self))
|
||||
|
||||
def client_request(self, method, url, **kwargs):
|
||||
return self.http_client.client_request(
|
||||
self, method, url, **kwargs)
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.client_request("HEAD", url, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self.client_request("GET", url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.client_request("POST", url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.client_request("PUT", url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.client_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.client_request("PATCH", url, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_class(api_name, version, version_map):
|
||||
"""Returns the client class for the requested API version
|
||||
|
||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
||||
:param version: the requested API version
|
||||
:param version_map: a dict of client classes keyed by version
|
||||
:rtype: a client class for the requested API version
|
||||
"""
|
||||
try:
|
||||
client_path = version_map[str(version)]
|
||||
except (KeyError, ValueError):
|
||||
msg = "Invalid %s client version '%s'. must be one of: %s" % (
|
||||
(api_name, version, ', '.join(version_map.keys())))
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
|
||||
return importutils.import_class(client_path)
|
446
troveclient/openstack/common/apiclient/exceptions.py
Normal file
446
troveclient/openstack/common/apiclient/exceptions.py
Normal file
@ -0,0 +1,446 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingArgs(ClientException):
|
||||
"""Supplied arguments are not sufficient for calling a function."""
|
||||
def __init__(self, missing):
|
||||
self.missing = missing
|
||||
msg = "Missing argument(s): %s" % ", ".join(missing)
|
||||
super(MissingArgs, self).__init__(msg)
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedVersion(ClientException):
|
||||
"""User is trying to use an unsupported version of the API."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(ClientException):
|
||||
"""Error in CLI tool."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(ClientException):
|
||||
"""Cannot authorize API client."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||
"""Auth plugin misses some options."""
|
||||
def __init__(self, opt_names):
|
||||
super(AuthPluginOptionsMissing, self).__init__(
|
||||
"Authentication failed. Missing options: %s" %
|
||||
", ".join(opt_names))
|
||||
self.opt_names = opt_names
|
||||
|
||||
|
||||
class AuthSystemNotFound(AuthorizationFailure):
|
||||
"""User has specified a AuthSystem that is not installed."""
|
||||
def __init__(self, auth_system):
|
||||
super(AuthSystemNotFound, self).__init__(
|
||||
"AuthSystemNotFound: %s" % repr(auth_system))
|
||||
self.auth_system = auth_system
|
||||
|
||||
|
||||
class NoUniqueMatch(ClientException):
|
||||
"""Multiple entities found instead of one."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointException(ClientException):
|
||||
"""Something is rotten in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(EndpointException):
|
||||
"""Could not find requested endpoint in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(EndpointException):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
super(AmbiguousEndpoints, self).__init__(
|
||||
"AmbiguousEndpoints: %s" % repr(endpoints))
|
||||
self.endpoints = endpoints
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class for all HTTP exceptions.
|
||||
"""
|
||||
http_status = 0
|
||||
message = "HTTP Error"
|
||||
|
||||
def __init__(self, message=None, details=None,
|
||||
response=None, request_id=None,
|
||||
url=None, method=None, http_status=None):
|
||||
self.http_status = http_status or self.http_status
|
||||
self.message = message or self.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.response = response
|
||||
self.url = url
|
||||
self.method = method
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||
if request_id:
|
||||
formatted_string += " (Request-ID: %s)" % request_id
|
||||
super(HttpError, self).__init__(formatted_string)
|
||||
|
||||
|
||||
class HTTPClientError(HttpError):
|
||||
"""Client-side HTTP error.
|
||||
|
||||
Exception for cases in which the client seems to have erred.
|
||||
"""
|
||||
message = "HTTP Client Error"
|
||||
|
||||
|
||||
class HttpServerError(HttpError):
|
||||
"""Server-side HTTP error.
|
||||
|
||||
Exception for cases in which the server is aware that it has
|
||||
erred or is incapable of performing the request.
|
||||
"""
|
||||
message = "HTTP Server Error"
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
http_status = 400
|
||||
message = "Bad Request"
|
||||
|
||||
|
||||
class Unauthorized(HTTPClientError):
|
||||
"""HTTP 401 - Unauthorized.
|
||||
|
||||
Similar to 403 Forbidden, but specifically for use when authentication
|
||||
is required and has failed or has not yet been provided.
|
||||
"""
|
||||
http_status = 401
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
http_status = 402
|
||||
message = "Payment Required"
|
||||
|
||||
|
||||
class Forbidden(HTTPClientError):
|
||||
"""HTTP 403 - Forbidden.
|
||||
|
||||
The request was a valid request, but the server is refusing to respond
|
||||
to it.
|
||||
"""
|
||||
http_status = 403
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(HTTPClientError):
|
||||
"""HTTP 404 - Not Found.
|
||||
|
||||
The requested resource could not be found but may be available again
|
||||
in the future.
|
||||
"""
|
||||
http_status = 404
|
||||
message = "Not Found"
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPClientError):
|
||||
"""HTTP 405 - Method Not Allowed.
|
||||
|
||||
A request was made of a resource using a request method not supported
|
||||
by that resource.
|
||||
"""
|
||||
http_status = 405
|
||||
message = "Method Not Allowed"
|
||||
|
||||
|
||||
class NotAcceptable(HTTPClientError):
|
||||
"""HTTP 406 - Not Acceptable.
|
||||
|
||||
The requested resource is only capable of generating content not
|
||||
acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
http_status = 406
|
||||
message = "Not Acceptable"
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
http_status = 407
|
||||
message = "Proxy Authentication Required"
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
http_status = 408
|
||||
message = "Request Timeout"
|
||||
|
||||
|
||||
class Conflict(HTTPClientError):
|
||||
"""HTTP 409 - Conflict.
|
||||
|
||||
Indicates that the request could not be processed because of conflict
|
||||
in the request, such as an edit conflict.
|
||||
"""
|
||||
http_status = 409
|
||||
message = "Conflict"
|
||||
|
||||
|
||||
class Gone(HTTPClientError):
|
||||
"""HTTP 410 - Gone.
|
||||
|
||||
Indicates that the resource requested is no longer available and will
|
||||
not be available again.
|
||||
"""
|
||||
http_status = 410
|
||||
message = "Gone"
|
||||
|
||||
|
||||
class LengthRequired(HTTPClientError):
|
||||
"""HTTP 411 - Length Required.
|
||||
|
||||
The request did not specify the length of its content, which is
|
||||
required by the requested resource.
|
||||
"""
|
||||
http_status = 411
|
||||
message = "Length Required"
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPClientError):
|
||||
"""HTTP 412 - Precondition Failed.
|
||||
|
||||
The server does not meet one of the preconditions that the requester
|
||||
put on the request.
|
||||
"""
|
||||
http_status = 412
|
||||
message = "Precondition Failed"
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPClientError):
|
||||
"""HTTP 413 - Request Entity Too Large.
|
||||
|
||||
The request is larger than the server is willing or able to process.
|
||||
"""
|
||||
http_status = 413
|
||||
message = "Request Entity Too Large"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestUriTooLong(HTTPClientError):
|
||||
"""HTTP 414 - Request-URI Too Long.
|
||||
|
||||
The URI provided was too long for the server to process.
|
||||
"""
|
||||
http_status = 414
|
||||
message = "Request-URI Too Long"
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPClientError):
|
||||
"""HTTP 415 - Unsupported Media Type.
|
||||
|
||||
The request entity has a media type which the server or resource does
|
||||
not support.
|
||||
"""
|
||||
http_status = 415
|
||||
message = "Unsupported Media Type"
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||
|
||||
The client has asked for a portion of the file, but the server cannot
|
||||
supply that portion.
|
||||
"""
|
||||
http_status = 416
|
||||
message = "Requested Range Not Satisfiable"
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPClientError):
|
||||
"""HTTP 417 - Expectation Failed.
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header field.
|
||||
"""
|
||||
http_status = 417
|
||||
message = "Expectation Failed"
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPClientError):
|
||||
"""HTTP 422 - Unprocessable Entity.
|
||||
|
||||
The request was well-formed but was unable to be followed due to semantic
|
||||
errors.
|
||||
"""
|
||||
http_status = 422
|
||||
message = "Unprocessable Entity"
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
http_status = 500
|
||||
message = "Internal Server Error"
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HttpNotImplemented(HttpServerError):
|
||||
"""HTTP 501 - Not Implemented.
|
||||
|
||||
The server either does not recognize the request method, or it lacks
|
||||
the ability to fulfill the request.
|
||||
"""
|
||||
http_status = 501
|
||||
message = "Not Implemented"
|
||||
|
||||
|
||||
class BadGateway(HttpServerError):
|
||||
"""HTTP 502 - Bad Gateway.
|
||||
|
||||
The server was acting as a gateway or proxy and received an invalid
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 502
|
||||
message = "Bad Gateway"
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
http_status = 503
|
||||
message = "Service Unavailable"
|
||||
|
||||
|
||||
class GatewayTimeout(HttpServerError):
|
||||
"""HTTP 504 - Gateway Timeout.
|
||||
|
||||
The server was acting as a gateway or proxy and did not receive a timely
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 504
|
||||
message = "Gateway Timeout"
|
||||
|
||||
|
||||
class HttpVersionNotSupported(HttpServerError):
|
||||
"""HTTP 505 - HttpVersion Not Supported.
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
http_status = 505
|
||||
message = "HTTP Version Not Supported"
|
||||
|
||||
|
||||
# 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 HttpError.__subclasses__())
|
||||
_code_map = {}
|
||||
for obj in sys.modules[__name__].__dict__.values():
|
||||
if isinstance(obj, type):
|
||||
try:
|
||||
http_status = obj.http_status
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if http_status:
|
||||
_code_map[http_status] = obj
|
||||
|
||||
|
||||
def from_response(response, method, url):
|
||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||
|
||||
:param response: instance of `requests.Response` class
|
||||
:param method: HTTP method used for request
|
||||
:param url: URL used for request
|
||||
"""
|
||||
kwargs = {
|
||||
"http_status": response.status_code,
|
||||
"response": response,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"request_id": response.headers.get("x-compute-request-id"),
|
||||
}
|
||||
if "retry-after" in response.headers:
|
||||
kwargs["retry_after"] = response.headers["retry-after"]
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if hasattr(body, "keys"):
|
||||
error = body[body.keys()[0]]
|
||||
kwargs["message"] = error.get("message", None)
|
||||
kwargs["details"] = error.get("details", None)
|
||||
elif content_type.startswith("text/"):
|
||||
kwargs["details"] = response.text
|
||||
|
||||
try:
|
||||
cls = _code_map[response.status_code]
|
||||
except KeyError:
|
||||
if 500 <= response.status_code < 600:
|
||||
cls = HttpServerError
|
||||
elif 400 <= response.status_code < 500:
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
return cls(**kwargs)
|
172
troveclient/openstack/common/apiclient/fake_client.py
Normal file
172
troveclient/openstack/common/apiclient/fake_client.py
Normal file
@ -0,0 +1,172 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
"""
|
||||
A fake server that "responds" to API methods with pre-canned responses.
|
||||
|
||||
All of these responses come from the spec, so if for some reason the spec's
|
||||
wrong the tests might raise AssertionError. I've indicated in comments the
|
||||
places where actual behavior differs from the spec.
|
||||
"""
|
||||
|
||||
# W0102: Dangerous default value %s as argument
|
||||
# pylint: disable=W0102
|
||||
|
||||
import json
|
||||
import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from troveclient.openstack.common.apiclient import client
|
||||
|
||||
|
||||
def assert_has_keys(dct, required=[], optional=[]):
|
||||
for k in required:
|
||||
try:
|
||||
assert k in dct
|
||||
except AssertionError:
|
||||
extra_keys = set(dct.keys()).difference(set(required + optional))
|
||||
raise AssertionError("found unexpected keys: %s" %
|
||||
list(extra_keys))
|
||||
|
||||
|
||||
class TestResponse(requests.Response):
|
||||
"""Wrap requests.Response and provide a convenient initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
super(TestResponse, self).__init__()
|
||||
self._content_consumed = True
|
||||
if isinstance(data, dict):
|
||||
self.status_code = data.get('status_code', 200)
|
||||
# Fake the text attribute to streamline Response creation
|
||||
text = data.get('text', "")
|
||||
if isinstance(text, (dict, list)):
|
||||
self._content = json.dumps(text)
|
||||
default_headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
else:
|
||||
self._content = text
|
||||
default_headers = {}
|
||||
self.headers = data.get('headers') or default_headers
|
||||
else:
|
||||
self.status_code = data
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.status_code == other.status_code and
|
||||
self.headers == other.headers and
|
||||
self._content == other._content)
|
||||
|
||||
|
||||
class FakeHTTPClient(client.HTTPClient):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.callstack = []
|
||||
self.fixtures = kwargs.pop("fixtures", None) or {}
|
||||
if not args and not "auth_plugin" in kwargs:
|
||||
args = (None, )
|
||||
super(FakeHTTPClient, self).__init__(*args, **kwargs)
|
||||
|
||||
def assert_called(self, method, url, body=None, pos=-1):
|
||||
"""Assert than an API method was just called.
|
||||
"""
|
||||
expected = (method, url)
|
||||
called = self.callstack[pos][0:2]
|
||||
assert self.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % \
|
||||
(expected + called)
|
||||
|
||||
if body is not None:
|
||||
if self.callstack[pos][3] != body:
|
||||
raise AssertionError('%r != %r' %
|
||||
(self.callstack[pos][3], body))
|
||||
|
||||
def assert_called_anytime(self, method, url, body=None):
|
||||
"""Assert than an API method was called anytime in the test.
|
||||
"""
|
||||
expected = (method, url)
|
||||
|
||||
assert self.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
found = False
|
||||
entry = None
|
||||
for entry in self.callstack:
|
||||
if expected == entry[0:2]:
|
||||
found = True
|
||||
break
|
||||
|
||||
assert found, 'Expected %s %s; got %s' % \
|
||||
(method, url, self.callstack)
|
||||
if body is not None:
|
||||
assert entry[3] == body, "%s != %s" % (entry[3], body)
|
||||
|
||||
self.callstack = []
|
||||
|
||||
def clear_callstack(self):
|
||||
self.callstack = []
|
||||
|
||||
def authenticate(self):
|
||||
pass
|
||||
|
||||
def client_request(self, client, method, url, **kwargs):
|
||||
# Check that certain things are called correctly
|
||||
if method in ["GET", "DELETE"]:
|
||||
assert "json" not in kwargs
|
||||
|
||||
# Note the call
|
||||
self.callstack.append(
|
||||
(method,
|
||||
url,
|
||||
kwargs.get("headers") or {},
|
||||
kwargs.get("json") or kwargs.get("data")))
|
||||
try:
|
||||
fixture = self.fixtures[url][method]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
return TestResponse({"headers": fixture[0],
|
||||
"text": fixture[1]})
|
||||
|
||||
# Call the method
|
||||
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
|
||||
kwargs.update(args)
|
||||
munged_url = url.rsplit('?', 1)[0]
|
||||
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
||||
munged_url = munged_url.replace('-', '_')
|
||||
|
||||
callback = "%s_%s" % (method.lower(), munged_url)
|
||||
|
||||
if not hasattr(self, callback):
|
||||
raise AssertionError('Called unknown API method: %s %s, '
|
||||
'expected fakes method name: %s' %
|
||||
(method, url, callback))
|
||||
|
||||
resp = getattr(self, callback)(**kwargs)
|
||||
if len(resp) == 3:
|
||||
status, headers, body = resp
|
||||
else:
|
||||
status, body = resp
|
||||
headers = {}
|
||||
return TestResponse({
|
||||
"status_code": status,
|
||||
"text": body,
|
||||
"headers": headers,
|
||||
})
|
364
troveclient/openstack/common/gettextutils.py
Normal file
364
troveclient/openstack/common/gettextutils.py
Normal file
@ -0,0 +1,364 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Red Hat, Inc.
|
||||
# Copyright 2013 IBM Corp.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
gettext for openstack-common modules.
|
||||
|
||||
Usual usage in an openstack.common module:
|
||||
|
||||
from troveclient.openstack.common.gettextutils import _
|
||||
"""
|
||||
|
||||
import copy
|
||||
import gettext
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
try:
|
||||
import UserString as _userString
|
||||
except ImportError:
|
||||
import collections as _userString
|
||||
|
||||
from babel import localedata
|
||||
import six
|
||||
|
||||
_localedir = os.environ.get('troveclient'.upper() + '_LOCALEDIR')
|
||||
_t = gettext.translation('troveclient', localedir=_localedir, fallback=True)
|
||||
|
||||
_AVAILABLE_LANGUAGES = {}
|
||||
USE_LAZY = False
|
||||
|
||||
|
||||
def enable_lazy():
|
||||
"""Convenience function for configuring _() to use lazy gettext
|
||||
|
||||
Call this at the start of execution to enable the gettextutils._
|
||||
function to use lazy gettext functionality. This is useful if
|
||||
your project is importing _ directly instead of using the
|
||||
gettextutils.install() way of importing the _ function.
|
||||
"""
|
||||
global USE_LAZY
|
||||
USE_LAZY = True
|
||||
|
||||
|
||||
def _(msg):
|
||||
if USE_LAZY:
|
||||
return Message(msg, 'troveclient')
|
||||
else:
|
||||
if six.PY3:
|
||||
return _t.gettext(msg)
|
||||
return _t.ugettext(msg)
|
||||
|
||||
|
||||
def install(domain, lazy=False):
|
||||
"""Install a _() function using the given translation domain.
|
||||
|
||||
Given a translation domain, install a _() function using gettext's
|
||||
install() function.
|
||||
|
||||
The main difference from gettext.install() is that we allow
|
||||
overriding the default localedir (e.g. /usr/share/locale) using
|
||||
a translation-domain-specific environment variable (e.g.
|
||||
NOVA_LOCALEDIR).
|
||||
|
||||
:param domain: the translation domain
|
||||
:param lazy: indicates whether or not to install the lazy _() function.
|
||||
The lazy _() introduces a way to do deferred translation
|
||||
of messages by installing a _ that builds Message objects,
|
||||
instead of strings, which can then be lazily translated into
|
||||
any available locale.
|
||||
"""
|
||||
if lazy:
|
||||
# NOTE(mrodden): Lazy gettext functionality.
|
||||
#
|
||||
# The following introduces a deferred way to do translations on
|
||||
# messages in OpenStack. We override the standard _() function
|
||||
# and % (format string) operation to build Message objects that can
|
||||
# later be translated when we have more information.
|
||||
#
|
||||
# Also included below is an example LocaleHandler that translates
|
||||
# Messages to an associated locale, effectively allowing many logs,
|
||||
# each with their own locale.
|
||||
|
||||
def _lazy_gettext(msg):
|
||||
"""Create and return a Message object.
|
||||
|
||||
Lazy gettext function for a given domain, it is a factory method
|
||||
for a project/module to get a lazy gettext function for its own
|
||||
translation domain (i.e. nova, glance, cinder, etc.)
|
||||
|
||||
Message encapsulates a string so that we can translate
|
||||
it later when needed.
|
||||
"""
|
||||
return Message(msg, domain)
|
||||
|
||||
from six import moves
|
||||
moves.builtins.__dict__['_'] = _lazy_gettext
|
||||
else:
|
||||
localedir = '%s_LOCALEDIR' % domain.upper()
|
||||
if six.PY3:
|
||||
gettext.install(domain,
|
||||
localedir=os.environ.get(localedir))
|
||||
else:
|
||||
gettext.install(domain,
|
||||
localedir=os.environ.get(localedir),
|
||||
unicode=True)
|
||||
|
||||
|
||||
class Message(_userString.UserString, object):
|
||||
"""Class used to encapsulate translatable messages."""
|
||||
def __init__(self, msg, domain):
|
||||
# _msg is the gettext msgid and should never change
|
||||
self._msg = msg
|
||||
self._left_extra_msg = ''
|
||||
self._right_extra_msg = ''
|
||||
self._locale = None
|
||||
self.params = None
|
||||
self.domain = domain
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
# NOTE(mrodden): this should always resolve to a unicode string
|
||||
# that best represents the state of the message currently
|
||||
|
||||
localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
|
||||
if self.locale:
|
||||
lang = gettext.translation(self.domain,
|
||||
localedir=localedir,
|
||||
languages=[self.locale],
|
||||
fallback=True)
|
||||
else:
|
||||
# use system locale for translations
|
||||
lang = gettext.translation(self.domain,
|
||||
localedir=localedir,
|
||||
fallback=True)
|
||||
|
||||
if six.PY3:
|
||||
ugettext = lang.gettext
|
||||
else:
|
||||
ugettext = lang.ugettext
|
||||
|
||||
full_msg = (self._left_extra_msg +
|
||||
ugettext(self._msg) +
|
||||
self._right_extra_msg)
|
||||
|
||||
if self.params is not None:
|
||||
full_msg = full_msg % self.params
|
||||
|
||||
return six.text_type(full_msg)
|
||||
|
||||
@property
|
||||
def locale(self):
|
||||
return self._locale
|
||||
|
||||
@locale.setter
|
||||
def locale(self, value):
|
||||
self._locale = value
|
||||
if not self.params:
|
||||
return
|
||||
|
||||
# This Message object may have been constructed with one or more
|
||||
# Message objects as substitution parameters, given as a single
|
||||
# Message, or a tuple or Map containing some, so when setting the
|
||||
# locale for this Message we need to set it for those Messages too.
|
||||
if isinstance(self.params, Message):
|
||||
self.params.locale = value
|
||||
return
|
||||
if isinstance(self.params, tuple):
|
||||
for param in self.params:
|
||||
if isinstance(param, Message):
|
||||
param.locale = value
|
||||
return
|
||||
for param in self.params.values():
|
||||
if isinstance(param, Message):
|
||||
param.locale = value
|
||||
|
||||
def _save_dictionary_parameter(self, dict_param):
|
||||
full_msg = self.data
|
||||
# look for %(blah) fields in string;
|
||||
# ignore %% and deal with the
|
||||
# case where % is first character on the line
|
||||
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg)
|
||||
|
||||
# if we don't find any %(blah) blocks but have a %s
|
||||
if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg):
|
||||
# apparently the full dictionary is the parameter
|
||||
params = copy.deepcopy(dict_param)
|
||||
else:
|
||||
params = {}
|
||||
for key in keys:
|
||||
try:
|
||||
params[key] = copy.deepcopy(dict_param[key])
|
||||
except TypeError:
|
||||
# cast uncopyable thing to unicode string
|
||||
params[key] = six.text_type(dict_param[key])
|
||||
|
||||
return params
|
||||
|
||||
def _save_parameters(self, other):
|
||||
# we check for None later to see if
|
||||
# we actually have parameters to inject,
|
||||
# so encapsulate if our parameter is actually None
|
||||
if other is None:
|
||||
self.params = (other, )
|
||||
elif isinstance(other, dict):
|
||||
self.params = self._save_dictionary_parameter(other)
|
||||
else:
|
||||
# fallback to casting to unicode,
|
||||
# this will handle the problematic python code-like
|
||||
# objects that cannot be deep-copied
|
||||
try:
|
||||
self.params = copy.deepcopy(other)
|
||||
except TypeError:
|
||||
self.params = six.text_type(other)
|
||||
|
||||
return self
|
||||
|
||||
# overrides to be more string-like
|
||||
def __unicode__(self):
|
||||
return self.data
|
||||
|
||||
def __str__(self):
|
||||
if six.PY3:
|
||||
return self.__unicode__()
|
||||
return self.data.encode('utf-8')
|
||||
|
||||
def __getstate__(self):
|
||||
to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
|
||||
'domain', 'params', '_locale']
|
||||
new_dict = self.__dict__.fromkeys(to_copy)
|
||||
for attr in to_copy:
|
||||
new_dict[attr] = copy.deepcopy(self.__dict__[attr])
|
||||
|
||||
return new_dict
|
||||
|
||||
def __setstate__(self, state):
|
||||
for (k, v) in state.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
# operator overloads
|
||||
def __add__(self, other):
|
||||
copied = copy.deepcopy(self)
|
||||
copied._right_extra_msg += other.__str__()
|
||||
return copied
|
||||
|
||||
def __radd__(self, other):
|
||||
copied = copy.deepcopy(self)
|
||||
copied._left_extra_msg += other.__str__()
|
||||
return copied
|
||||
|
||||
def __mod__(self, other):
|
||||
# do a format string to catch and raise
|
||||
# any possible KeyErrors from missing parameters
|
||||
self.data % other
|
||||
copied = copy.deepcopy(self)
|
||||
return copied._save_parameters(other)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.data * other
|
||||
|
||||
def __rmul__(self, other):
|
||||
return other * self.data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.data[key]
|
||||
|
||||
def __getslice__(self, start, end):
|
||||
return self.data.__getslice__(start, end)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
# NOTE(mrodden): handle lossy operations that we can't deal with yet
|
||||
# These override the UserString implementation, since UserString
|
||||
# uses our __class__ attribute to try and build a new message
|
||||
# after running the inner data string through the operation.
|
||||
# At that point, we have lost the gettext message id and can just
|
||||
# safely resolve to a string instead.
|
||||
ops = ['capitalize', 'center', 'decode', 'encode',
|
||||
'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
|
||||
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
|
||||
if name in ops:
|
||||
return getattr(self.data, name)
|
||||
else:
|
||||
return _userString.UserString.__getattribute__(self, name)
|
||||
|
||||
|
||||
def get_available_languages(domain):
|
||||
"""Lists the available languages for the given translation domain.
|
||||
|
||||
:param domain: the domain to get languages for
|
||||
"""
|
||||
if domain in _AVAILABLE_LANGUAGES:
|
||||
return copy.copy(_AVAILABLE_LANGUAGES[domain])
|
||||
|
||||
localedir = '%s_LOCALEDIR' % domain.upper()
|
||||
find = lambda x: gettext.find(domain,
|
||||
localedir=os.environ.get(localedir),
|
||||
languages=[x])
|
||||
|
||||
# NOTE(mrodden): en_US should always be available (and first in case
|
||||
# order matters) since our in-line message strings are en_US
|
||||
language_list = ['en_US']
|
||||
# NOTE(luisg): Babel <1.0 used a function called list(), which was
|
||||
# renamed to locale_identifiers() in >=1.0, the requirements master list
|
||||
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
|
||||
# this check when the master list updates to >=1.0, and all projects udpate
|
||||
list_identifiers = (getattr(localedata, 'list', None) or
|
||||
getattr(localedata, 'locale_identifiers'))
|
||||
locale_identifiers = list_identifiers()
|
||||
for i in locale_identifiers:
|
||||
if find(i) is not None:
|
||||
language_list.append(i)
|
||||
_AVAILABLE_LANGUAGES[domain] = language_list
|
||||
return copy.copy(language_list)
|
||||
|
||||
|
||||
def get_localized_message(message, user_locale):
|
||||
"""Gets a localized version of the given message in the given locale."""
|
||||
if isinstance(message, Message):
|
||||
if user_locale:
|
||||
message.locale = user_locale
|
||||
return six.text_type(message)
|
||||
else:
|
||||
return message
|
||||
|
||||
|
||||
class LocaleHandler(logging.Handler):
|
||||
"""Handler that can have a locale associated to translate Messages.
|
||||
|
||||
A quick example of how to utilize the Message class above.
|
||||
LocaleHandler takes a locale and a target logging.Handler object
|
||||
to forward LogRecord objects to after translating the internal Message.
|
||||
"""
|
||||
|
||||
def __init__(self, locale, target):
|
||||
"""Initialize a LocaleHandler
|
||||
|
||||
:param locale: locale to use for translating messages
|
||||
:param target: logging.Handler object to forward
|
||||
LogRecord objects to after translation
|
||||
"""
|
||||
logging.Handler.__init__(self)
|
||||
self.locale = locale
|
||||
self.target = target
|
||||
|
||||
def emit(self, record):
|
||||
if isinstance(record.msg, Message):
|
||||
# set the locale and resolve to a string
|
||||
record.msg.locale = self.locale
|
||||
|
||||
self.target.emit(record)
|
68
troveclient/openstack/common/importutils.py
Normal file
68
troveclient/openstack/common/importutils.py
Normal file
@ -0,0 +1,68 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Import related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class."""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
try:
|
||||
__import__(mod_str)
|
||||
return getattr(sys.modules[mod_str], class_str)
|
||||
except (ValueError, AttributeError):
|
||||
raise ImportError('Class %s cannot be found (%s)' %
|
||||
(class_str,
|
||||
traceback.format_exception(*sys.exc_info())))
|
||||
|
||||
|
||||
def import_object(import_str, *args, **kwargs):
|
||||
"""Import a class and return an instance of it."""
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_object_ns(name_space, import_str, *args, **kwargs):
|
||||
"""Tries to import object from default namespace.
|
||||
|
||||
Imports a class and return an instance of it, first by trying
|
||||
to find the class in a default namespace, then failing back to
|
||||
a full path if not found in the default namespace.
|
||||
"""
|
||||
import_value = "%s.%s" % (name_space, import_str)
|
||||
try:
|
||||
return import_class(import_value)(*args, **kwargs)
|
||||
except ImportError:
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_module(import_str):
|
||||
"""Import a module."""
|
||||
__import__(import_str)
|
||||
return sys.modules[import_str]
|
||||
|
||||
|
||||
def try_import(import_str, default=None):
|
||||
"""Try to import a module and if it fails return default."""
|
||||
try:
|
||||
return import_module(import_str)
|
||||
except ImportError:
|
||||
return default
|
218
troveclient/openstack/common/strutils.py
Normal file
218
troveclient/openstack/common/strutils.py
Normal file
@ -0,0 +1,218 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
System-level utilities and helper functions.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
import six
|
||||
|
||||
from troveclient.openstack.common.gettextutils import _ # noqa
|
||||
|
||||
|
||||
# Used for looking up extensions of text
|
||||
# to their 'multiplied' byte amount
|
||||
BYTE_MULTIPLIERS = {
|
||||
'': 1,
|
||||
't': 1024 ** 4,
|
||||
'g': 1024 ** 3,
|
||||
'm': 1024 ** 2,
|
||||
'k': 1024,
|
||||
}
|
||||
BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)')
|
||||
|
||||
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
|
||||
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
|
||||
|
||||
SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
|
||||
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
|
||||
|
||||
|
||||
def int_from_bool_as_string(subject):
|
||||
"""Interpret a string as a boolean and return either 1 or 0.
|
||||
|
||||
Any string value in:
|
||||
|
||||
('True', 'true', 'On', 'on', '1')
|
||||
|
||||
is interpreted as a boolean True.
|
||||
|
||||
Useful for JSON-decoded stuff and config file parsing
|
||||
"""
|
||||
return bool_from_string(subject) and 1 or 0
|
||||
|
||||
|
||||
def bool_from_string(subject, strict=False):
|
||||
"""Interpret a string as a boolean.
|
||||
|
||||
A case-insensitive match is performed such that strings matching 't',
|
||||
'true', 'on', 'y', 'yes', or '1' are considered True and, when
|
||||
`strict=False`, anything else is considered False.
|
||||
|
||||
Useful for JSON-decoded stuff and config file parsing.
|
||||
|
||||
If `strict=True`, unrecognized values, including None, will raise a
|
||||
ValueError which is useful when parsing values passed in from an API call.
|
||||
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
|
||||
"""
|
||||
if not isinstance(subject, six.string_types):
|
||||
subject = str(subject)
|
||||
|
||||
lowered = subject.strip().lower()
|
||||
|
||||
if lowered in TRUE_STRINGS:
|
||||
return True
|
||||
elif lowered in FALSE_STRINGS:
|
||||
return False
|
||||
elif strict:
|
||||
acceptable = ', '.join(
|
||||
"'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
|
||||
msg = _("Unrecognized value '%(val)s', acceptable values are:"
|
||||
" %(acceptable)s") % {'val': subject,
|
||||
'acceptable': acceptable}
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def safe_decode(text, incoming=None, errors='strict'):
|
||||
"""Decodes incoming str using `incoming` if they're not already unicode.
|
||||
|
||||
:param incoming: Text's current encoding
|
||||
:param errors: Errors handling policy. See here for valid
|
||||
values http://docs.python.org/2/library/codecs.html
|
||||
:returns: text or a unicode `incoming` encoded
|
||||
representation of it.
|
||||
:raises TypeError: If text is not an isntance of str
|
||||
"""
|
||||
if not isinstance(text, six.string_types):
|
||||
raise TypeError("%s can't be decoded" % type(text))
|
||||
|
||||
if isinstance(text, six.text_type):
|
||||
return text
|
||||
|
||||
if not incoming:
|
||||
incoming = (sys.stdin.encoding or
|
||||
sys.getdefaultencoding())
|
||||
|
||||
try:
|
||||
return text.decode(incoming, errors)
|
||||
except UnicodeDecodeError:
|
||||
# Note(flaper87) If we get here, it means that
|
||||
# sys.stdin.encoding / sys.getdefaultencoding
|
||||
# didn't return a suitable encoding to decode
|
||||
# text. This happens mostly when global LANG
|
||||
# var is not set correctly and there's no
|
||||
# default encoding. In this case, most likely
|
||||
# python will use ASCII or ANSI encoders as
|
||||
# default encodings but they won't be capable
|
||||
# of decoding non-ASCII characters.
|
||||
#
|
||||
# Also, UTF-8 is being used since it's an ASCII
|
||||
# extension.
|
||||
return text.decode('utf-8', errors)
|
||||
|
||||
|
||||
def safe_encode(text, incoming=None,
|
||||
encoding='utf-8', errors='strict'):
|
||||
"""Encodes incoming str/unicode using `encoding`.
|
||||
|
||||
If incoming is not specified, text is expected to be encoded with
|
||||
current python's default encoding. (`sys.getdefaultencoding`)
|
||||
|
||||
:param incoming: Text's current encoding
|
||||
:param encoding: Expected encoding for text (Default UTF-8)
|
||||
:param errors: Errors handling policy. See here for valid
|
||||
values http://docs.python.org/2/library/codecs.html
|
||||
:returns: text or a bytestring `encoding` encoded
|
||||
representation of it.
|
||||
:raises TypeError: If text is not an isntance of str
|
||||
"""
|
||||
if not isinstance(text, six.string_types):
|
||||
raise TypeError("%s can't be encoded" % type(text))
|
||||
|
||||
if not incoming:
|
||||
incoming = (sys.stdin.encoding or
|
||||
sys.getdefaultencoding())
|
||||
|
||||
if isinstance(text, six.text_type):
|
||||
return text.encode(encoding, errors)
|
||||
elif text and encoding != incoming:
|
||||
# Decode text before encoding it with `encoding`
|
||||
text = safe_decode(text, incoming, errors)
|
||||
return text.encode(encoding, errors)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def to_bytes(text, default=0):
|
||||
"""Converts a string into an integer of bytes.
|
||||
|
||||
Looks at the last characters of the text to determine
|
||||
what conversion is needed to turn the input text into a byte number.
|
||||
Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive)
|
||||
|
||||
:param text: String input for bytes size conversion.
|
||||
:param default: Default return value when text is blank.
|
||||
|
||||
"""
|
||||
match = BYTE_REGEX.search(text)
|
||||
if match:
|
||||
magnitude = int(match.group(1))
|
||||
mult_key_org = match.group(2)
|
||||
if not mult_key_org:
|
||||
return magnitude
|
||||
elif text:
|
||||
msg = _('Invalid string format: %s') % text
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
return default
|
||||
mult_key = mult_key_org.lower().replace('b', '', 1)
|
||||
multiplier = BYTE_MULTIPLIERS.get(mult_key)
|
||||
if multiplier is None:
|
||||
msg = _('Unknown byte multiplier: %s') % mult_key_org
|
||||
raise TypeError(msg)
|
||||
return magnitude * multiplier
|
||||
|
||||
|
||||
def to_slug(value, incoming=None, errors="strict"):
|
||||
"""Normalize string.
|
||||
|
||||
Convert to lowercase, remove non-word characters, and convert spaces
|
||||
to hyphens.
|
||||
|
||||
Inspired by Django's `slugify` filter.
|
||||
|
||||
:param value: Text to slugify
|
||||
:param incoming: Text's current encoding
|
||||
:param errors: Errors handling policy. See here for valid
|
||||
values http://docs.python.org/2/library/codecs.html
|
||||
:returns: slugified unicode representation of `value`
|
||||
:raises TypeError: If text is not an instance of str
|
||||
"""
|
||||
value = safe_decode(value, incoming, errors)
|
||||
# NOTE(aababilov): no need to use safe_(encode|decode) here:
|
||||
# encodings are always "ascii", error handling is always "ignore"
|
||||
# and types are always known (first: unicode; second: str)
|
||||
value = unicodedata.normalize("NFKD", value).encode(
|
||||
"ascii", "ignore").decode("ascii")
|
||||
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
|
||||
return SLUGIFY_HYPHENATE_RE.sub("-", value)
|
86
troveclient/service_catalog.py
Normal file
86
troveclient/service_catalog.py
Normal file
@ -0,0 +1,86 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011, Piston Cloud Computing, Inc.
|
||||
#
|
||||
# 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 openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
class ServiceCatalog(object):
|
||||
"""Helper methods for dealing with a Keystone Service Catalog."""
|
||||
|
||||
def __init__(self, resource_dict):
|
||||
self.catalog = resource_dict
|
||||
|
||||
def get_token(self):
|
||||
return self.catalog['access']['token']['id']
|
||||
|
||||
def url_for(self, attr=None, filter_value=None,
|
||||
service_type=None, endpoint_type='publicURL',
|
||||
service_name=None, database_service_name=None):
|
||||
"""Fetch the public URL from the Compute service for
|
||||
a particular endpoint attribute. If none given, return
|
||||
the first. See tests for sample service catalog.
|
||||
"""
|
||||
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 'serviceCatalog' not in self.catalog['access']:
|
||||
return None
|
||||
|
||||
# Full catalog ...
|
||||
catalog = self.catalog['access']['serviceCatalog']
|
||||
|
||||
for service in catalog:
|
||||
|
||||
# NOTE(thingee): For backwards compatibility, if they have v2
|
||||
# enabled and the service_type is set to 'database', go ahead and
|
||||
# accept that.
|
||||
skip_service_type_check = False
|
||||
if service_type == 'databasev2' and service['type'] == 'database':
|
||||
version = service['endpoints'][0]['publicURL'].split('/')[3]
|
||||
if version == 'v2':
|
||||
skip_service_type_check = True
|
||||
|
||||
if (not skip_service_type_check
|
||||
and service.get("type") != service_type):
|
||||
continue
|
||||
|
||||
if (database_service_name and service_type in ('database',
|
||||
'databasev2')
|
||||
and service.get('name') != database_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][endpoint_type]
|
533
troveclient/shell.py
Normal file
533
troveclient/shell.py
Normal file
@ -0,0 +1,533 @@
|
||||
# Copyright 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.
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack Trove API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import imp
|
||||
import itertools
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
import troveclient
|
||||
from troveclient import client
|
||||
#from troveclient import exceptions as exc
|
||||
#import troveclient.extension
|
||||
from troveclient.openstack.common import strutils
|
||||
from troveclient.openstack.common.apiclient import exceptions as exc
|
||||
from troveclient import utils
|
||||
from troveclient.v1 import shell as shell_v1
|
||||
|
||||
DEFAULT_OS_DATABASE_API_VERSION = "1.0"
|
||||
DEFAULT_TROVE_ENDPOINT_TYPE = 'publicURL'
|
||||
DEFAULT_TROVE_SERVICE_TYPE = 'database'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TroveClientArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TroveClientArgumentParser, self).__init__(*args, **kwargs)
|
||||
|
||||
def error(self, message):
|
||||
"""error(message: string)
|
||||
|
||||
Prints a usage message incorporating the message to stderr and
|
||||
exits.
|
||||
"""
|
||||
self.print_usage(sys.stderr)
|
||||
#FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
|
||||
choose_from = ' (choose from'
|
||||
progparts = self.prog.partition(' ')
|
||||
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
|
||||
" for more information.\n" %
|
||||
{'errmsg': message.split(choose_from)[0],
|
||||
'mainp': progparts[0],
|
||||
'subp': progparts[2]})
|
||||
|
||||
|
||||
class OpenStackTroveShell(object):
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = TroveClientArgumentParser(
|
||||
prog='trove',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "trove help COMMAND" '
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--version',
|
||||
action='version',
|
||||
version=troveclient.__version__)
|
||||
|
||||
parser.add_argument('--debug',
|
||||
action='store_true',
|
||||
default=utils.env('TROVECLIENT_DEBUG',
|
||||
default=False),
|
||||
help="Print debugging output")
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
metavar='<auth-user-name>',
|
||||
default=utils.env('OS_USERNAME',
|
||||
'TROVE_USERNAME'),
|
||||
help='Defaults to env[OS_USERNAME].')
|
||||
parser.add_argument('--os_username',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-password',
|
||||
metavar='<auth-password>',
|
||||
default=utils.env('OS_PASSWORD',
|
||||
'TROVE_PASSWORD'),
|
||||
help='Defaults to env[OS_PASSWORD].')
|
||||
parser.add_argument('--os_password',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-tenant-name',
|
||||
metavar='<auth-tenant-name>',
|
||||
default=utils.env('OS_TENANT_NAME',
|
||||
'TROVE_PROJECT_ID'),
|
||||
help='Defaults to env[OS_TENANT_NAME].')
|
||||
parser.add_argument('--os_tenant_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-tenant-id',
|
||||
metavar='<auth-tenant-id>',
|
||||
default=utils.env('OS_TENANT_ID',
|
||||
'TROVE_TENANT_ID'),
|
||||
help='Defaults to env[OS_TENANT_ID].')
|
||||
parser.add_argument('--os_tenant_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-auth-url',
|
||||
metavar='<auth-url>',
|
||||
default=utils.env('OS_AUTH_URL',
|
||||
'TROVE_URL'),
|
||||
help='Defaults to env[OS_AUTH_URL].')
|
||||
parser.add_argument('--os_auth_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-region-name',
|
||||
metavar='<region-name>',
|
||||
default=utils.env('OS_REGION_NAME',
|
||||
'TROVE_REGION_NAME'),
|
||||
help='Defaults to env[OS_REGION_NAME].')
|
||||
parser.add_argument('--os_region_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--service-type',
|
||||
metavar='<service-type>',
|
||||
help='Defaults to database for most actions')
|
||||
parser.add_argument('--service_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--service-name',
|
||||
metavar='<service-name>',
|
||||
default=utils.env('TROVE_SERVICE_NAME'),
|
||||
help='Defaults to env[TROVE_SERVICE_NAME]')
|
||||
parser.add_argument('--service_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--database-service-name',
|
||||
metavar='<database-service-name>',
|
||||
default=utils.env('TROVE_DATABASE_SERVICE_NAME'),
|
||||
help='Defaults to env'
|
||||
'[TROVE_DATABASE_SERVICE_NAME]')
|
||||
parser.add_argument('--database_service_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--endpoint-type',
|
||||
metavar='<endpoint-type>',
|
||||
default=utils.env('TROVE_ENDPOINT_TYPE',
|
||||
default=DEFAULT_TROVE_ENDPOINT_TYPE),
|
||||
help='Defaults to env[TROVE_ENDPOINT_TYPE] or '
|
||||
+ DEFAULT_TROVE_ENDPOINT_TYPE + '.')
|
||||
parser.add_argument('--endpoint_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-database-api-version',
|
||||
metavar='<database-api-ver>',
|
||||
default=utils.env('OS_DATABASE_API_VERSION',
|
||||
default=DEFAULT_OS_DATABASE_API_VERSION),
|
||||
help='Accepts 1,defaults '
|
||||
'to env[OS_DATABASE_API_VERSION].')
|
||||
parser.add_argument('--os_database_api_version',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-cacert',
|
||||
metavar='<ca-certificate>',
|
||||
default=utils.env('OS_CACERT', default=None),
|
||||
help='Specify a CA bundle file to use in '
|
||||
'verifying a TLS (https) server certificate. '
|
||||
'Defaults to env[OS_CACERT]')
|
||||
|
||||
parser.add_argument('--insecure',
|
||||
default=utils.env('TROVECLIENT_INSECURE',
|
||||
default=False),
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--retries',
|
||||
metavar='<retries>',
|
||||
type=int,
|
||||
default=0,
|
||||
help='Number of retries.')
|
||||
|
||||
# FIXME(dtroyer): The args below are here for diablo compatibility,
|
||||
# remove them in folsum cycle
|
||||
|
||||
# alias for --os-username, left in for backwards compatibility
|
||||
parser.add_argument('--username',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
# alias for --os-region_name, left in for backwards compatibility
|
||||
parser.add_argument('--region_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
# alias for --os-password, left in for backwards compatibility
|
||||
parser.add_argument('--apikey', '--password', dest='apikey',
|
||||
default=utils.env('TROVE_API_KEY'),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
# alias for --os-tenant-name, left in for backward compatibility
|
||||
parser.add_argument('--projectid', '--tenant_name', dest='projectid',
|
||||
default=utils.env('TROVE_PROJECT_ID'),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
# alias for --os-auth-url, left in for backward compatibility
|
||||
parser.add_argument('--url', '--auth_url', dest='url',
|
||||
default=utils.env('TROVE_URL'),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
|
||||
try:
|
||||
actions_module = {
|
||||
'1.0': shell_v1,
|
||||
}[version]
|
||||
except KeyError:
|
||||
actions_module = shell_v1
|
||||
|
||||
self._find_actions(subparsers, actions_module)
|
||||
self._find_actions(subparsers, self)
|
||||
|
||||
for extension in self.extensions:
|
||||
self._find_actions(subparsers, extension.module)
|
||||
|
||||
self._add_bash_completion_subparser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
def _discover_extensions(self, version):
|
||||
extensions = []
|
||||
for name, module in itertools.chain(
|
||||
self._discover_via_python_path(version),
|
||||
self._discover_via_contrib_path(version)):
|
||||
|
||||
extension = troveclient.extension.Extension(name, module)
|
||||
extensions.append(extension)
|
||||
|
||||
return extensions
|
||||
|
||||
def _discover_via_python_path(self, version):
|
||||
for (module_loader, name, ispkg) in pkgutil.iter_modules():
|
||||
if name.endswith('python_troveclient_ext'):
|
||||
if not hasattr(module_loader, 'load_module'):
|
||||
# Python 2.6 compat: actually get an ImpImporter obj
|
||||
module_loader = module_loader.find_module(name)
|
||||
|
||||
module = module_loader.load_module(name)
|
||||
yield name, module
|
||||
|
||||
def _discover_via_contrib_path(self, version):
|
||||
module_path = os.path.dirname(os.path.abspath(__file__))
|
||||
version_str = "v%s" % version.replace('.', '_')
|
||||
ext_path = os.path.join(module_path, version_str, 'contrib')
|
||||
ext_glob = os.path.join(ext_path, "*.py")
|
||||
|
||||
for ext_path in glob.iglob(ext_glob):
|
||||
name = os.path.basename(ext_path)[:-3]
|
||||
|
||||
if name == "__init__":
|
||||
continue
|
||||
|
||||
module = imp.load_source(name, ext_path)
|
||||
yield name, module
|
||||
|
||||
def _add_bash_completion_subparser(self, subparsers):
|
||||
subparser = subparsers.add_parser(
|
||||
'bash_completion',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter)
|
||||
|
||||
self.subcommands['bash_completion'] = subparser
|
||||
subparser.set_defaults(func=self.do_bash_completion)
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hypen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter)
|
||||
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,)
|
||||
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def setup_debugging(self, debug):
|
||||
if not debug:
|
||||
return
|
||||
|
||||
streamhandler = logging.StreamHandler()
|
||||
streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
|
||||
streamhandler.setFormatter(logging.Formatter(streamformat))
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.addHandler(streamhandler)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version and debug settings
|
||||
parser = self.get_base_parser()
|
||||
(options, args) = parser.parse_known_args(argv)
|
||||
self.setup_debugging(options.debug)
|
||||
|
||||
# build available subcommands based on version
|
||||
self.extensions = self._discover_extensions(
|
||||
options.os_database_api_version)
|
||||
self._run_extension_hooks('__pre_parse_args__')
|
||||
|
||||
subcommand_parser = self.get_subcommand_parser(
|
||||
options.os_database_api_version)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
if options.help or not argv:
|
||||
subcommand_parser.print_help()
|
||||
return 0
|
||||
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
self._run_extension_hooks('__post_parse_args__', args)
|
||||
|
||||
# Short-circuit and deal with help right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
elif args.func == self.do_bash_completion:
|
||||
self.do_bash_completion(args)
|
||||
return 0
|
||||
|
||||
(os_username, os_password, os_tenant_name, os_auth_url,
|
||||
os_region_name, os_tenant_id, endpoint_type, insecure,
|
||||
service_type, service_name, database_service_name,
|
||||
username, apikey, projectid, url, region_name, cacert) = (
|
||||
args.os_username, args.os_password,
|
||||
args.os_tenant_name, args.os_auth_url,
|
||||
args.os_region_name, args.os_tenant_id,
|
||||
args.endpoint_type, args.insecure,
|
||||
args.service_type, args.service_name,
|
||||
args.database_service_name, args.username,
|
||||
args.apikey, args.projectid,
|
||||
args.url, args.region_name, args.os_cacert)
|
||||
|
||||
if not endpoint_type:
|
||||
endpoint_type = DEFAULT_TROVE_ENDPOINT_TYPE
|
||||
|
||||
if not service_type:
|
||||
service_type = DEFAULT_TROVE_SERVICE_TYPE
|
||||
service_type = utils.get_service_type(args.func) or service_type
|
||||
|
||||
#FIXME(usrleon): Here should be restrict for project id same as
|
||||
# for os_username or os_password but for compatibility it is not.
|
||||
|
||||
if not utils.isunauthenticated(args.func):
|
||||
if not os_username:
|
||||
if not username:
|
||||
raise exc.CommandError(
|
||||
"You must provide a username "
|
||||
"via either --os-username or env[OS_USERNAME]")
|
||||
else:
|
||||
os_username = username
|
||||
|
||||
if not os_password:
|
||||
if not apikey:
|
||||
raise exc.CommandError("You must provide a password "
|
||||
"via either --os-password or via "
|
||||
"env[OS_PASSWORD]")
|
||||
else:
|
||||
os_password = apikey
|
||||
|
||||
if not (os_tenant_name or os_tenant_id):
|
||||
if not projectid:
|
||||
raise exc.CommandError("You must provide a tenant_id "
|
||||
"via either --os-tenant-id or "
|
||||
"env[OS_TENANT_ID]")
|
||||
else:
|
||||
os_tenant_name = projectid
|
||||
|
||||
if not os_auth_url:
|
||||
if not url:
|
||||
raise exc.CommandError(
|
||||
"You must provide an auth url "
|
||||
"via either --os-auth-url or env[OS_AUTH_URL]")
|
||||
else:
|
||||
os_auth_url = url
|
||||
|
||||
if not os_region_name and region_name:
|
||||
os_region_name = region_name
|
||||
|
||||
if not (os_tenant_name or os_tenant_id):
|
||||
raise exc.CommandError(
|
||||
"You must provide a tenant_id "
|
||||
"via either --os-tenant-id or env[OS_TENANT_ID]")
|
||||
|
||||
if not os_auth_url:
|
||||
raise exc.CommandError(
|
||||
"You must provide an auth url "
|
||||
"via either --os-auth-url or env[OS_AUTH_URL]")
|
||||
|
||||
self.cs = client.Client(options.os_database_api_version, os_username,
|
||||
os_password, os_tenant_name, os_auth_url,
|
||||
insecure, region_name=os_region_name,
|
||||
tenant_id=os_tenant_id,
|
||||
endpoint_type=endpoint_type,
|
||||
extensions=self.extensions,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
database_service_name=database_service_name,
|
||||
retries=options.retries,
|
||||
http_log_debug=args.debug,
|
||||
cacert=cacert)
|
||||
|
||||
try:
|
||||
if not utils.isunauthenticated(args.func):
|
||||
self.cs.authenticate()
|
||||
except exc.Unauthorized:
|
||||
raise exc.CommandError("Invalid OpenStack Trove credentials.")
|
||||
except exc.AuthorizationFailure:
|
||||
raise exc.CommandError("Unable to authorize user")
|
||||
|
||||
endpoint_api_version = self.cs.get_database_api_version_from_endpoint()
|
||||
if endpoint_api_version != options.os_database_api_version:
|
||||
msg = (("Database API version is set to %s "
|
||||
"but you are accessing a %s endpoint. "
|
||||
"Change its value via either --os-database-api-version "
|
||||
"or env[OS_DATABASE_API_VERSION]")
|
||||
% (options.os_database_api_version, endpoint_api_version))
|
||||
#raise exc.InvalidAPIVersion(msg)
|
||||
raise exc.UnsupportedVersion(msg)
|
||||
|
||||
args.func(self.cs, args)
|
||||
|
||||
def _run_extension_hooks(self, hook_type, *args, **kwargs):
|
||||
"""Run hooks for all registered extensions."""
|
||||
for extension in self.extensions:
|
||||
extension.run_hooks(hook_type, *args, **kwargs)
|
||||
|
||||
def do_bash_completion(self, args):
|
||||
"""Print arguments for bash_completion.
|
||||
|
||||
Prints all of the commands and options to stdout so that the
|
||||
trove.bash_completion script doesn't have to hard code them.
|
||||
"""
|
||||
commands = set()
|
||||
options = set()
|
||||
for sc_str, sc in list(self.subcommands.items()):
|
||||
commands.add(sc_str)
|
||||
for option in list(sc._optionals._option_string_actions.keys()):
|
||||
options.add(option)
|
||||
|
||||
commands.remove('bash-completion')
|
||||
commands.remove('bash_completion')
|
||||
print(' '.join(commands | options))
|
||||
|
||||
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help='Display help for <subcommand>')
|
||||
def do_help(self, args):
|
||||
"""
|
||||
Display help about this program or one of its subcommands.
|
||||
"""
|
||||
if args.command:
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||
args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
# I'm picky about my shell help.
|
||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
if sys.version_info >= (3, 0):
|
||||
OpenStackTroveShell().main(sys.argv[1:])
|
||||
else:
|
||||
OpenStackTroveShell().main(map(strutils.safe_decode,
|
||||
sys.argv[1:]))
|
||||
except KeyboardInterrupt:
|
||||
print("... terminating trove client", file=sys.stderr)
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
logger.debug(e, exc_info=1)
|
||||
message = e.message
|
||||
if not isinstance(message, six.string_types):
|
||||
message = str(message)
|
||||
print("ERROR: %s" % strutils.safe_encode(message), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,7 +1,7 @@
|
||||
from testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import accounts
|
||||
from troveclient.v1 import accounts
|
||||
from troveclient import base
|
||||
|
||||
"""
|
||||
@ -55,11 +55,11 @@ class AccountsTest(TestCase):
|
||||
|
||||
def test_index(self):
|
||||
resp = Mock()
|
||||
resp.status = 400
|
||||
resp.status_code = 400
|
||||
body = {"Accounts": {}}
|
||||
self.accounts.api.client.get = Mock(return_value=(resp, body))
|
||||
self.assertRaises(Exception, self.accounts.index)
|
||||
resp.status = 200
|
||||
resp.status_code = 200
|
||||
self.assertTrue(isinstance(self.accounts.index(), base.Resource))
|
||||
self.accounts.api.client.get = Mock(return_value=(resp, None))
|
||||
self.assertRaises(Exception, self.accounts.index)
|
||||
|
@ -5,12 +5,15 @@ from testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import base
|
||||
from troveclient import exceptions
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
from troveclient import utils
|
||||
|
||||
"""
|
||||
Unit tests for base.py
|
||||
"""
|
||||
|
||||
UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
|
||||
|
||||
|
||||
def obj_class(self, res, loaded=True):
|
||||
return res
|
||||
@ -234,69 +237,72 @@ class ManagerListTest(ManagerTest):
|
||||
self.assertEqual(len(data_), len(l))
|
||||
|
||||
|
||||
class ManagerWithFind(TestCase):
|
||||
class FakeResource(object):
|
||||
|
||||
def __init__(self, _id, properties):
|
||||
self.id = _id
|
||||
try:
|
||||
self.name = properties['name']
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
self.display_name = properties['display_name']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class FakeManager(base.ManagerWithFind):
|
||||
|
||||
resource_class = FakeResource
|
||||
|
||||
resources = [
|
||||
FakeResource('1234', {'name': 'entity_one'}),
|
||||
FakeResource(UUID, {'name': 'entity_two'}),
|
||||
FakeResource('4242', {'display_name': 'entity_three'}),
|
||||
FakeResource('5678', {'name': '9876'})
|
||||
]
|
||||
|
||||
def get(self, resource_id):
|
||||
for resource in self.resources:
|
||||
if resource.id == str(resource_id):
|
||||
return resource
|
||||
raise exceptions.NotFound(resource_id)
|
||||
|
||||
def list(self):
|
||||
return self.resources
|
||||
|
||||
|
||||
class FindResourceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ManagerWithFind, self).setUp()
|
||||
self.orig__init = base.ManagerWithFind.__init__
|
||||
base.ManagerWithFind.__init__ = Mock(return_value=None)
|
||||
self.manager = base.ManagerWithFind()
|
||||
super(FindResourceTestCase, self).setUp()
|
||||
self.manager = FakeManager(None)
|
||||
|
||||
def tearDown(self):
|
||||
super(ManagerWithFind, self).tearDown()
|
||||
base.ManagerWithFind.__init__ = self.orig__init
|
||||
def test_find_none(self):
|
||||
self.assertRaises(exceptions.CommandError,
|
||||
utils.find_resource,
|
||||
self.manager,
|
||||
'asdf')
|
||||
|
||||
def test_find(self):
|
||||
obj1 = Mock()
|
||||
obj1.attr1 = "v1"
|
||||
obj1.attr2 = "v2"
|
||||
obj1.attr3 = "v3"
|
||||
def test_find_by_integer_id(self):
|
||||
output = utils.find_resource(self.manager, 1234)
|
||||
self.assertEqual(output, self.manager.get('1234'))
|
||||
|
||||
obj2 = Mock()
|
||||
obj2.attr1 = "v1"
|
||||
obj2.attr2 = "v2"
|
||||
def test_find_by_str_id(self):
|
||||
output = utils.find_resource(self.manager, '1234')
|
||||
self.assertEqual(output, self.manager.get('1234'))
|
||||
|
||||
self.manager.list = Mock(return_value=[obj1, obj2])
|
||||
self.manager.resource_class = Mock
|
||||
def test_find_by_uuid(self):
|
||||
output = utils.find_resource(self.manager, UUID)
|
||||
self.assertEqual(output, self.manager.get(UUID))
|
||||
|
||||
# exactly one match case
|
||||
found = self.manager.find(attr1="v1", attr2="v2", attr3="v3")
|
||||
self.assertEqual(obj1, found)
|
||||
def test_find_by_str_name(self):
|
||||
output = utils.find_resource(self.manager, 'entity_one')
|
||||
self.assertEqual(output, self.manager.get('1234'))
|
||||
|
||||
# no match case
|
||||
self.assertRaises(exceptions.NotFound, self.manager.find,
|
||||
attr1="v2", attr2="v2", attr3="v3")
|
||||
|
||||
# multiple matches case
|
||||
obj2.attr3 = "v3"
|
||||
self.assertRaises(exceptions.NoUniqueMatch, self.manager.find,
|
||||
attr1="v1", attr2="v2", attr3="v3")
|
||||
|
||||
def test_findall(self):
|
||||
obj1 = Mock()
|
||||
obj1.attr1 = "v1"
|
||||
obj1.attr2 = "v2"
|
||||
obj1.attr3 = "v3"
|
||||
|
||||
obj2 = Mock()
|
||||
obj2.attr1 = "v1"
|
||||
obj2.attr2 = "v2"
|
||||
|
||||
self.manager.list = Mock(return_value=[obj1, obj2])
|
||||
|
||||
found = self.manager.findall(attr1="v1", attr2="v2", attr3="v3")
|
||||
self.assertEqual(1, len(found))
|
||||
self.assertEqual(obj1, found[0])
|
||||
|
||||
found = self.manager.findall(attr1="v2", attr2="v2", attr3="v3")
|
||||
self.assertEqual(0, len(found))
|
||||
|
||||
found = self.manager.findall(attr7="v1", attr2="v2")
|
||||
self.assertEqual(0, len(found))
|
||||
|
||||
def test_list(self):
|
||||
# this method is not yet implemented, exception expected
|
||||
self.assertRaises(NotImplementedError, self.manager.list)
|
||||
def test_find_by_str_displayname(self):
|
||||
output = utils.find_resource(self.manager, 'entity_three')
|
||||
self.assertEqual(output, self.manager.get('4242'))
|
||||
|
||||
|
||||
class ResourceTest(TestCase):
|
||||
|
@ -1,317 +1,34 @@
|
||||
import logging
|
||||
import httplib2
|
||||
import time
|
||||
# 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 testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import client
|
||||
from troveclient import exceptions
|
||||
|
||||
"""
|
||||
Unit tests for client.py
|
||||
"""
|
||||
import troveclient.v1.client
|
||||
from troveclient.client import get_version_map
|
||||
from troveclient.openstack.common.apiclient import client
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
class ClientTest(TestCase):
|
||||
def test_log_to_streamhandler(self):
|
||||
client.log_to_streamhandler()
|
||||
self.assertTrue(client._logger.level == logging.DEBUG)
|
||||
|
||||
def test_get_client_class_v1(self):
|
||||
version_map = get_version_map()
|
||||
output = client.BaseClient.get_class('database',
|
||||
'1.0', version_map)
|
||||
self.assertEqual(output, troveclient.v1.client.Client)
|
||||
|
||||
class TroveHTTPClientTest(TestCase):
|
||||
def setUp(self):
|
||||
super(TroveHTTPClientTest, self).setUp()
|
||||
self.orig__init = client.TroveHTTPClient.__init__
|
||||
client.TroveHTTPClient.__init__ = Mock(return_value=None)
|
||||
self.hc = client.TroveHTTPClient()
|
||||
self.hc.auth_token = "test-auth-token"
|
||||
self.hc.service_url = "test-service-url/"
|
||||
self.hc.tenant = "test-tenant"
|
||||
|
||||
self.__debug_lines = list()
|
||||
|
||||
self.orig_client__logger = client._logger
|
||||
client._logger = Mock()
|
||||
|
||||
self.orig_time = time.time
|
||||
self.orig_htttp_request = httplib2.Http.request
|
||||
|
||||
def tearDown(self):
|
||||
super(TroveHTTPClientTest, self).tearDown()
|
||||
client.TroveHTTPClient.__init__ = self.orig__init
|
||||
client._logger = self.orig_client__logger
|
||||
time.time = self.orig_time
|
||||
httplib2.Http.request = self.orig_htttp_request
|
||||
|
||||
def side_effect_func_for_moc_debug(self, s, *args):
|
||||
self.__debug_lines.append(s)
|
||||
|
||||
def test___init__(self):
|
||||
client.TroveHTTPClient.__init__ = self.orig__init
|
||||
|
||||
user = "test-user"
|
||||
password = "test-password"
|
||||
tenant = "test-tenant"
|
||||
auth_url = "http://test-auth-url/"
|
||||
service_name = None
|
||||
|
||||
# when there is no auth_strategy provided
|
||||
self.assertRaises(ValueError, client.TroveHTTPClient, user,
|
||||
password, tenant, auth_url, service_name)
|
||||
|
||||
hc = client.TroveHTTPClient(user, password, tenant, auth_url,
|
||||
service_name, auth_strategy="fake")
|
||||
self.assertEqual("http://test-auth-url", hc.auth_url)
|
||||
|
||||
# auth_url is none
|
||||
hc = client.TroveHTTPClient(user, password, tenant, None,
|
||||
service_name, auth_strategy="fake")
|
||||
self.assertEqual(None, hc.auth_url)
|
||||
|
||||
def test_get_timings(self):
|
||||
self.hc.times = ["item1", "item2"]
|
||||
self.assertEqual(2, len(self.hc.get_timings()))
|
||||
self.assertEqual("item1", self.hc.get_timings()[0])
|
||||
self.assertEqual("item2", self.hc.get_timings()[1])
|
||||
|
||||
def test_http_log(self):
|
||||
self.hc.simple_log = Mock(return_value=None)
|
||||
self.hc.pretty_log = Mock(return_value=None)
|
||||
|
||||
client.RDC_PP = False
|
||||
self.hc.http_log(None, None, None, None)
|
||||
self.assertEqual(1, self.hc.simple_log.call_count)
|
||||
|
||||
client.RDC_PP = True
|
||||
self.hc.http_log(None, None, None, None)
|
||||
self.assertEqual(1, self.hc.pretty_log.call_count)
|
||||
|
||||
def test_simple_log(self):
|
||||
client._logger.isEnabledFor = Mock(return_value=False)
|
||||
self.hc.simple_log(None, None, None, None)
|
||||
self.assertEqual(0, len(self.__debug_lines))
|
||||
|
||||
client._logger.isEnabledFor = Mock(return_value=True)
|
||||
se = self.side_effect_func_for_moc_debug
|
||||
client._logger.debug = Mock(side_effect=se)
|
||||
self.hc.simple_log(['item1', 'GET', 'item3', 'POST', 'item5'],
|
||||
{'headers': {'e1': 'e1-v', 'e2': 'e2-v'},
|
||||
'body': 'body'}, None, None)
|
||||
self.assertEqual(3, len(self.__debug_lines))
|
||||
self.assertTrue(self.__debug_lines[0].startswith('REQ: curl -i'))
|
||||
self.assertTrue(self.__debug_lines[1].startswith('REQ BODY:'))
|
||||
self.assertTrue(self.__debug_lines[2].startswith('RESP:'))
|
||||
|
||||
def test_pretty_log(self):
|
||||
client._logger.isEnabledFor = Mock(return_value=False)
|
||||
self.hc.pretty_log(None, None, None, None)
|
||||
self.assertEqual(0, len(self.__debug_lines))
|
||||
|
||||
client._logger.isEnabledFor = Mock(return_value=True)
|
||||
se = self.side_effect_func_for_moc_debug
|
||||
client._logger.debug = Mock(side_effect=se)
|
||||
self.hc.pretty_log(['item1', 'GET', 'item3', 'POST', 'item5'],
|
||||
{'headers': {'e1': 'e1-v', 'e2': 'e2-v'},
|
||||
'body': 'body'}, None, None)
|
||||
self.assertEqual(5, len(self.__debug_lines))
|
||||
self.assertTrue(self.__debug_lines[0].startswith('REQUEST:'))
|
||||
self.assertTrue(self.__debug_lines[1].startswith('curl -i'))
|
||||
self.assertTrue(self.__debug_lines[2].startswith('BODY:'))
|
||||
self.assertTrue(self.__debug_lines[3].startswith('RESPONSE HEADERS:'))
|
||||
self.assertTrue(self.__debug_lines[4].startswith('RESPONSE BODY'))
|
||||
|
||||
# no body case
|
||||
self.__debug_lines = list()
|
||||
self.hc.pretty_log(['item1', 'GET', 'item3', 'POST', 'item5'],
|
||||
{'headers': {'e1': 'e1-v', 'e2': 'e2-v'}},
|
||||
None, None)
|
||||
self.assertEqual(4, len(self.__debug_lines))
|
||||
self.assertTrue(self.__debug_lines[0].startswith('REQUEST:'))
|
||||
self.assertTrue(self.__debug_lines[1].startswith('curl -i'))
|
||||
self.assertTrue(self.__debug_lines[2].startswith('RESPONSE HEADERS:'))
|
||||
self.assertTrue(self.__debug_lines[3].startswith('RESPONSE BODY'))
|
||||
|
||||
def test_request(self):
|
||||
self.hc.USER_AGENT = "user-agent"
|
||||
resp = Mock()
|
||||
body = Mock()
|
||||
resp.status = 200
|
||||
httplib2.Http.request = Mock(return_value=(resp, body))
|
||||
self.hc.morph_response_body = Mock(return_value=body)
|
||||
r, b = self.hc.request()
|
||||
self.assertEqual(resp, r)
|
||||
self.assertEqual(body, b)
|
||||
self.assertEqual((resp, body), self.hc.last_response)
|
||||
|
||||
httplib2.Http.request = Mock(return_value=(resp, None))
|
||||
r, b = self.hc.request()
|
||||
self.assertEqual(resp, r)
|
||||
self.assertEqual(None, b)
|
||||
|
||||
status_list = [400, 401, 403, 404, 408, 409, 413, 500, 501]
|
||||
for status in status_list:
|
||||
resp.status = status
|
||||
self.assertRaises(Exception, self.hc.request)
|
||||
|
||||
exception = exceptions.ResponseFormatError
|
||||
self.hc.morph_response_body = Mock(side_effect=exception)
|
||||
self.assertRaises(Exception, self.hc.request)
|
||||
|
||||
def test_raise_error_from_status(self):
|
||||
resp = Mock()
|
||||
resp.status = 200
|
||||
self.hc.raise_error_from_status(resp, Mock())
|
||||
|
||||
status_list = [400, 401, 403, 404, 408, 409, 413, 500, 501]
|
||||
for status in status_list:
|
||||
resp.status = status
|
||||
self.assertRaises(Exception,
|
||||
self.hc.raise_error_from_status, resp, Mock())
|
||||
|
||||
def test_morph_request(self):
|
||||
kwargs = dict()
|
||||
kwargs['headers'] = dict()
|
||||
kwargs['body'] = ['body', {'item1': 'value1'}]
|
||||
self.hc.morph_request(kwargs)
|
||||
expected = {'body': '["body", {"item1": "value1"}]',
|
||||
'headers': {'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'}}
|
||||
self.assertEqual(expected, kwargs)
|
||||
|
||||
def test_morph_response_body(self):
|
||||
body_string = '["body", {"item1": "value1"}]'
|
||||
expected = ['body', {'item1': 'value1'}]
|
||||
self.assertEqual(expected, self.hc.morph_response_body(body_string))
|
||||
body_string = '["body", {"item1": }]'
|
||||
self.assertRaises(exceptions.ResponseFormatError,
|
||||
self.hc.morph_response_body, body_string)
|
||||
|
||||
def test__time_request(self):
|
||||
self.__time = 0
|
||||
|
||||
def side_effect_func():
|
||||
self.__time = self.__time + 1
|
||||
return self.__time
|
||||
|
||||
time.time = Mock(side_effect=side_effect_func)
|
||||
self.hc.request = Mock(return_value=("mock-response", "mock-body"))
|
||||
self.hc.times = list()
|
||||
resp, body = self.hc._time_request("test-url", "Get")
|
||||
self.assertEqual(("mock-response", "mock-body"), (resp, body))
|
||||
self.assertEqual([('Get test-url', 1, 2)], self.hc.times)
|
||||
|
||||
def mock_time_request_func(self):
|
||||
def side_effect_func(url, method, **kwargs):
|
||||
return url, method
|
||||
|
||||
self.hc._time_request = Mock(side_effect=side_effect_func)
|
||||
|
||||
def test__cs_request(self):
|
||||
self.mock_time_request_func()
|
||||
resp, body = self.hc._cs_request("test-url", "GET")
|
||||
self.assertEqual(('test-service-url/test-url', 'GET'), (resp, body))
|
||||
|
||||
self.hc.authenticate = Mock(side_effect=ValueError)
|
||||
self.hc.auth_token = None
|
||||
self.hc.service_url = None
|
||||
self.assertRaises(ValueError, self.hc._cs_request, "test-url", "GET")
|
||||
|
||||
self.hc.authenticate = Mock(return_value=None)
|
||||
self.hc.service_url = "test-service-url/"
|
||||
|
||||
def side_effect_func_time_req(url, method, **kwargs):
|
||||
raise exceptions.Unauthorized(None)
|
||||
|
||||
self.hc._time_request = Mock(side_effect=side_effect_func_time_req)
|
||||
self.assertRaises(exceptions.Unauthorized,
|
||||
self.hc._cs_request, "test-url", "GET")
|
||||
|
||||
def test_get(self):
|
||||
self.mock_time_request_func()
|
||||
resp, body = self.hc.get("test-url")
|
||||
self.assertEqual(("test-service-url/test-url", "GET"), (resp, body))
|
||||
|
||||
def test_post(self):
|
||||
self.mock_time_request_func()
|
||||
resp, body = self.hc.post("test-url")
|
||||
self.assertEqual(("test-service-url/test-url", "POST"), (resp, body))
|
||||
|
||||
def test_put(self):
|
||||
self.mock_time_request_func()
|
||||
resp, body = self.hc.put("test-url")
|
||||
self.assertEqual(("test-service-url/test-url", "PUT"), (resp, body))
|
||||
|
||||
def test_delete(self):
|
||||
self.mock_time_request_func()
|
||||
resp, body = self.hc.delete("test-url")
|
||||
self.assertEqual(("test-service-url/test-url", "DELETE"), (resp, body))
|
||||
|
||||
def test_authenticate(self):
|
||||
self.hc.authenticator = Mock()
|
||||
catalog = Mock()
|
||||
catalog.get_public_url = Mock(return_value="public-url")
|
||||
catalog.get_management_url = Mock(return_value="mng-url")
|
||||
catalog.get_token = Mock(return_value="test-token")
|
||||
|
||||
self.__auth_calls = []
|
||||
|
||||
def side_effect_func(token, url):
|
||||
self.__auth_calls = [token, url]
|
||||
|
||||
self.hc.authenticate_with_token = Mock(side_effect=side_effect_func)
|
||||
self.hc.authenticator.authenticate = Mock(return_value=catalog)
|
||||
self.hc.endpoint_type = "publicURL"
|
||||
self.hc.authenticate()
|
||||
self.assertEqual(["test-token", None],
|
||||
self.__auth_calls)
|
||||
|
||||
self.__auth_calls = []
|
||||
self.hc.service_url = None
|
||||
self.hc.authenticate()
|
||||
self.assertEqual(["test-token", "public-url"], self.__auth_calls)
|
||||
|
||||
self.__auth_calls = []
|
||||
self.hc.endpoint_type = "adminURL"
|
||||
self.hc.authenticate()
|
||||
self.assertEqual(["test-token", "mng-url"], self.__auth_calls)
|
||||
|
||||
def test_authenticate_with_token(self):
|
||||
self.hc.service_url = None
|
||||
self.assertRaises(exceptions.ServiceUrlNotGiven,
|
||||
self.hc.authenticate_with_token, "token", None)
|
||||
self.hc.authenticate_with_token("token", "test-url")
|
||||
self.assertEqual("test-url", self.hc.service_url)
|
||||
self.assertEqual("token", self.hc.auth_token)
|
||||
|
||||
|
||||
class DbaasTest(TestCase):
|
||||
def setUp(self):
|
||||
super(DbaasTest, self).setUp()
|
||||
self.orig__init = client.TroveHTTPClient.__init__
|
||||
client.TroveHTTPClient.__init__ = Mock(return_value=None)
|
||||
self.dbaas = client.Dbaas("user", "api-key")
|
||||
|
||||
def tearDown(self):
|
||||
super(DbaasTest, self).tearDown()
|
||||
client.TroveHTTPClient.__init__ = self.orig__init
|
||||
|
||||
def test___init__(self):
|
||||
client.TroveHTTPClient.__init__ = Mock(return_value=None)
|
||||
self.assertNotEqual(None, self.dbaas.mgmt)
|
||||
|
||||
def test_set_management_url(self):
|
||||
self.dbaas.set_management_url("test-management-url")
|
||||
self.assertEqual("test-management-url",
|
||||
self.dbaas.client.management_url)
|
||||
|
||||
def test_get_timings(self):
|
||||
__timings = {'start': 1, 'end': 2}
|
||||
self.dbaas.client.get_timings = Mock(return_value=__timings)
|
||||
self.assertEqual(__timings, self.dbaas.get_timings())
|
||||
|
||||
def test_authenticate(self):
|
||||
mock_auth = Mock(return_value=None)
|
||||
self.dbaas.client.authenticate = mock_auth
|
||||
self.dbaas.authenticate()
|
||||
self.assertEqual(1, mock_auth.call_count)
|
||||
def test_get_client_class_unknown(self):
|
||||
version_map = get_version_map()
|
||||
self.assertRaises(exceptions.UnsupportedVersion,
|
||||
client.BaseClient.get_class, 'database',
|
||||
'0', version_map)
|
||||
|
@ -6,7 +6,7 @@ import collections
|
||||
from testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import common
|
||||
from troveclient.compat import common
|
||||
from troveclient import client
|
||||
|
||||
"""
|
||||
@ -40,13 +40,14 @@ class CommonTest(TestCase):
|
||||
status = [400, 422, 500]
|
||||
for s in status:
|
||||
resp = Mock()
|
||||
#compat still uses status
|
||||
resp.status = s
|
||||
self.assertRaises(Exception,
|
||||
common.check_for_exceptions, resp, "body")
|
||||
|
||||
# a no-exception case
|
||||
resp = Mock()
|
||||
resp.status = 200
|
||||
resp.status_code = 200
|
||||
common.check_for_exceptions(resp, "body")
|
||||
|
||||
def test_print_actions(self):
|
||||
@ -156,26 +157,6 @@ class CommandsBaseTest(TestCase):
|
||||
def test___init__(self):
|
||||
self.assertNotEqual(None, self.cmd_base)
|
||||
|
||||
def test__get_client(self):
|
||||
client.log_to_streamhandler = Mock(return_value=None)
|
||||
expected = Mock()
|
||||
client.Dbaas = Mock(return_value=expected)
|
||||
|
||||
self.cmd_base.xml = Mock()
|
||||
self.cmd_base.verbose = False
|
||||
r = self.cmd_base._get_client()
|
||||
self.assertEqual(expected, r)
|
||||
|
||||
self.cmd_base.xml = None
|
||||
self.cmd_base.verbose = True
|
||||
r = self.cmd_base._get_client()
|
||||
self.assertEqual(expected, r)
|
||||
|
||||
# test debug true
|
||||
self.cmd_base.debug = True
|
||||
client.Dbaas = Mock(side_effect=ValueError)
|
||||
self.assertRaises(ValueError, self.cmd_base._get_client)
|
||||
|
||||
def test__safe_exec(self):
|
||||
func = Mock(return_value="test")
|
||||
self.cmd_base.debug = True
|
||||
|
@ -1,7 +1,7 @@
|
||||
from testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import instances
|
||||
from troveclient.v1 import instances
|
||||
from troveclient import base
|
||||
|
||||
"""
|
||||
@ -113,17 +113,17 @@ class InstancesTest(TestCase):
|
||||
|
||||
def test_delete(self):
|
||||
resp = Mock()
|
||||
resp.status = 200
|
||||
resp.status_code = 200
|
||||
body = None
|
||||
self.instances.api.client.delete = Mock(return_value=(resp, body))
|
||||
self.instances.delete('instance1')
|
||||
resp.status = 500
|
||||
resp.status_code = 500
|
||||
self.assertRaises(Exception, self.instances.delete, 'instance1')
|
||||
|
||||
def test__action(self):
|
||||
body = Mock()
|
||||
resp = Mock()
|
||||
resp.status = 200
|
||||
resp.status_code = 200
|
||||
self.instances.api.client.post = Mock(return_value=(resp, body))
|
||||
self.assertEqual('instance-1', self.instances._action(1, body))
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from testtools import TestCase
|
||||
from mock import Mock
|
||||
from troveclient import limits
|
||||
from troveclient.v1 import limits
|
||||
|
||||
|
||||
class LimitsTest(TestCase):
|
||||
@ -18,7 +18,7 @@ class LimitsTest(TestCase):
|
||||
|
||||
def test_list(self):
|
||||
resp = Mock()
|
||||
resp.status = 200
|
||||
resp.status_code = 200
|
||||
body = {"limits":
|
||||
[
|
||||
{'maxTotalInstances': 55,
|
||||
@ -66,7 +66,7 @@ class LimitsTest(TestCase):
|
||||
RESPONSE_KEY = "limits"
|
||||
|
||||
resp = Mock()
|
||||
resp.status = status_code
|
||||
resp.status_code = status_code
|
||||
body = {RESPONSE_KEY: {
|
||||
'absolute': {},
|
||||
'rate': [
|
||||
|
@ -1,7 +1,7 @@
|
||||
from testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import management
|
||||
from troveclient.v1 import management
|
||||
from troveclient import base
|
||||
|
||||
"""
|
||||
@ -92,10 +92,10 @@ class ManagementTest(TestCase):
|
||||
def test__action(self):
|
||||
resp = Mock()
|
||||
self.management.api.client.post = Mock(return_value=(resp, 'body'))
|
||||
resp.status = 200
|
||||
resp.status_code = 200
|
||||
self.management._action(1, 'body')
|
||||
self.assertEqual(1, self.management.api.client.post.call_count)
|
||||
resp.status = 400
|
||||
resp.status_code = 400
|
||||
self.assertRaises(Exception, self.management._action, 1, 'body')
|
||||
self.assertEqual(2, self.management.api.client.post.call_count)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import security_groups
|
||||
from troveclient.v1 import security_groups
|
||||
|
||||
"""
|
||||
Unit tests for security_groups.py
|
||||
@ -96,6 +96,6 @@ class SecGroupRuleTest(TestCase):
|
||||
self.security_group_rules.api.client.delete = \
|
||||
Mock(return_value=(resp, body))
|
||||
self.security_group_rules.delete(self.id)
|
||||
resp.status = 500
|
||||
resp.status_code = 500
|
||||
self.assertRaises(Exception, self.security_group_rules.delete,
|
||||
self.id)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from testtools import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from troveclient import users
|
||||
from troveclient.v1 import users
|
||||
from troveclient import base
|
||||
|
||||
"""
|
||||
@ -65,7 +65,7 @@ class UsersTest(TestCase):
|
||||
|
||||
def test_create(self):
|
||||
self.users.api.client.post = self._get_mock_method()
|
||||
self._resp.status = 200
|
||||
self._resp.status_code = 200
|
||||
user = self._build_fake_user('user1')
|
||||
|
||||
self.users.create(23, [user])
|
||||
@ -87,15 +87,15 @@ class UsersTest(TestCase):
|
||||
|
||||
# Make sure that response of 400 is recognized as an error.
|
||||
user['host'] = '%'
|
||||
self._resp.status = 400
|
||||
self._resp.status_code = 400
|
||||
self.assertRaises(Exception, self.users.create, 12, [user])
|
||||
|
||||
def test_delete(self):
|
||||
self.users.api.client.delete = self._get_mock_method()
|
||||
self._resp.status = 200
|
||||
self._resp.status_code = 200
|
||||
self.users.delete(27, 'user1')
|
||||
self.assertEqual('/instances/27/users/user1', self._url)
|
||||
self._resp.status = 400
|
||||
self._resp.status_code = 400
|
||||
self.assertRaises(Exception, self.users.delete, 34, 'user1')
|
||||
|
||||
def test__list(self):
|
||||
@ -109,7 +109,7 @@ class UsersTest(TestCase):
|
||||
body.__getitem__ = Mock(return_value=["test-value"])
|
||||
|
||||
resp = Mock()
|
||||
resp.status = 200
|
||||
resp.status_code = 200
|
||||
self.users.resource_class = Mock(side_effect=side_effect_func)
|
||||
self.users.api.client.get = Mock(return_value=(resp, body))
|
||||
self.assertEqual(["test-value"], self.users._list('url', key).items)
|
||||
|
@ -1,9 +1,11 @@
|
||||
from testtools import TestCase
|
||||
from lxml import etree
|
||||
from troveclient import xml
|
||||
#from troveclient import xml
|
||||
|
||||
|
||||
class XmlTest(TestCase):
|
||||
# Killing this until xml support is brought back.
|
||||
#class XmlTest(TestCase):
|
||||
class XmlTest(object):
|
||||
ELEMENT = '''
|
||||
<instances>
|
||||
<instance>
|
||||
|
@ -12,8 +12,186 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import six
|
||||
import prettytable
|
||||
|
||||
from troveclient.openstack.common.apiclient import exceptions
|
||||
from troveclient.openstack.common import strutils
|
||||
|
||||
|
||||
def arg(*args, **kwargs):
|
||||
"""Decorator for CLI args."""
|
||||
def _decorator(func):
|
||||
add_arg(func, *args, **kwargs)
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
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', '')
|
||||
|
||||
|
||||
def add_arg(f, *args, **kwargs):
|
||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||
|
||||
if not hasattr(f, 'arguments'):
|
||||
f.arguments = []
|
||||
|
||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||
# tests.
|
||||
if (args, kwargs) not in f.arguments:
|
||||
# Because of the sematics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
f.arguments.insert(0, (args, kwargs))
|
||||
|
||||
|
||||
def unauthenticated(f):
|
||||
"""
|
||||
Adds 'unauthenticated' attribute to decorated function.
|
||||
Usage:
|
||||
@unauthenticated
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
f.unauthenticated = True
|
||||
return f
|
||||
|
||||
|
||||
def isunauthenticated(f):
|
||||
"""
|
||||
Checks to see if the function is marked as not requiring authentication
|
||||
with the @unauthenticated decorator. Returns True if decorator is
|
||||
set to True, False otherwise.
|
||||
"""
|
||||
return getattr(f, 'unauthenticated', False)
|
||||
|
||||
|
||||
def service_type(stype):
|
||||
"""
|
||||
Adds 'service_type' attribute to decorated function.
|
||||
Usage:
|
||||
@service_type('database')
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
def inner(f):
|
||||
f.service_type = stype
|
||||
return f
|
||||
return inner
|
||||
|
||||
|
||||
def get_service_type(f):
|
||||
"""
|
||||
Retrieves service type from function
|
||||
"""
|
||||
return getattr(f, 'service_type', None)
|
||||
|
||||
|
||||
def translate_keys(collection, convert):
|
||||
for item in collection:
|
||||
keys = list(item.__dict__.keys())
|
||||
for from_key, to_key in convert:
|
||||
if from_key in keys and to_key not in keys:
|
||||
setattr(item, to_key, item._info[from_key])
|
||||
|
||||
|
||||
def _print(pt, order):
|
||||
if sys.version_info >= (3, 0):
|
||||
print(pt.get_string(sortby=order))
|
||||
else:
|
||||
print(strutils.safe_encode(pt.get_string(sortby=order)))
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters={}, order_by=None):
|
||||
mixed_case_fields = []
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.aligns = ['l' for f in fields]
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
if field in mixed_case_fields:
|
||||
field_name = field.replace(' ', '_')
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
if order_by is None:
|
||||
order_by = fields[0]
|
||||
_print(pt, order_by)
|
||||
|
||||
|
||||
def print_dict(d, property="Property"):
|
||||
pt = prettytable.PrettyTable([property, 'Value'], caching=False)
|
||||
pt.aligns = ['l', 'l']
|
||||
[pt.add_row(list(r)) for r in six.iteritems(d)]
|
||||
_print(pt, property)
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||
return manager.get(int(name_or_id))
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
if sys.version_info <= (3, 0):
|
||||
name_or_id = strutils.safe_decode(name_or_id)
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
uuid.UUID(name_or_id)
|
||||
return manager.get(name_or_id)
|
||||
except (ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
return manager.find(human_id=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
# finally try to find entity by name
|
||||
try:
|
||||
return manager.find(name=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
try:
|
||||
return manager.find(display_name=name_or_id)
|
||||
except (UnicodeDecodeError, exceptions.NotFound):
|
||||
try:
|
||||
# Instances does not have name, but display_name
|
||||
return manager.find(display_name=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
msg = "No %s with a name or ID of '%s' exists." % \
|
||||
(manager.resource_class.__name__.lower(), name_or_id)
|
||||
raise exceptions.CommandError(msg)
|
||||
except exceptions.NoUniqueMatch:
|
||||
msg = ("Multiple %s matches found for '%s', use an ID to be more"
|
||||
" specific." % (manager.resource_class.__name__.lower(),
|
||||
name_or_id))
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
|
||||
class HookableMixin(object):
|
||||
@ -34,18 +212,6 @@ class HookableMixin(object):
|
||||
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]+')
|
||||
|
||||
|
0
troveclient/v1/__init__.py
Normal file
0
troveclient/v1/__init__.py
Normal file
@ -58,6 +58,10 @@ class Accounts(base.ManagerWithFind):
|
||||
acct_name = self._get_account_name(account)
|
||||
return self._list("/mgmt/accounts/%s" % acct_name, 'account')
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _get_account_name(account):
|
||||
try:
|
@ -70,5 +70,5 @@ class Backups(base.ManagerWithFind):
|
||||
:param backup_id: The backup id to delete
|
||||
"""
|
||||
resp, body = self.api.client.delete("/backups/%s" % backup_id)
|
||||
if resp.status in (422, 500):
|
||||
if resp.status_code in (422, 500):
|
||||
raise exceptions.from_response(resp, body)
|
105
troveclient/v1/client.py
Normal file
105
troveclient/v1/client.py
Normal file
@ -0,0 +1,105 @@
|
||||
from troveclient import client
|
||||
from troveclient.v1.databases import Databases
|
||||
from troveclient.v1.flavors import Flavors
|
||||
from troveclient.v1.instances import Instances
|
||||
from troveclient.v1.limits import Limits
|
||||
from troveclient.v1.users import Users
|
||||
from troveclient.v1.root import Root
|
||||
from troveclient.v1.hosts import Hosts
|
||||
from troveclient.v1.quota import Quotas
|
||||
from troveclient.v1.backups import Backups
|
||||
from troveclient.v1.security_groups import SecurityGroups
|
||||
from troveclient.v1.security_groups import SecurityGroupRules
|
||||
from troveclient.v1.storage import StorageInfo
|
||||
from troveclient.v1.management import Management
|
||||
from troveclient.v1.management import MgmtFlavors
|
||||
from troveclient.v1.accounts import Accounts
|
||||
from troveclient.v1.diagnostics import DiagnosticsInterrogator
|
||||
from troveclient.v1.diagnostics import HwInfoInterrogator
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
Top-level object to access the OpenStack Database API.
|
||||
|
||||
Create an instance with your creds::
|
||||
|
||||
>>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>>> client.instances.list()
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, username, password, project_id=None, auth_url='',
|
||||
insecure=False, timeout=None, tenant_id=None,
|
||||
proxy_tenant_id=None, proxy_token=None, region_name=None,
|
||||
endpoint_type='publicURL', extensions=None,
|
||||
service_type='database', service_name=None,
|
||||
database_service_name=None, retries=None,
|
||||
http_log_debug=False,
|
||||
cacert=None):
|
||||
# self.limits = limits.LimitsManager(self)
|
||||
|
||||
# extensions
|
||||
self.flavors = Flavors(self)
|
||||
self.users = Users(self)
|
||||
self.databases = Databases(self)
|
||||
self.backups = Backups(self)
|
||||
self.instances = Instances(self)
|
||||
self.limits = Limits(self)
|
||||
self.root = Root(self)
|
||||
self.security_group_rules = SecurityGroupRules(self)
|
||||
self.security_groups = SecurityGroups(self)
|
||||
|
||||
#self.hosts = Hosts(self)
|
||||
#self.quota = Quotas(self)
|
||||
#self.storage = StorageInfo(self)
|
||||
#self.management = Management(self)
|
||||
#self.mgmt_flavor = MgmtFlavors(self)
|
||||
#self.accounts = Accounts(self)
|
||||
#self.diagnostics = DiagnosticsInterrogator(self)
|
||||
#self.hwinfo = HwInfoInterrogator(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
for extension in extensions:
|
||||
if extension.manager_class:
|
||||
setattr(self, extension.name,
|
||||
extension.manager_class(self))
|
||||
|
||||
self.client = client.HTTPClient(
|
||||
username,
|
||||
password,
|
||||
project_id,
|
||||
auth_url,
|
||||
insecure=insecure,
|
||||
timeout=timeout,
|
||||
tenant_id=tenant_id,
|
||||
proxy_token=proxy_token,
|
||||
proxy_tenant_id=proxy_tenant_id,
|
||||
region_name=region_name,
|
||||
endpoint_type=endpoint_type,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
database_service_name=database_service_name,
|
||||
retries=retries,
|
||||
http_log_debug=http_log_debug,
|
||||
cacert=cacert)
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Authenticate against the server.
|
||||
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
||||
|
||||
def get_database_api_version_from_endpoint(self):
|
||||
return self.client.get_database_api_version_from_endpoint()
|
@ -37,6 +37,10 @@ class DiagnosticsInterrogator(base.ManagerWithFind):
|
||||
return self._get("/mgmt/instances/%s/diagnostics" %
|
||||
base.getid(instance), "diagnostics")
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
|
||||
class HwInfo(base.Resource):
|
||||
|
||||
@ -55,3 +59,7 @@ class HwInfoInterrogator(base.ManagerWithFind):
|
||||
Get the hardware information of the instance.
|
||||
"""
|
||||
return self._get("/mgmt/instances/%s/hwinfo" % base.getid(instance))
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
@ -32,15 +32,6 @@ class Flavors(base.ManagerWithFind):
|
||||
"""
|
||||
resource_class = Flavor
|
||||
|
||||
def __repr__(self):
|
||||
return "<Flavors Manager at %s>" % id(self)
|
||||
|
||||
def _list(self, url, response_key):
|
||||
resp, body = self.api.client.get(url)
|
||||
if not body:
|
||||
raise Exception("Call to " + url + " did not return a body.")
|
||||
return [self.resource_class(self, res) for res in body[response_key]]
|
||||
|
||||
def list(self):
|
||||
"""
|
||||
Get a list of all flavors.
|
@ -76,3 +76,7 @@ class Hosts(base.ManagerWithFind):
|
||||
return host.name
|
||||
except AttributeError:
|
||||
return host
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
@ -127,7 +127,7 @@ class Instances(base.ManagerWithFind):
|
||||
"""
|
||||
resp, body = self.api.client.delete("/instances/%s" %
|
||||
base.getid(instance))
|
||||
if resp.status in (422, 500):
|
||||
if resp.status_code in (422, 500):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
def _action(self, instance_id, body):
|
@ -35,7 +35,7 @@ class Limits(base.ManagerWithFind):
|
||||
def _list(self, url, response_key):
|
||||
resp, body = self.api.client.get(url)
|
||||
|
||||
if resp is None or resp.status != 200:
|
||||
if resp is None or resp.status_code != 200:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
if not body:
|
@ -19,8 +19,8 @@ import urlparse
|
||||
from troveclient.common import check_for_exceptions
|
||||
from troveclient.common import limit_url
|
||||
from troveclient.common import Paginated
|
||||
from troveclient.instances import Instance
|
||||
from troveclient.flavors import Flavor
|
||||
from troveclient.v1.instances import Instance
|
||||
from troveclient.v1.flavors import Flavor
|
||||
|
||||
|
||||
class RootHistory(base.Resource):
|
||||
@ -35,6 +35,10 @@ class Management(base.ManagerWithFind):
|
||||
"""
|
||||
resource_class = Instance
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def _list(self, url, response_key, limit=None, marker=None):
|
||||
resp, body = self.api.client.get(limit_url(url, limit, marker))
|
||||
if not body:
|
||||
@ -146,6 +150,10 @@ class MgmtFlavors(base.ManagerWithFind):
|
||||
def __repr__(self):
|
||||
return "<Flavors Manager at %s>" % id(self)
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def create(self, name, ram, disk, vcpus,
|
||||
flavorid="auto", ephemeral=None, swap=None, rxtx_factor=None,
|
||||
service_type=None):
|
@ -49,3 +49,7 @@ class Quotas(base.ManagerWithFind):
|
||||
if 'quotas' not in body:
|
||||
raise Exception("Missing key value 'quotas' in response body.")
|
||||
return body['quotas']
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
@ -15,7 +15,7 @@
|
||||
|
||||
from troveclient import base
|
||||
|
||||
from troveclient import users
|
||||
from troveclient.v1 import users
|
||||
from troveclient.common import check_for_exceptions
|
||||
|
||||
|
||||
@ -41,3 +41,7 @@ class Root(base.ManagerWithFind):
|
||||
resp, body = self.api.client.get(self.url % instance_id)
|
||||
check_for_exceptions(resp, body)
|
||||
return self.resource_class(self, body, loaded=True)
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
@ -116,5 +116,9 @@ class SecurityGroupRules(base.ManagerWithFind):
|
||||
"""
|
||||
resp, body = self.api.client.delete("/security-group-rules/%s" %
|
||||
base.getid(security_group_rule))
|
||||
if resp.status in (422, 500):
|
||||
if resp.status_code in (422, 500):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
492
troveclient/v1/shell.py
Normal file
492
troveclient/v1/shell.py
Normal file
@ -0,0 +1,492 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from troveclient import exceptions
|
||||
from troveclient import utils
|
||||
|
||||
|
||||
def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
|
||||
poll_period=5, show_progress=True):
|
||||
"""Block while an action is being performed, periodically printing
|
||||
progress.
|
||||
"""
|
||||
def print_progress(progress):
|
||||
if show_progress:
|
||||
msg = ('\rInstance %(action)s... %(progress)s%% complete'
|
||||
% dict(action=action, progress=progress))
|
||||
else:
|
||||
msg = '\rInstance %(action)s...' % dict(action=action)
|
||||
|
||||
sys.stdout.write(msg)
|
||||
sys.stdout.flush()
|
||||
|
||||
print()
|
||||
while True:
|
||||
obj = poll_fn(obj_id)
|
||||
status = obj.status.lower()
|
||||
progress = getattr(obj, 'progress', None) or 0
|
||||
if status in final_ok_states:
|
||||
print_progress(100)
|
||||
print("\nFinished")
|
||||
break
|
||||
elif status == "error":
|
||||
print("\nError %(action)s instance" % {'action': action})
|
||||
break
|
||||
else:
|
||||
print_progress(progress)
|
||||
time.sleep(poll_period)
|
||||
|
||||
|
||||
def _print_instance(instance):
|
||||
# Get rid of those ugly links
|
||||
if instance._info.get('links'):
|
||||
del(instance._info['links'])
|
||||
utils.print_dict(instance._info)
|
||||
|
||||
|
||||
def _find_instance(cs, instance):
|
||||
"""Get a instance by ID."""
|
||||
return utils.find_resource(cs.instances, instance)
|
||||
|
||||
|
||||
def _find_flavor(cs, flavor):
|
||||
"""Get a flavor by ID."""
|
||||
return utils.find_resource(cs.flavors, flavor)
|
||||
|
||||
|
||||
def _find_backup(cs, backup):
|
||||
"""Gets a backup by ID."""
|
||||
return utils.find_resource(cs.backups, backup)
|
||||
|
||||
|
||||
# Flavor related calls
|
||||
|
||||
@utils.service_type('database')
|
||||
def do_list_flavors(cs, args):
|
||||
"""Lists available flavors."""
|
||||
flavors = cs.flavors.list()
|
||||
utils.print_list(flavors, ['id', 'name', 'ram'])
|
||||
|
||||
|
||||
@utils.arg('flavor', metavar='<flavor>', help='ID of the flavor.')
|
||||
@utils.service_type('database')
|
||||
def do_show_flavor(cs, args):
|
||||
"""Show details of a flavor."""
|
||||
flavor = _find_flavor(cs, args.flavor)
|
||||
_print_instance(flavor)
|
||||
|
||||
|
||||
# Instance related calls
|
||||
|
||||
@utils.service_type('database')
|
||||
def do_list(cs, args):
|
||||
"""List all the instances."""
|
||||
instances = cs.instances.list()
|
||||
|
||||
for instance in instances:
|
||||
setattr(instance, 'flavor_id', instance.flavor['id'])
|
||||
if hasattr(instance, 'volume'):
|
||||
setattr(instance, 'size', instance.volume['size'])
|
||||
utils.print_list(instances, ['id', 'name', 'status', 'flavor_id', 'size'])
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='ID of the instance.')
|
||||
@utils.service_type('database')
|
||||
def do_show(cs, args):
|
||||
"""Show details of an instance."""
|
||||
instance = _find_instance(cs, args.instance)
|
||||
instance._info['flavor'] = instance.flavor['id']
|
||||
if hasattr(instance, 'volume'):
|
||||
instance._info['volume'] = instance.volume['size']
|
||||
|
||||
_print_instance(instance)
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='ID of the instance.')
|
||||
@utils.service_type('database')
|
||||
def do_delete(cs, args):
|
||||
"""Deletes an instance."""
|
||||
cs.instances.delete(args.instance)
|
||||
|
||||
|
||||
@utils.arg('name',
|
||||
metavar='<name>',
|
||||
type=str,
|
||||
help='Name of the instance')
|
||||
@utils.arg('--size',
|
||||
metavar='<size>',
|
||||
type=int,
|
||||
default=None,
|
||||
help='Size of the instance disk in GB')
|
||||
@utils.arg('flavor_id',
|
||||
metavar='<flavor_id>',
|
||||
help='Flavor of the instance')
|
||||
@utils.arg('--databases', metavar='<databases>',
|
||||
help='Optional list of databases.',
|
||||
nargs="+", default=[])
|
||||
@utils.arg('--users', metavar='<users>',
|
||||
help='Optional list of users in the form user:password.',
|
||||
nargs="+", default=[])
|
||||
@utils.arg('--backup',
|
||||
metavar='<backup>',
|
||||
default=None,
|
||||
help='A backup UUID')
|
||||
@utils.arg('--availability_zone',
|
||||
metavar='<availability_zone>',
|
||||
default=None,
|
||||
help='The Zone hint to give to nova')
|
||||
@utils.service_type('database')
|
||||
def do_create(cs, args):
|
||||
"""Add a new instance."""
|
||||
volume = None
|
||||
if args.size:
|
||||
volume = {"size": args.size}
|
||||
restore_point = None
|
||||
if args.backup:
|
||||
restore_point = {"backupRef": self.backup}
|
||||
databases = [{'name': value} for value in args.databases]
|
||||
users = [{'name': n, 'password': p} for (n, p) in
|
||||
[z.split(':')[:2] for z in args.users]]
|
||||
instance = cs.instances.create(args.name,
|
||||
args.flavor_id,
|
||||
volume=volume,
|
||||
databases=databases,
|
||||
users=users,
|
||||
restorePoint=restore_point,
|
||||
availability_zone=args.availability_zone)
|
||||
instance._info['flavor'] = instance.flavor['id']
|
||||
if hasattr(instance, 'volume'):
|
||||
instance._info['volume'] = instance.volume['size']
|
||||
del(instance._info['links'])
|
||||
|
||||
_print_instance(instance)
|
||||
|
||||
|
||||
@utils.arg('instance',
|
||||
metavar='<instance>',
|
||||
type=str,
|
||||
help='UUID of the instance')
|
||||
@utils.arg('flavor_id',
|
||||
metavar='<flavor_id>',
|
||||
help='Flavor of the instance')
|
||||
def resize_flavor(cs, args):
|
||||
"""Resizes the flavor of an instance."""
|
||||
cs.instances.resize_flavor(args.instance, args.flavor_id)
|
||||
|
||||
|
||||
@utils.arg('instance',
|
||||
metavar='<instance>',
|
||||
type=str,
|
||||
help='UUID of the instance')
|
||||
@utils.arg('size',
|
||||
metavar='<size>',
|
||||
type=int,
|
||||
default=None,
|
||||
help='Size of the instance disk in GB')
|
||||
def resize_volume(cs, args):
|
||||
"""Resizes the volume size of an instance."""
|
||||
cs.instances.resize_volume(args.instance, args.size)
|
||||
|
||||
|
||||
@utils.arg('instance',
|
||||
metavar='<instance>',
|
||||
type=str,
|
||||
help='UUID of the instance')
|
||||
def restart(cs, args):
|
||||
"""Restarts the instance."""
|
||||
cs.instances.restart(args.instance)
|
||||
|
||||
|
||||
# Backup related commands
|
||||
|
||||
@utils.arg('backup', metavar='<backup>', help='ID of the backup.')
|
||||
@utils.service_type('database')
|
||||
def do_show_backup(cs, args):
|
||||
"""Show details of a backup."""
|
||||
backups = _find_backup(args.backup)
|
||||
_print_instance(backup)
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='ID of the instance.')
|
||||
@utils.service_type('database')
|
||||
def do_list_instance_backups(cs, args):
|
||||
"""List available backups for an instance."""
|
||||
backups = cs.instances.backups(args.instance)
|
||||
utils.print_list(backups, ['id', 'instance_id',
|
||||
'name', 'description', 'status'])
|
||||
|
||||
|
||||
@utils.service_type('database')
|
||||
def do_list_backups(cs, args):
|
||||
"""List available backups."""
|
||||
backups = cs.backups.list()
|
||||
utils.print_list(backups, ['id', 'instance_id',
|
||||
'name', 'description', 'status'])
|
||||
|
||||
|
||||
@utils.arg('backup', metavar='<backup>', help='ID of the backup.')
|
||||
@utils.service_type('database')
|
||||
def do_delete_backup(cs, args):
|
||||
"""Deletes a backup."""
|
||||
cs.backups.delete(args.backup)
|
||||
|
||||
|
||||
@utils.arg('name', metavar='<name>', help='Name of the backup.')
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('--description', metavar='<description>',
|
||||
default=None,
|
||||
help='An optional description for the backup.')
|
||||
@utils.service_type('database')
|
||||
def do_create_backup(cs, args):
|
||||
"""Deletes a backup."""
|
||||
backup = cs.backups.create(args.name, args.instance,
|
||||
description=args.description)
|
||||
_print_instance(backup)
|
||||
|
||||
|
||||
# Database related actions
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of the backup.')
|
||||
@utils.arg('--character_set', metavar='<character_set>',
|
||||
default=None,
|
||||
help='Optional character set for database')
|
||||
@utils.arg('--collate', metavar='<collate>', default=None,
|
||||
help='Optional collation type for database')
|
||||
@utils.service_type('database')
|
||||
def do_create_database(cs, args):
|
||||
"""Creates a database on an instance."""
|
||||
database_dict = {'name': args.name}
|
||||
if args.collate:
|
||||
database_dict['collate'] = args.collate
|
||||
if args.character_set:
|
||||
database_dict['character_set'] = args.character_set
|
||||
cs.databases.create(args.instance,
|
||||
[database_dict])
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.service_type('database')
|
||||
def do_list_databases(cs, args):
|
||||
"""Lists available databases on an instance."""
|
||||
wrapper = cs.databases.list(args.instance)
|
||||
databases = wrapper.items
|
||||
while (wrapper.next):
|
||||
wrapper = cs.databases.list(args.instance, marker=wrapper.next)
|
||||
databases = wrapper.items
|
||||
|
||||
utils.print_list(databases, ['name'])
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('database', metavar='<database>', help='Name of the database.')
|
||||
@utils.service_type('database')
|
||||
def do_delete_database(cs, args):
|
||||
"""Deletes a database."""
|
||||
cs.databases.delete(args.instance, args.database)
|
||||
|
||||
|
||||
# User related actions
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of user')
|
||||
@utils.arg('password', metavar='<password>', help='Password of user')
|
||||
@utils.arg('--host', metavar='<host>', default=None,
|
||||
help='Optional host of user')
|
||||
@utils.arg('--databases', metavar='<databases>',
|
||||
help='Optional list of databases.',
|
||||
nargs="+", default=[])
|
||||
@utils.service_type('database')
|
||||
def do_create_user(cs, args):
|
||||
"""Creates a user."""
|
||||
databases = [{'name': value} for value in args.databases]
|
||||
user = {'name': args.name, 'password': args.password,
|
||||
'databases': databases}
|
||||
if args.host:
|
||||
user['host'] = args.host
|
||||
cs.users.create(args.instance, [user])
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.service_type('database')
|
||||
def do_list_users(cs, args):
|
||||
"""Lists the users for a instance."""
|
||||
wrapper = cs.users.list(args.instance)
|
||||
users = wrapper.items
|
||||
while (wrapper.next):
|
||||
wrapper = cs.users.list(args.instance, marker=wrapper.next)
|
||||
users += wrapper.items
|
||||
|
||||
utils.print_list(users, ['name', 'host', 'databases'])
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of user')
|
||||
@utils.arg('--host', metavar='<host>', default=None,
|
||||
help='Optional host of user')
|
||||
@utils.service_type('database')
|
||||
def do_delete_user(cs, args):
|
||||
"""Deletes a user from the instance."""
|
||||
cs.users.delete(args.instance, args.name, hostname=args.host)
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of user')
|
||||
@utils.arg('--host', metavar='<host>', default=None,
|
||||
help='Optional host of user')
|
||||
@utils.service_type('database')
|
||||
# Quoting is not working now that we arent using httplib2
|
||||
# anymore and instead are using requests
|
||||
def do_get_user(cs, args):
|
||||
"""Gets a user from the instance."""
|
||||
user = cs.users.get(args.instance, args.name, hostname=args.host)
|
||||
_print_instance(user)
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of user')
|
||||
@utils.arg('--host', metavar='<host>', default=None,
|
||||
help='Optional host of user')
|
||||
@utils.service_type('database')
|
||||
# Quoting is not working now that we arent using httplib2
|
||||
# anymore and instead are using requests
|
||||
def do_get_user_access(cs, args):
|
||||
"""Gets a users access from the instance."""
|
||||
access = cs.users.list_access(args.instance, args.name, hostname=args.host)
|
||||
utils.print_list(access, ['name'])
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of user')
|
||||
@utils.arg('--host', metavar='<host>', default=None,
|
||||
help='Optional host of user')
|
||||
@utils.arg('--new_name', metavar='<new_name>', default=None,
|
||||
help='Optional new name of user')
|
||||
@utils.arg('--new_password', metavar='<new_password>', default=None,
|
||||
help='Optional new password of user')
|
||||
@utils.arg('--new_host', metavar='<new_host>', default=None,
|
||||
help='Optional new host of user')
|
||||
@utils.service_type('database')
|
||||
# Quoting is not working now that we arent using httplib2
|
||||
# anymore and instead are using requests
|
||||
def do_update_user_attributes(cs, args):
|
||||
"""Updates a users attributes from the instance."""
|
||||
new_attrs = {}
|
||||
if args.new_name:
|
||||
new_attrs['name'] = args.new_name
|
||||
if args.new_password:
|
||||
new_attrs['password'] = args.new_password
|
||||
if args.new_host:
|
||||
new_attrs['host'] = args.new_host
|
||||
cs.users.update_attributes(args.instance, args.name,
|
||||
newuserattr=new_attrs, hostname=args.host)
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of user')
|
||||
@utils.arg('--host', metavar='<host>', default=None,
|
||||
help='Optional host of user')
|
||||
@utils.arg('databases', metavar='<databases>',
|
||||
help='List of databases.',
|
||||
nargs="+", default=[])
|
||||
@utils.service_type('database')
|
||||
def do_grant_user_access(cs, args):
|
||||
"""Grants access to a atabase(s) for a user."""
|
||||
cs.users.grant(args.instance, args.name,
|
||||
args.databases, hostname=args.host)
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.arg('name', metavar='<name>', help='Name of user')
|
||||
@utils.arg('database', metavar='<database>', help='A single database.')
|
||||
@utils.arg('--host', metavar='<host>', default=None,
|
||||
help='Optional host of user')
|
||||
@utils.service_type('database')
|
||||
def do_revoke_user_access(cs, args):
|
||||
"""Revokes access to a database for a user."""
|
||||
cs.users.revoke(args.instance, args.name,
|
||||
args.database, hostname=args.host)
|
||||
|
||||
|
||||
# Limits related commands
|
||||
|
||||
@utils.service_type('database')
|
||||
def do_list_limits(cs, args):
|
||||
"""Lists the limits for a tenant."""
|
||||
limits = cs.limits.list()
|
||||
# Pop the first one, its absolute limits
|
||||
absolute = limits.pop(0)
|
||||
_print_instance(absolute)
|
||||
utils.print_list(limits, ['value', 'verb', 'remaining', 'unit'])
|
||||
|
||||
|
||||
# Root related commands
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.service_type('database')
|
||||
def do_enable_root(cs, args):
|
||||
"""Enables root for a instance."""
|
||||
root = cs.root.create(args.instance)
|
||||
utils.print_dict({'name': root[0], 'password': root[1]})
|
||||
|
||||
|
||||
@utils.arg('instance', metavar='<instance>', help='UUID of the instance.')
|
||||
@utils.service_type('database')
|
||||
def do_get_root(cs, args):
|
||||
"""Gets root enabled status for a instance."""
|
||||
root = cs.root.is_root_enabled(args.instance)
|
||||
utils.print_dict({'is_root_enabled': root.rootEnabled})
|
||||
|
||||
|
||||
# security group related functions
|
||||
|
||||
@utils.service_type('database')
|
||||
def do_list_security_groups(cs, args):
|
||||
"""Lists all security gropus."""
|
||||
wrapper = cs.security_groups.list()
|
||||
sec_grps = wrapper.items
|
||||
while (wrapper.next):
|
||||
wrapper = cs.security_groups.list()
|
||||
sec_grps += wrapper.items
|
||||
|
||||
utils.print_list(sec_grps, ['id', 'name', 'rules', 'instance_id'])
|
||||
|
||||
|
||||
@utils.arg('security_group', metavar='<security_group>',
|
||||
help='ID of the security group.')
|
||||
@utils.service_type('database')
|
||||
def do_show_security_group(cs, args):
|
||||
"""Shows details about a security group."""
|
||||
sec_grp = cs.security_groups.get(args.security_group)
|
||||
_print_instance(sec_grp)
|
||||
|
||||
|
||||
@utils.arg('security_group', metavar='<security_group>',
|
||||
help='Security group name')
|
||||
@utils.arg('protocol', metavar='<protocol>', help='Protocol')
|
||||
@utils.arg('from_port', metavar='<from_port>', help='from port')
|
||||
@utils.arg('to_port', metavar='<to_port>', help='to port')
|
||||
@utils.arg('cidr', metavar='<cidr>', help='CIDR address')
|
||||
@utils.service_type('database')
|
||||
def do_create_security_group_rule(cs, args):
|
||||
"""Creates a security group rule."""
|
||||
rule = cs.security_group_rules.create(args.security_group,
|
||||
args.protocol,
|
||||
args.from_port,
|
||||
args.to_port,
|
||||
args.cidr)
|
||||
|
||||
_print_instance(rule)
|
||||
|
||||
|
||||
@utils.arg('security_group_rule', metavar='<security_group_rule>',
|
||||
help='Security group rule')
|
||||
@utils.service_type('database')
|
||||
def do_delete_security_group_rule(cs, args):
|
||||
"""Deletes a security group rule."""
|
||||
cs.security_group_rules.delete(args.security_group_rule)
|
@ -43,3 +43,7 @@ class StorageInfo(base.ManagerWithFind):
|
||||
:rtype: list of :class:`Storages`.
|
||||
"""
|
||||
return self._list("/mgmt/storage", "devices")
|
||||
|
||||
# Appease the abc gods
|
||||
def list(self):
|
||||
pass
|
@ -14,7 +14,7 @@
|
||||
# under the License.
|
||||
|
||||
from troveclient import base
|
||||
from troveclient import databases
|
||||
from troveclient.v1 import databases
|
||||
from troveclient.common import check_for_exceptions
|
||||
from troveclient.common import limit_url
|
||||
from troveclient.common import Paginated
|
Loading…
x
Reference in New Issue
Block a user