From 9916c8f2733b683d859770d05dacd2c9c82912d7 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Mon, 17 Jun 2013 23:34:27 -0700 Subject: Rename from reddwarf to trove. Implements Blueprint reddwarf-trove-rename Change-Id: Ib2d694c7466887ca297bea4250eca17cdc06b7bf --- .testr.conf | 2 +- README.rst | 10 +- reddwarfclient/__init__.py | 31 --- reddwarfclient/accounts.py | 67 ----- reddwarfclient/auth.py | 269 ------------------- reddwarfclient/backups.py | 71 ----- reddwarfclient/base.py | 293 --------------------- reddwarfclient/cli.py | 385 --------------------------- reddwarfclient/client.py | 370 -------------------------- reddwarfclient/common.py | 406 ----------------------------- reddwarfclient/databases.py | 79 ------ reddwarfclient/diagnostics.py | 58 ----- reddwarfclient/exceptions.py | 179 ------------- reddwarfclient/flavors.py | 62 ----- reddwarfclient/hosts.py | 78 ------ reddwarfclient/instances.py | 185 ------------- reddwarfclient/limits.py | 50 ---- reddwarfclient/management.py | 136 ---------- reddwarfclient/mcli.py | 246 ------------------ reddwarfclient/quota.py | 51 ---- reddwarfclient/root.py | 44 ---- reddwarfclient/security_groups.py | 120 --------- reddwarfclient/storage.py | 45 ---- reddwarfclient/tests/test_accounts.py | 84 ------ reddwarfclient/tests/test_auth.py | 414 ----------------------------- reddwarfclient/tests/test_base.py | 447 -------------------------------- reddwarfclient/tests/test_client.py | 322 ----------------------- reddwarfclient/tests/test_common.py | 395 ---------------------------- reddwarfclient/tests/test_instances.py | 176 ------------- reddwarfclient/tests/test_limits.py | 79 ------ reddwarfclient/tests/test_management.py | 144 ---------- reddwarfclient/tests/test_secgroups.py | 102 -------- reddwarfclient/tests/test_users.py | 126 --------- reddwarfclient/tests/test_utils.py | 41 --- reddwarfclient/tests/test_xml.py | 241 ----------------- reddwarfclient/users.py | 127 --------- reddwarfclient/utils.py | 68 ----- reddwarfclient/versions.py | 41 --- reddwarfclient/xml.py | 293 --------------------- run_local.sh | 18 +- setup.cfg | 6 +- test-requirements.txt | 4 +- troveclient/__init__.py | 31 +++ troveclient/accounts.py | 67 +++++ troveclient/auth.py | 269 +++++++++++++++++++ troveclient/backups.py | 71 +++++ troveclient/base.py | 293 +++++++++++++++++++++ troveclient/cli.py | 385 +++++++++++++++++++++++++++ troveclient/client.py | 370 ++++++++++++++++++++++++++ troveclient/common.py | 406 +++++++++++++++++++++++++++++ troveclient/databases.py | 79 ++++++ troveclient/diagnostics.py | 58 +++++ troveclient/exceptions.py | 179 +++++++++++++ troveclient/flavors.py | 62 +++++ troveclient/hosts.py | 78 ++++++ troveclient/instances.py | 185 +++++++++++++ troveclient/limits.py | 50 ++++ troveclient/management.py | 136 ++++++++++ troveclient/mcli.py | 246 ++++++++++++++++++ troveclient/quota.py | 51 ++++ troveclient/root.py | 44 ++++ troveclient/security_groups.py | 120 +++++++++ troveclient/storage.py | 45 ++++ troveclient/tests/__init__.py | 0 troveclient/tests/test_accounts.py | 84 ++++++ troveclient/tests/test_auth.py | 414 +++++++++++++++++++++++++++++ troveclient/tests/test_base.py | 447 ++++++++++++++++++++++++++++++++ troveclient/tests/test_client.py | 322 +++++++++++++++++++++++ troveclient/tests/test_common.py | 395 ++++++++++++++++++++++++++++ troveclient/tests/test_instances.py | 176 +++++++++++++ troveclient/tests/test_limits.py | 79 ++++++ troveclient/tests/test_management.py | 144 ++++++++++ troveclient/tests/test_secgroups.py | 102 ++++++++ troveclient/tests/test_users.py | 126 +++++++++ troveclient/tests/test_utils.py | 41 +++ troveclient/tests/test_xml.py | 241 +++++++++++++++++ troveclient/users.py | 127 +++++++++ troveclient/utils.py | 68 +++++ troveclient/versions.py | 41 +++ troveclient/xml.py | 293 +++++++++++++++++++++ 80 files changed, 6344 insertions(+), 6346 deletions(-) delete mode 100644 reddwarfclient/__init__.py delete mode 100644 reddwarfclient/accounts.py delete mode 100644 reddwarfclient/auth.py delete mode 100644 reddwarfclient/backups.py delete mode 100644 reddwarfclient/base.py delete mode 100644 reddwarfclient/cli.py delete mode 100644 reddwarfclient/client.py delete mode 100644 reddwarfclient/common.py delete mode 100644 reddwarfclient/databases.py delete mode 100644 reddwarfclient/diagnostics.py delete mode 100644 reddwarfclient/exceptions.py delete mode 100644 reddwarfclient/flavors.py delete mode 100644 reddwarfclient/hosts.py delete mode 100644 reddwarfclient/instances.py delete mode 100644 reddwarfclient/limits.py delete mode 100644 reddwarfclient/management.py delete mode 100644 reddwarfclient/mcli.py delete mode 100644 reddwarfclient/quota.py delete mode 100644 reddwarfclient/root.py delete mode 100644 reddwarfclient/security_groups.py delete mode 100644 reddwarfclient/storage.py delete mode 100644 reddwarfclient/tests/test_accounts.py delete mode 100644 reddwarfclient/tests/test_auth.py delete mode 100644 reddwarfclient/tests/test_base.py delete mode 100644 reddwarfclient/tests/test_client.py delete mode 100644 reddwarfclient/tests/test_common.py delete mode 100644 reddwarfclient/tests/test_instances.py delete mode 100644 reddwarfclient/tests/test_limits.py delete mode 100644 reddwarfclient/tests/test_management.py delete mode 100644 reddwarfclient/tests/test_secgroups.py delete mode 100644 reddwarfclient/tests/test_users.py delete mode 100644 reddwarfclient/tests/test_utils.py delete mode 100644 reddwarfclient/tests/test_xml.py delete mode 100644 reddwarfclient/users.py delete mode 100644 reddwarfclient/utils.py delete mode 100644 reddwarfclient/versions.py delete mode 100644 reddwarfclient/xml.py create mode 100644 troveclient/__init__.py create mode 100644 troveclient/accounts.py create mode 100644 troveclient/auth.py create mode 100644 troveclient/backups.py create mode 100644 troveclient/base.py create mode 100644 troveclient/cli.py create mode 100644 troveclient/client.py create mode 100644 troveclient/common.py create mode 100644 troveclient/databases.py create mode 100644 troveclient/diagnostics.py create mode 100644 troveclient/exceptions.py create mode 100644 troveclient/flavors.py create mode 100644 troveclient/hosts.py create mode 100644 troveclient/instances.py create mode 100644 troveclient/limits.py create mode 100644 troveclient/management.py create mode 100644 troveclient/mcli.py create mode 100644 troveclient/quota.py create mode 100644 troveclient/root.py create mode 100644 troveclient/security_groups.py create mode 100644 troveclient/storage.py create mode 100644 troveclient/tests/__init__.py create mode 100644 troveclient/tests/test_accounts.py create mode 100644 troveclient/tests/test_auth.py create mode 100644 troveclient/tests/test_base.py create mode 100644 troveclient/tests/test_client.py create mode 100644 troveclient/tests/test_common.py create mode 100644 troveclient/tests/test_instances.py create mode 100644 troveclient/tests/test_limits.py create mode 100644 troveclient/tests/test_management.py create mode 100644 troveclient/tests/test_secgroups.py create mode 100644 troveclient/tests/test_users.py create mode 100644 troveclient/tests/test_utils.py create mode 100644 troveclient/tests/test_xml.py create mode 100644 troveclient/users.py create mode 100644 troveclient/utils.py create mode 100644 troveclient/versions.py create mode 100644 troveclient/xml.py diff --git a/.testr.conf b/.testr.conf index 8f0b597..d45a93a 100644 --- a/.testr.conf +++ b/.testr.conf @@ -2,7 +2,7 @@ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover . $LISTOPT $IDOPTION + ${PYTHON:-python} -m subunit.run discover -t ./ ./troveclient/tests $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list diff --git a/README.rst b/README.rst index 929143b..2be9bfb 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ -Python bindings to the Reddwarf API +Python bindings to the Trove API ================================================== -This is a client for the Reddwarf API. There's a Python API (the -``reddwarfclient`` module), and a command-line script (``reddwarf``). Each -implements 100% (or less ;) ) of the Reddwarf API. +This is a client for the Trove API. There's a Python API (the +``troveclient`` module), and a command-line script (``trove``). Each +implements 100% (or less ;) ) of the Trove API. Command-line API ---------------- @@ -13,7 +13,7 @@ tenant, and appropriate auth url. .. code-block:: bash - $ reddwarf-cli --username=jsmith --apikey=abcdefg --tenant=12345 --auth_url=http://reddwarf_auth:35357/v2.0/tokens auth login + $ trove-cli --username=jsmith --apikey=abcdefg --tenant=12345 --auth_url=http://trove_auth:35357/v2.0/tokens auth login At this point you will be authenticated and given a token, which is stored at ~/.apitoken. From there you can make other calls to the CLI. diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py deleted file mode 100644 index 0383828..0000000 --- a/reddwarfclient/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -from reddwarfclient.accounts import Accounts -from reddwarfclient.databases import Databases -from reddwarfclient.flavors import Flavors -from reddwarfclient.instances import Instances -from reddwarfclient.hosts import Hosts -from reddwarfclient.management import Management -from reddwarfclient.management import RootHistory -from reddwarfclient.root import Root -from reddwarfclient.storage import StorageInfo -from reddwarfclient.users import Users -from reddwarfclient.versions import Versions -from reddwarfclient.diagnostics import DiagnosticsInterrogator -from reddwarfclient.diagnostics import HwInfoInterrogator -from reddwarfclient.client import Dbaas -from reddwarfclient.client import ReddwarfHTTPClient diff --git a/reddwarfclient/accounts.py b/reddwarfclient/accounts.py deleted file mode 100644 index 43e9135..0000000 --- a/reddwarfclient/accounts.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base -from reddwarfclient.common import check_for_exceptions - - -class Account(base.Resource): - """ - Account is an opaque instance used to hold account information. - """ - def __repr__(self): - return "" % self.name - - -class Accounts(base.ManagerWithFind): - """ - Manage :class:`Account` information. - """ - - resource_class = Account - - 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, body[response_key]) - - def index(self): - """Get a list of all accounts with non-deleted instances""" - - url = "/mgmt/accounts" - resp, body = self.api.client.get(url) - check_for_exceptions(resp, body) - if not body: - raise Exception("Call to " + url + " did not return a body.") - return base.Resource(self, body) - - def show(self, account): - """ - Get details of one account. - - :rtype: :class:`Account`. - """ - - acct_name = self._get_account_name(account) - return self._list("/mgmt/accounts/%s" % acct_name, 'account') - - @staticmethod - def _get_account_name(account): - try: - if account.name: - return account.name - except AttributeError: - return account diff --git a/reddwarfclient/auth.py b/reddwarfclient/auth.py deleted file mode 100644 index 4494447..0000000 --- a/reddwarfclient/auth.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright 2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import exceptions - - -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') - - try: - print(resp_body) - self.auth_token = resp_body['auth']['token']['id'] - except KeyError: - raise nova_exceptions.AuthorizationFailure() - - catalog = resp_body['auth']['serviceCatalog'] - if 'cloudDatabases' not in catalog: - raise nova_exceptions.EndpointNotFound() - endpoints = catalog['cloudDatabases'] - for endpoint in endpoints: - if self.region_name is None or \ - endpoint['region'] == self.region_name: - self.management_url = endpoint['publicURL'] - return - raise nova_exceptions.EndpointNotFound() - - -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 Reddwarf service for a particular - endpoint attribute. If none given, return the first. - """ - matching_endpoints = [] - if 'endpoints' in self.catalog: - # We have a bastardized service catalog. Treat it special. :/ - for endpoint in self.catalog['endpoints']: - if not filter_value or endpoint[attr] == filter_value: - matching_endpoints.append(endpoint) - if not matching_endpoints: - raise exceptions.EndpointNotFound() - - # We don't always get a service catalog back ... - if not 'serviceCatalog' in self.catalog[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) diff --git a/reddwarfclient/backups.py b/reddwarfclient/backups.py deleted file mode 100644 index c78b840..0000000 --- a/reddwarfclient/backups.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base -import exceptions - - -class Backup(base.Resource): - """ - Backup is a resource used to hold backup information. - """ - def __repr__(self): - return "" % self.name - - -class Backups(base.ManagerWithFind): - """ - Manage :class:`Backups` information. - """ - - resource_class = Backup - - def get(self, backup): - """ - Get a specific backup. - - :rtype: :class:`Backups` - """ - return self._get("/backups/%s" % base.getid(backup), - "backup") - - def list(self, limit=None, marker=None): - """ - Get a list of all backups. - - :rtype: list of :class:`Backups`. - """ - return self._list("/backups", "backups", limit, marker) - - def create(self, name, instance, description=None): - """ - Create a new backup from the given instance. - """ - body = {"backup": { - "name": name, - "instance": instance, - "description": description, - }} - return self._create("/backups", body, "backup") - - def delete(self, backup_id): - """ - Delete the specified backup. - - :param backup_id: The backup id to delete - """ - resp, body = self.api.client.delete("/backups/%s" % backup_id) - if resp.status in (422, 500): - raise exceptions.from_response(resp, body) diff --git a/reddwarfclient/base.py b/reddwarfclient/base.py deleted file mode 100644 index 6660ff4..0000000 --- a/reddwarfclient/base.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Base utilities to build API operation managers and objects on top of. -""" - -import contextlib -import hashlib -import os -from reddwarfclient import exceptions -from reddwarfclient import utils - - -# Python 2.4 compat -try: - all -except NameError: - def all(iterable): - return True not in (not x for x in iterable) - - -def getid(obj): - """ - 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="~/.reddwarfclient") - - # NOTE(sirp): Keep separate UUID caches for each username + endpoint - # pair - username = utils.env('OS_USERNAME', 'USERNAME') - url = utils.env('OS_URL', 'SERVICE_URL') - uniqifier = hashlib.md5(username + url).hexdigest() - - cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) - - try: - os.makedirs(cache_dir, 0755) - except OSError: - # NOTE(kiall): This is typicaly either permission denied while - # attempting to create the directory, or the directory - # already exists. Either way, don't fail. - pass - - resource = obj_class.__name__.lower() - filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) - path = os.path.join(cache_dir, filename) - - cache_attr = "_%s_cache" % cache_type - - try: - setattr(self, cache_attr, open(path, mode)) - except IOError: - # NOTE(kiall): This is typicaly a permission denied while - # attempting to write the cache file. - pass - - try: - yield - finally: - cache = getattr(self, cache_attr, None) - if cache: - cache.close() - delattr(self, cache_attr) - - def write_to_completion_cache(self, cache_type, val): - cache = getattr(self, "_%s_cache" % cache_type, None) - if cache: - cache.write("%s\n" % val) - - def _get(self, url, response_key=None): - resp, body = self.api.client.get(url) - if response_key: - return self.resource_class(self, body[response_key], loaded=True) - else: - return self.resource_class(self, body, loaded=True) - - def _create(self, url, body, response_key, return_raw=False, **kwargs): - self.run_hooks('modify_body_for_create', body, **kwargs) - resp, body = self.api.client.post(url, body=body) - if return_raw: - return body[response_key] - - with self.completion_cache('human_id', self.resource_class, mode="a"): - with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key]) - - def _delete(self, url): - resp, body = self.api.client.delete(url) - - def _update(self, url, body, **kwargs): - self.run_hooks('modify_body_for_update', body, **kwargs) - resp, body = self.api.client.put(url, body=body) - return body - - -class ManagerWithFind(Manager): - """ - Like a `Manager`, but with additional `find()`/`findall()` methods. - """ - def find(self, **kwargs): - """ - Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - matches = self.findall(**kwargs) - num_matches = len(matches) - if num_matches == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) - raise exceptions.NotFound(404, msg) - elif num_matches > 1: - raise exceptions.NoUniqueMatch - else: - return matches[0] - - def findall(self, **kwargs): - """ - Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - found = [] - searches = kwargs.items() - - for obj in self.list(): - try: - if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) - except AttributeError: - continue - - return found - - def list(self): - raise NotImplementedError - - -class Resource(object): - """ - A resource represents a particular instance of an object (server, flavor, - etc). This is pretty much just a bag for attributes. - - :param manager: Manager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - HUMAN_ID = False - - def __init__(self, manager, info, loaded=False): - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded - - # NOTE(sirp): ensure `id` is already present because if it isn't we'll - # enter an infinite loop of __getattr__ -> get -> __init__ -> - # __getattr__ -> ... - if 'id' in self.__dict__ and len(str(self.id)) == 36: - self.manager.write_to_completion_cache('uuid', self.id) - - human_id = self.human_id - if human_id: - self.manager.write_to_completion_cache('human_id', human_id) - - @property - def human_id(self): - """Subclasses may override this provide a pretty ID which can be used - for bash completion. - """ - if 'name' in self.__dict__ and self.HUMAN_ID: - return utils.slugify(self.name) - return None - - def _add_details(self, info): - for (k, v) in info.iteritems(): - try: - setattr(self, k, v) - except AttributeError: - # In this case we already defined the attribute on the class - pass - - def __getattr__(self, k): - if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - def __repr__(self): - reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - - def get(self): - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id - return self._info == other._info - - def is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py deleted file mode 100644 index f83de6d..0000000 --- a/reddwarfclient/cli.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2011 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Reddwarf Command line tool -""" - -#TODO(tim.simpson): optparse is deprecated. Replace with argparse. -import optparse -import os -import sys - - -# If ../reddwarf/__init__.py exists, add ../ to Python search path, so that -# it will override what happens to be installed in /usr/(local/)lib/python... -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, 'reddwarfclient', - '__init__.py')): - sys.path.insert(0, possible_topdir) - - -from reddwarfclient import common - - -class InstanceCommands(common.AuthedCommandsBase): - """Commands to perform various instances operations and actions""" - - params = [ - 'flavor', - 'id', - 'limit', - 'marker', - 'name', - 'size', - 'backup' - ] - - def create(self): - """Create a new instance""" - self._require('name', 'flavor') - volume = None - if self.size is not None: - volume = {"size": self.size} - restorePoint = None - if self.backup is not None: - restorePoint = {"backupRef": self.backup} - self._pretty_print(self.dbaas.instances.create, self.name, - self.flavor, volume, restorePoint=restorePoint) - - def delete(self): - """Delete the specified instance""" - self._require('id') - print self.dbaas.instances.delete(self.id) - - def get(self): - """Get details for the specified instance""" - self._require('id') - self._pretty_print(self.dbaas.instances.get, self.id) - - def backups(self): - """Get a list of backups for the specified instance""" - self._require('id') - self._pretty_list(self.dbaas.instances.backups, self.id) - - def list(self): - """List all instances for account""" - # limit and marker are not required. - limit = self.limit or None - if limit: - limit = int(limit, 10) - self._pretty_paged(self.dbaas.instances.list) - - def resize_volume(self): - """Resize an instance volume""" - self._require('id', 'size') - self._pretty_print(self.dbaas.instances.resize_volume, self.id, - self.size) - - def resize_instance(self): - """Resize an instance flavor""" - self._require('id', 'flavor') - self._pretty_print(self.dbaas.instances.resize_instance, self.id, - self.flavor) - - def restart(self): - """Restart the database""" - self._require('id') - self._pretty_print(self.dbaas.instances.restart, self.id) - - def reset_password(self): - """Reset the root user Password""" - self._require('id') - self._pretty_print(self.dbaas.instances.reset_password, self.id) - - -class FlavorsCommands(common.AuthedCommandsBase): - """Commands for listing Flavors""" - - params = [] - - def list(self): - """List the available flavors""" - self._pretty_list(self.dbaas.flavors.list) - - -class DatabaseCommands(common.AuthedCommandsBase): - """Database CRUD operations on an instance""" - - params = [ - 'name', - 'id', - 'limit', - 'marker', - ] - - def create(self): - """Create a database""" - self._require('id', 'name') - databases = [{'name': self.name}] - print self.dbaas.databases.create(self.id, databases) - - def delete(self): - """Delete a database""" - self._require('id', 'name') - print self.dbaas.databases.delete(self.id, self.name) - - def list(self): - """List the databases""" - self._require('id') - self._pretty_paged(self.dbaas.databases.list, self.id) - - -class UserCommands(common.AuthedCommandsBase): - """User CRUD operations on an instance""" - params = [ - 'id', - 'database', - 'databases', - 'hostname', - 'name', - 'password', - ] - - def create(self): - """Create a user in instance, with access to one or more databases""" - self._require('id', 'name', 'password', 'databases') - self._make_list('databases') - databases = [{'name': dbname} for dbname in self.databases] - users = [{'name': self.name, 'host': self.hostname, - 'password': self.password, 'databases': databases}] - self.dbaas.users.create(self.id, users) - - def delete(self): - """Delete the specified user""" - self._require('id', 'name') - self.dbaas.users.delete(self.id, self.name, self.hostname) - - def get(self): - """Get a single user.""" - self._require('id', 'name') - self._pretty_print(self.dbaas.users.get, self.id, - self.name, self.hostname) - - def list(self): - """List all the users for an instance""" - self._require('id') - self._pretty_paged(self.dbaas.users.list, self.id) - - def access(self): - """Show all databases the user has access to.""" - self._require('id', 'name') - self._pretty_list(self.dbaas.users.list_access, self.id, - self.name, self.hostname) - - def grant(self): - """Allow an existing user permissions to access one or more - databases.""" - self._require('id', 'name', 'databases') - self._make_list('databases') - self.dbaas.users.grant(self.id, self.name, self.databases, - self.hostname) - - def revoke(self): - """Revoke from an existing user access permissions to a database.""" - self._require('id', 'name', 'database') - self.dbaas.users.revoke(self.id, self.name, self.database, - self.hostname) - - def change_password(self): - """Change the password of a single user.""" - self._require('id', 'name', 'password') - users = [{'name': self.name, - 'host': self.hostname, - 'password': self.password}] - self.dbaas.users.change_passwords(self.id, users) - - -class RootCommands(common.AuthedCommandsBase): - """Root user related operations on an instance""" - - params = [ - 'id', - ] - - def create(self): - """Enable the instance's root user.""" - self._require('id') - try: - user, password = self.dbaas.root.create(self.id) - print "User:\t\t%s\nPassword:\t%s" % (user, password) - except: - print sys.exc_info()[1] - - def enabled(self): - """Check the instance for root access""" - self._require('id') - self._pretty_print(self.dbaas.root.is_root_enabled, self.id) - - -class VersionCommands(common.AuthedCommandsBase): - """List available versions""" - - params = [ - 'url', - ] - - def list(self): - """List all the supported versions""" - self._require('url') - self._pretty_list(self.dbaas.versions.index, self.url) - - -class LimitsCommands(common.AuthedCommandsBase): - """Show the rate limits and absolute limits""" - - def list(self): - """List the rate limits and absolute limits""" - self._pretty_list(self.dbaas.limits.list) - - -class BackupsCommands(common.AuthedCommandsBase): - """Command to manage and show backups""" - params = ['name', 'instance', 'description'] - - def get(self): - """Get details for the specified backup""" - self._require('id') - self._pretty_print(self.dbaas.backups.get, self.id) - - def list(self): - """List backups""" - self._pretty_list(self.dbaas.backups.list) - - def create(self): - """Create a new backup""" - self._require('name', 'instance') - self._pretty_print(self.dbaas.backups.create, self.name, - self.instance, self.description) - - def delete(self): - """Delete a backup""" - self._require('id') - self._pretty_print(self.dbaas.backups.delete, self.id) - - -class SecurityGroupCommands(common.AuthedCommandsBase): - """Commands to list and show Security Groups For an Instance and """ - """create and delete security group rules for them. """ - params = ['id', - 'secgroup_id', - 'protocol', - 'from_port', - 'to_port', - 'cidr' - ] - - def get(self): - """Get a security group associated with an instance.""" - self._require('id') - self._pretty_print(self.dbaas.security_groups.get, self.id) - - def list(self): - """List all the Security Groups and the rules""" - self._pretty_paged(self.dbaas.security_groups.list) - - def add_rule(self): - """Add a security group rule""" - self._require('secgroup_id', 'protocol', - 'from_port', 'to_port', 'cidr') - self.dbaas.security_group_rules.create(self.secgroup_id, self.protocol, - self.from_port, self.to_port, - self.cidr) - - def delete_rule(self): - """Delete a security group rule""" - self._require('id') - self.dbaas.security_group_rules.delete(self.id) - - -COMMANDS = {'auth': common.Auth, - 'instance': InstanceCommands, - 'flavor': FlavorsCommands, - 'database': DatabaseCommands, - 'limit': LimitsCommands, - 'backup': BackupsCommands, - 'user': UserCommands, - 'root': RootCommands, - 'version': VersionCommands, - 'secgroup': SecurityGroupCommands, - } - - -def main(): - # Parse arguments - load_file = True - for index, arg in enumerate(sys.argv): - if (arg == "auth" and len(sys.argv) > (index + 1) - and sys.argv[index + 1] == "login"): - load_file = False - - oparser = common.CliOptions.create_optparser(load_file) - for k, v in COMMANDS.items(): - v._prepare_parser(oparser) - (options, args) = oparser.parse_args() - - if not args: - common.print_commands(COMMANDS) - - if options.verbose: - os.environ['RDC_PP'] = "True" - os.environ['REDDWARFCLIENT_DEBUG'] = "True" - - # Pop the command and check if it's in the known commands - cmd = args.pop(0) - if cmd in COMMANDS: - fn = COMMANDS.get(cmd) - command_object = None - try: - command_object = fn(oparser) - except Exception as ex: - if options.debug: - raise - print(ex) - - # Get a list of supported actions for the command - actions = common.methods_of(command_object) - - if len(args) < 1: - common.print_actions(cmd, actions) - - # Check for a valid action and perform that action - action = args.pop(0) - if action in actions: - if not options.debug: - try: - getattr(command_object, action)() - except Exception as ex: - if options.debug: - raise - print ex - else: - getattr(command_object, action)() - else: - common.print_actions(cmd, actions) - else: - common.print_commands(COMMANDS) - - -if __name__ == '__main__': - main() diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py deleted file mode 100644 index f602afb..0000000 --- a/reddwarfclient/client.py +++ /dev/null @@ -1,370 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -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 reddwarfclient import auth -from reddwarfclient 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 ReddwarfHTTPClient(httplib2.Http): - - USER_AGENT = 'python-reddwarfclient' - - 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(ReddwarfHTTPClient, 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): - from reddwarfclient import common - 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(ReddwarfHTTPClient, self).request(*args, **kwargs) - - # Save this in case anyone wants it. - self.last_response = (resp, body) - self.http_log(args, kwargs, resp, body) - - if body: - 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='reddwarf', - service_url=None, insecure=False, auth_strategy='keystone', - region_name=None, client_cls=ReddwarfHTTPClient): - from reddwarfclient.versions import Versions - from reddwarfclient.databases import Databases - from reddwarfclient.flavors import Flavors - from reddwarfclient.instances import Instances - from reddwarfclient.limits import Limits - from reddwarfclient.users import Users - from reddwarfclient.root import Root - from reddwarfclient.hosts import Hosts - from reddwarfclient.quota import Quotas - from reddwarfclient.backups import Backups - from reddwarfclient.security_groups import SecurityGroups - from reddwarfclient.security_groups import SecurityGroupRules - from reddwarfclient.storage import StorageInfo - from reddwarfclient.management import Management - from reddwarfclient.accounts import Accounts - from reddwarfclient.diagnostics import DiagnosticsInterrogator - from reddwarfclient.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.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() diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py deleted file mode 100644 index 9c66c49..0000000 --- a/reddwarfclient/common.py +++ /dev/null @@ -1,406 +0,0 @@ -# Copyright 2011 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import copy -import json -import optparse -import os -import pickle -import sys - -from reddwarfclient import client -from reddwarfclient.xml import ReddwarfXmlClient -from reddwarfclient 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': 'reddwarf', - '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] ", - 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 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 = ReddwarfXmlClient - else: - client_cls = client.ReddwarfHTTPClient - 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 _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 diff --git a/reddwarfclient/databases.py b/reddwarfclient/databases.py deleted file mode 100644 index d7f31e1..0000000 --- a/reddwarfclient/databases.py +++ /dev/null @@ -1,79 +0,0 @@ -from reddwarfclient import base -from reddwarfclient.common import check_for_exceptions -from reddwarfclient.common import limit_url -from reddwarfclient.common import Paginated -import exceptions -import urlparse - - -class Database(base.Resource): - """ - According to Wikipedia, "A database is a system intended to organize, - store, and retrieve - large amounts of data easily." - """ - def __repr__(self): - return "" % self.name - - -class Databases(base.ManagerWithFind): - """ - Manage :class:`Databases` resources. - """ - resource_class = Database - - def create(self, instance_id, databases): - """ - Create new databases within the specified instance - """ - body = {"databases": databases} - url = "/instances/%s/databases" % instance_id - resp, body = self.api.client.post(url, body=body) - check_for_exceptions(resp, body) - - def delete(self, instance_id, dbname): - """Delete an existing database in the specified instance""" - url = "/instances/%s/databases/%s" % (instance_id, dbname) - resp, body = self.api.client.delete(url) - check_for_exceptions(resp, body) - - def _list(self, url, response_key, limit=None, marker=None): - resp, body = self.api.client.get(limit_url(url, limit, marker)) - check_for_exceptions(resp, body) - if not body: - raise Exception("Call to " + url + - " did not return a body.") - links = body.get('links', []) - next_links = [link['href'] for link in links if link['rel'] == 'next'] - next_marker = None - for link in next_links: - # Extract the marker from the url. - parsed_url = urlparse.urlparse(link) - query_dict = dict(urlparse.parse_qsl(parsed_url.query)) - next_marker = query_dict.get('marker', None) - databases = body[response_key] - databases = [self.resource_class(self, res) for res in databases] - return Paginated(databases, next_marker=next_marker, links=links) - - def list(self, instance, limit=None, marker=None): - """ - Get a list of all Databases from the instance. - - :rtype: list of :class:`Database`. - """ - return self._list("/instances/%s/databases" % base.getid(instance), - "databases", limit, marker) - -# def get(self, instance, database): -# """ -# Get a specific instances. -# -# :param flavor: The ID of the :class:`Database` to get. -# :rtype: :class:`Database` -# """ -# assert isinstance(instance, Instance) -# assert isinstance(database, (Database, int)) -# instance_id = base.getid(instance) -# db_id = base.getid(database) -# url = "/instances/%s/databases/%s" % (instance_id, db_id) -# return self._get(url, "database") diff --git a/reddwarfclient/diagnostics.py b/reddwarfclient/diagnostics.py deleted file mode 100644 index 06da06c..0000000 --- a/reddwarfclient/diagnostics.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base -import exceptions - - -class Diagnostics(base.Resource): - """ - Account is an opaque instance used to hold account information. - """ - def __repr__(self): - return "" % self.version - - -class DiagnosticsInterrogator(base.ManagerWithFind): - """ - Manager class for Interrogator resource - """ - resource_class = Diagnostics - - def get(self, instance): - """ - Get the diagnostics of the guest on the instance. - """ - return self._get("/mgmt/instances/%s/diagnostics" % - base.getid(instance), "diagnostics") - - -class HwInfo(base.Resource): - - def __repr__(self): - return "" % self.version - - -class HwInfoInterrogator(base.ManagerWithFind): - """ - Manager class for HwInfo - """ - resource_class = HwInfo - - def get(self, instance): - """ - Get the hardware information of the instance. - """ - return self._get("/mgmt/instances/%s/hwinfo" % base.getid(instance)) diff --git a/reddwarfclient/exceptions.py b/reddwarfclient/exceptions.py deleted file mode 100644 index 88c3f7e..0000000 --- a/reddwarfclient/exceptions.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright 2011 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -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) diff --git a/reddwarfclient/flavors.py b/reddwarfclient/flavors.py deleted file mode 100644 index ba01a5f..0000000 --- a/reddwarfclient/flavors.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) 2012 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -from reddwarfclient import base - -import exceptions - -from reddwarfclient.common import check_for_exceptions - - -class Flavor(base.Resource): - """ - A Flavor is an Instance type, specifying among other things, RAM size. - """ - def __repr__(self): - return "" % self.name - - -class Flavors(base.ManagerWithFind): - """ - Manage :class:`Flavor` resources. - """ - resource_class = Flavor - - def __repr__(self): - return "" % 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. - - :rtype: list of :class:`Flavor`. - """ - return self._list("/flavors", "flavors") - - def get(self, flavor): - """ - Get a specific flavor. - - :rtype: :class:`Flavor` - """ - return self._get("/flavors/%s" % base.getid(flavor), - "flavor") diff --git a/reddwarfclient/hosts.py b/reddwarfclient/hosts.py deleted file mode 100644 index fb03d37..0000000 --- a/reddwarfclient/hosts.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base - -from reddwarfclient.common import check_for_exceptions - - -class Host(base.Resource): - """ - A Hosts is an opaque instance used to store Host instances. - """ - def __repr__(self): - return "" % self.name - - -class Hosts(base.ManagerWithFind): - """ - Manage :class:`Host` resources. - """ - resource_class = Host - - 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 _action(self, host_id, body): - """ - Perform a host "action" -- update - """ - url = "/mgmt/hosts/%s/instances/action" % host_id - resp, body = self.api.client.post(url, body=body) - check_for_exceptions(resp, body) - - def update_all(self, host_id): - """ - Update all instances on a host. - """ - body = {'update': ''} - self._action(host_id, body) - - def index(self): - """ - Get a list of all hosts. - - :rtype: list of :class:`Hosts`. - """ - return self._list("/mgmt/hosts", "hosts") - - def get(self, host): - """ - Get a specific host. - - :rtype: :class:`host` - """ - return self._get("/mgmt/hosts/%s" % self._get_host_name(host), "host") - - @staticmethod - def _get_host_name(host): - try: - if host.name: - return host.name - except AttributeError: - return host diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py deleted file mode 100644 index 66b091c..0000000 --- a/reddwarfclient/instances.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base - -import exceptions -import urlparse - -from reddwarfclient.common import check_for_exceptions -from reddwarfclient.common import limit_url -from reddwarfclient.common import Paginated - - -REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' - - -class Instance(base.Resource): - """ - An Instance is an opaque instance used to store Database instances. - """ - def __repr__(self): - return "" % self.name - - def list_databases(self): - return self.manager.databases.list(self) - - def delete(self): - """ - Delete the instance. - """ - self.manager.delete(self) - - def restart(self): - """ - Restart the database instance - """ - self.manager.restart(self.id) - - -class Instances(base.ManagerWithFind): - """ - Manage :class:`Instance` resources. - """ - resource_class = Instance - - def create(self, name, flavor_id, volume=None, databases=None, users=None, - restorePoint=None): - """ - Create (boot) a new instance. - """ - body = {"instance": { - "name": name, - "flavorRef": flavor_id - }} - if volume: - body["instance"]["volume"] = volume - if databases: - body["instance"]["databases"] = databases - if users: - body["instance"]["users"] = users - if restorePoint: - body["instance"]["restorePoint"] = restorePoint - - return self._create("/instances", body, "instance") - - def _list(self, url, response_key, limit=None, marker=None): - resp, body = self.api.client.get(limit_url(url, limit, marker)) - if not body: - raise Exception("Call to " + url + " did not return a body.") - links = body.get('links', []) - next_links = [link['href'] for link in links if link['rel'] == 'next'] - next_marker = None - for link in next_links: - # Extract the marker from the url. - parsed_url = urlparse.urlparse(link) - query_dict = dict(urlparse.parse_qsl(parsed_url.query)) - next_marker = query_dict.get('marker', None) - instances = body[response_key] - instances = [self.resource_class(self, res) for res in instances] - return Paginated(instances, next_marker=next_marker, links=links) - - def list(self, limit=None, marker=None): - """ - Get a list of all instances. - - :rtype: list of :class:`Instance`. - """ - return self._list("/instances", "instances", limit, marker) - - def get(self, instance): - """ - Get a specific instances. - - :rtype: :class:`Instance` - """ - return self._get("/instances/%s" % base.getid(instance), - "instance") - - def backups(self, instance): - """ - Get the list of backups for a specific instance. - - :rtype: list of :class:`Backups`. - """ - return self._list("/instances/%s/backups" % base.getid(instance), - "backups") - - def delete(self, instance): - """ - Delete the specified instance. - - :param instance_id: The instance id to delete - """ - resp, body = self.api.client.delete("/instances/%s" % - base.getid(instance)) - if resp.status in (422, 500): - raise exceptions.from_response(resp, body) - - def _action(self, instance_id, body): - """ - Perform a server "action" -- reboot/rebuild/resize/etc. - """ - url = "/instances/%s/action" % instance_id - resp, body = self.api.client.post(url, body=body) - check_for_exceptions(resp, body) - if body: - return self.resource_class(self, body, loaded=True) - return body - - def resize_volume(self, instance_id, volume_size): - """ - Resize the volume on an existing instances - """ - body = {"resize": {"volume": {"size": volume_size}}} - self._action(instance_id, body) - - def resize_instance(self, instance_id, flavor_id): - """ - Resize the volume on an existing instances - """ - body = {"resize": {"flavorRef": flavor_id}} - self._action(instance_id, body) - - def restart(self, instance_id): - """ - Restart the database instance. - - :param instance_id: The :class:`Instance` (or its ID) to share onto. - """ - body = {'restart': {}} - self._action(instance_id, body) - - def reset_password(self, instance_id): - """ - Resets the database instance root password. - - :param instance_id: The :class:`Instance` (or its ID) to share onto. - """ - body = {'reset-password': {}} - return self._action(instance_id, body) - -Instances.resize_flavor = Instances.resize_instance - - -class InstanceStatus(object): - - ACTIVE = "ACTIVE" - BLOCKED = "BLOCKED" - BUILD = "BUILD" - FAILED = "FAILED" - REBOOT = "REBOOT" - RESIZE = "RESIZE" - SHUTDOWN = "SHUTDOWN" diff --git a/reddwarfclient/limits.py b/reddwarfclient/limits.py deleted file mode 100644 index f2b8703..0000000 --- a/reddwarfclient/limits.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2013 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base -import exceptions - - -class Limit(base.Resource): - - def __repr__(self): - return "" % self.verb - - -class Limits(base.ManagerWithFind): - """ - Manages :class `Limit` resources - """ - resource_class = Limit - - def __repr__(self): - return "" % id(self) - - def _list(self, url, response_key): - resp, body = self.api.client.get(url) - - if resp is None or resp.status != 200: - raise exceptions.from_response(resp, body) - - 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): - """ - Retrieve the limits - """ - return self._list("/limits", "limits") diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py deleted file mode 100644 index 931c0d5..0000000 --- a/reddwarfclient/management.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base -import urlparse - -from reddwarfclient.common import check_for_exceptions -from reddwarfclient.common import limit_url -from reddwarfclient.common import Paginated -from reddwarfclient.instances import Instance - - -class RootHistory(base.Resource): - def __repr__(self): - return ("" - % (self.id, self.created, self.user)) - - -class Management(base.ManagerWithFind): - """ - Manage :class:`Instances` resources. - """ - resource_class = Instance - - def _list(self, url, response_key, limit=None, marker=None): - resp, body = self.api.client.get(limit_url(url, limit, marker)) - if not body: - raise Exception("Call to " + url + " did not return a body.") - links = body.get('links', []) - next_links = [link['href'] for link in links if link['rel'] == 'next'] - next_marker = None - for link in next_links: - # Extract the marker from the url. - parsed_url = urlparse.urlparse(link) - query_dict = dict(urlparse.parse_qsl(parsed_url.query)) - next_marker = query_dict.get('marker', None) - instances = body[response_key] - instances = [self.resource_class(self, res) for res in instances] - return Paginated(instances, next_marker=next_marker, links=links) - - def show(self, instance): - """ - Get details of one instance. - - :rtype: :class:`Instance`. - """ - - return self._get("/mgmt/instances/%s" % base.getid(instance), - 'instance') - - def index(self, deleted=None, limit=None, marker=None): - """ - Show an overview of all local instances. - Optionally, filter by deleted status. - - :rtype: list of :class:`Instance`. - """ - form = '' - if deleted is not None: - if deleted: - form = "?deleted=true" - else: - form = "?deleted=false" - - url = "/mgmt/instances%s" % form - return self._list(url, "instances", limit, marker) - - def root_enabled_history(self, instance): - """ - Get root access history of one instance. - - """ - url = "/mgmt/instances/%s/root" % base.getid(instance) - resp, body = self.api.client.get(url) - if not body: - raise Exception("Call to " + url + " did not return a body.") - return RootHistory(self, body['root_history']) - - def _action(self, instance_id, body): - """ - Perform a server "action" -- reboot/rebuild/resize/etc. - """ - url = "/mgmt/instances/%s/action" % instance_id - resp, body = self.api.client.post(url, body=body) - check_for_exceptions(resp, body) - - def stop(self, instance_id): - body = {'stop': {}} - self._action(instance_id, body) - - def reboot(self, instance_id): - """ - Reboot the underlying OS. - - :param instance_id: The :class:`Instance` (or its ID) to share onto. - """ - body = {'reboot': {}} - self._action(instance_id, body) - - def migrate(self, instance_id, host=None): - """ - Migrate the instance. - - :param instance_id: The :class:`Instance` (or its ID) to share onto. - """ - if host: - body = {'migrate': {'host': host}} - else: - body = {'migrate': {}} - self._action(instance_id, body) - - def update(self, instance_id): - """ - Update the guest agent via apt-get. - """ - body = {'update': {}} - self._action(instance_id, body) - - def reset_task_status(self, instance_id): - """ - Set the task status to NONE. - """ - body = {'reset-task-status': {}} - self._action(instance_id, body) diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py deleted file mode 100644 index 62371cc..0000000 --- a/reddwarfclient/mcli.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2011 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Reddwarf Management Command line tool -""" - -import json -import optparse -import os -import sys - - -# If ../reddwarf/__init__.py exists, add ../ to Python search path, so that -# it will override what happens to be installed in /usr/(local/)lib/python... -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, 'reddwarfclient', - '__init__.py')): - sys.path.insert(0, possible_topdir) - - -from reddwarfclient import common - - -oparser = None - - -def _pretty_print(info): - print json.dumps(info, sort_keys=True, indent=4) - - -class HostCommands(common.AuthedCommandsBase): - """Commands to list info on hosts""" - - params = [ - 'name', - ] - - def update_all(self): - """Update all instances on a host""" - self._require('name') - self.dbaas.hosts.update_all(self.name) - - def get(self): - """List details for the specified host""" - self._require('name') - self._pretty_print(self.dbaas.hosts.get, self.name) - - def list(self): - """List all compute hosts""" - self._pretty_list(self.dbaas.hosts.index) - - -class QuotaCommands(common.AuthedCommandsBase): - """List and update quota limits for a tenant.""" - - params = ['id', - 'instances', - 'volumes', - 'backups'] - - def list(self): - """List all quotas for a tenant""" - self._require('id') - self._pretty_print(self.dbaas.quota.show, self.id) - - def update(self): - """Update quota limits for a tenant""" - self._require('id') - self._pretty_print(self.dbaas.quota.update, self.id, - dict((param, getattr(self, param)) - for param in self.params if param != 'id')) - - -class RootCommands(common.AuthedCommandsBase): - """List details about the root info for an instance.""" - - params = [ - 'id', - ] - - def history(self): - """List root history for the instance.""" - self._require('id') - self._pretty_print(self.dbaas.management.root_enabled_history, self.id) - - -class AccountCommands(common.AuthedCommandsBase): - """Commands to list account info""" - - params = [ - 'id', - ] - - def list(self): - """List all accounts with non-deleted instances""" - self._pretty_print(self.dbaas.accounts.index) - - def get(self): - """List details for the account provided""" - self._require('id') - self._pretty_print(self.dbaas.accounts.show, self.id) - - -class InstanceCommands(common.AuthedCommandsBase): - """List details about an instance.""" - - params = [ - 'deleted', - 'id', - 'limit', - 'marker', - 'host', - ] - - def get(self): - """List details for the instance.""" - self._require('id') - self._pretty_print(self.dbaas.management.show, self.id) - - def list(self): - """List all instances for account""" - deleted = None - if self.deleted is not None: - if self.deleted.lower() in ['true']: - deleted = True - elif self.deleted.lower() in ['false']: - deleted = False - self._pretty_paged(self.dbaas.management.index, deleted=deleted) - - def hwinfo(self): - """Show hardware information details about an instance.""" - self._require('id') - self._pretty_print(self.dbaas.hwinfo.get, self.id) - - def diagnostic(self): - """List diagnostic details about an instance.""" - self._require('id') - self._pretty_print(self.dbaas.diagnostics.get, self.id) - - def stop(self): - """Stop MySQL on the given instance.""" - self._require('id') - self._pretty_print(self.dbaas.management.stop, self.id) - - def reboot(self): - """Reboot the instance.""" - self._require('id') - self._pretty_print(self.dbaas.management.reboot, self.id) - - def migrate(self): - """Migrate the instance.""" - self._require('id') - self._pretty_print(self.dbaas.management.migrate, self.id, self.host) - - def reset_task_status(self): - """Set the instance's task status to NONE.""" - self._require('id') - self._pretty_print(self.dbaas.management.reset_task_status, self.id) - - -class StorageCommands(common.AuthedCommandsBase): - """Commands to list devices info""" - - params = [] - - def list(self): - """List details for the storage device""" - self._pretty_list(self.dbaas.storage.index) - - -def config_options(oparser): - oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", - help="Auth API endpoint URL with port and version. \ - Default: http://localhost:5000/v1.1") - - -COMMANDS = {'account': AccountCommands, - 'host': HostCommands, - 'instance': InstanceCommands, - 'root': RootCommands, - 'storage': StorageCommands, - 'quota': QuotaCommands, - } - - -def main(): - # Parse arguments - oparser = common.CliOptions.create_optparser(True) - for k, v in COMMANDS.items(): - v._prepare_parser(oparser) - (options, args) = oparser.parse_args() - - if not args: - common.print_commands(COMMANDS) - - # Pop the command and check if it's in the known commands - cmd = args.pop(0) - if cmd in COMMANDS: - fn = COMMANDS.get(cmd) - command_object = None - try: - command_object = fn(oparser) - except Exception as ex: - if options.debug: - raise - print(ex) - - # Get a list of supported actions for the command - actions = common.methods_of(command_object) - - if len(args) < 1: - common.print_actions(cmd, actions) - - # Check for a valid action and perform that action - action = args.pop(0) - if action in actions: - try: - getattr(command_object, action)() - except Exception as ex: - if options.debug: - raise - print ex - else: - common.print_actions(cmd, actions) - else: - common.print_commands(COMMANDS) - - -if __name__ == '__main__': - main() diff --git a/reddwarfclient/quota.py b/reddwarfclient/quota.py deleted file mode 100644 index e5a8f74..0000000 --- a/reddwarfclient/quota.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base -from reddwarfclient.common import check_for_exceptions - - -class Quotas(base.ManagerWithFind): - """ - Manage :class:`Quota` information. - """ - - resource_class = base.Resource - - def show(self, tenant_id): - """Get a list of all quotas for a tenant id""" - - url = "/mgmt/quotas/%s" % tenant_id - resp, body = self.api.client.get(url) - check_for_exceptions(resp, body) - if not body: - raise Exception("Call to " + url + " did not return a body.") - if 'quotas' not in body: - raise Exception("Missing key value 'quotas' in response body.") - return body['quotas'] - - def update(self, id, quotas): - """ - Set limits for quotas - """ - url = "/mgmt/quotas/%s" % id - body = {"quotas": quotas} - resp, body = self.api.client.put(url, body=body) - check_for_exceptions(resp, body) - if not body: - raise Exception("Call to " + url + " did not return a body.") - if 'quotas' not in body: - raise Exception("Missing key value 'quotas' in response body.") - return body['quotas'] diff --git a/reddwarfclient/root.py b/reddwarfclient/root.py deleted file mode 100644 index 33b0da7..0000000 --- a/reddwarfclient/root.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base - -from reddwarfclient import users -from reddwarfclient.common import check_for_exceptions -import exceptions - - -class Root(base.ManagerWithFind): - """ - Manager class for Root resource - """ - resource_class = users.User - url = "/instances/%s/root" - - def create(self, instance_id): - """ - Enable the root user and return the root password for the - sepcified db instance - """ - resp, body = self.api.client.post(self.url % instance_id) - check_for_exceptions(resp, body) - return body['user']['name'], body['user']['password'] - - def is_root_enabled(self, instance_id): - """ Return True if root is enabled for the instance; - False otherwise""" - resp, body = self.api.client.get(self.url % instance_id) - check_for_exceptions(resp, body) - return body['rootEnabled'] diff --git a/reddwarfclient/security_groups.py b/reddwarfclient/security_groups.py deleted file mode 100644 index d26cc86..0000000 --- a/reddwarfclient/security_groups.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# 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 reddwarfclient import base - -import exceptions -import urlparse - -from reddwarfclient.common import limit_url -from reddwarfclient.common import Paginated - - -class SecurityGroup(base.Resource): - """ - Security Group is a resource used to hold security group information. - """ - def __repr__(self): - return "" % self.name - - -class SecurityGroups(base.ManagerWithFind): - """ - Manage :class:`SecurityGroup` resources. - """ - resource_class = SecurityGroup - - def _list(self, url, response_key, limit=None, marker=None): - resp, body = self.api.client.get(limit_url(url, limit, marker)) - if not body: - raise Exception("Call to " + url + " did not return a body.") - links = body.get('links', []) - next_links = [link['href'] for link in links if link['rel'] == 'next'] - next_marker = None - for link in next_links: - # Extract the marker from the url. - parsed_url = urlparse.urlparse(link) - query_dict = dict(urlparse.parse_qsl(parsed_url.query)) - next_marker = query_dict.get('marker', None) - instances = body[response_key] - instances = [self.resource_class(self, res) for res in instances] - return Paginated(instances, next_marker=next_marker, links=links) - - def list(self, limit=None, marker=None): - """ - Get a list of all security groups. - - :rtype: list of :class:`SecurityGroup`. - """ - return self._list("/security-groups", "security_groups", limit, - marker) - - def get(self, security_group): - """ - Get a specific security group. - - :rtype: :class:`SecurityGroup` - """ - return self._get("/security-groups/%s" % base.getid(security_group), - "security_group") - - -class SecurityGroupRule(base.Resource): - """ - Security Group Rule is a resource used to hold security group - rule related information. - """ - def __repr__(self): - return \ - "" % (self.group_id, self.protocol, self.from_port, - self.to_port, self.cidr) - - -class SecurityGroupRules(base.ManagerWithFind): - """ - Manage :class:`SecurityGroupRules` resources. - """ - resource_class = SecurityGroupRule - - def create(self, group_id, protocol, from_port, to_port, cidr): - """ - Create a new security group rule. - """ - body = {"security_group_rule": { - "group_id": group_id, - "protocol": protocol, - "from_port": from_port, - "to_port": to_port, - "cidr": cidr - }} - return self._create("/security-group-rules", body, - "security_group_rule") - - def delete(self, security_group_rule): - """ - Delete the specified security group rule. - - :param security_group_rule: The security group rule to delete - """ - resp, body = self.api.client.delete("/security-group-rules/%s" % - base.getid(security_group_rule)) - if resp.status in (422, 500): - raise exceptions.from_response(resp, body) diff --git a/reddwarfclient/storage.py b/reddwarfclient/storage.py deleted file mode 100644 index 653096e..0000000 --- a/reddwarfclient/storage.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base - - -class Device(base.Resource): - """ - Storage is an opaque instance used to hold storage information. - """ - def __repr__(self): - return "" % self.name - - -class StorageInfo(base.ManagerWithFind): - """ - Manage :class:`Storage` resources. - """ - resource_class = Device - - 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 index(self): - """ - Get a list of all storages. - - :rtype: list of :class:`Storages`. - """ - return self._list("/mgmt/storage", "devices") diff --git a/reddwarfclient/tests/test_accounts.py b/reddwarfclient/tests/test_accounts.py deleted file mode 100644 index ec76f13..0000000 --- a/reddwarfclient/tests/test_accounts.py +++ /dev/null @@ -1,84 +0,0 @@ -from testtools import TestCase -from mock import Mock - -from reddwarfclient import accounts -from reddwarfclient import base - -""" -Unit tests for accounts.py -""" - - -class AccountTest(TestCase): - - def setUp(self): - super(AccountTest, self).setUp() - self.orig__init = accounts.Account.__init__ - accounts.Account.__init__ = Mock(return_value=None) - self.account = accounts.Account() - - def tearDown(self): - super(AccountTest, self).tearDown() - accounts.Account.__init__ = self.orig__init - - def test___repr__(self): - self.account.name = "account-1" - self.assertEqual('', self.account.__repr__()) - - -class AccountsTest(TestCase): - - def setUp(self): - super(AccountsTest, self).setUp() - self.orig__init = accounts.Accounts.__init__ - accounts.Accounts.__init__ = Mock(return_value=None) - self.accounts = accounts.Accounts() - self.accounts.api = Mock() - self.accounts.api.client = Mock() - - def tearDown(self): - super(AccountsTest, self).tearDown() - accounts.Accounts.__init__ = self.orig__init - - def test__list(self): - def side_effect_func(self, val): - return val - - self.accounts.resource_class = Mock(side_effect=side_effect_func) - key_ = 'key' - body_ = {key_: "test-value"} - self.accounts.api.client.get = Mock(return_value=('resp', body_)) - self.assertEqual("test-value", self.accounts._list('url', key_)) - - self.accounts.api.client.get = Mock(return_value=('resp', None)) - self.assertRaises(Exception, self.accounts._list, 'url', None) - - def test_index(self): - resp = Mock() - resp.status = 400 - body = {"Accounts": {}} - self.accounts.api.client.get = Mock(return_value=(resp, body)) - self.assertRaises(Exception, self.accounts.index) - resp.status = 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) - - def test_show(self): - def side_effect_func(acct_name, acct): - return acct_name, acct - - account_ = Mock() - account_.name = "test-account" - self.accounts._list = Mock(side_effect=side_effect_func) - self.assertEqual(('/mgmt/accounts/test-account', 'account'), - self.accounts.show(account_)) - - def test__get_account_name(self): - account_ = 'account with no name' - self.assertEqual(account_, - accounts.Accounts._get_account_name(account_)) - account_ = Mock() - account_.name = "account-name" - self.assertEqual("account-name", - accounts.Accounts._get_account_name(account_)) diff --git a/reddwarfclient/tests/test_auth.py b/reddwarfclient/tests/test_auth.py deleted file mode 100644 index f1c4b59..0000000 --- a/reddwarfclient/tests/test_auth.py +++ /dev/null @@ -1,414 +0,0 @@ -import contextlib - -from testtools import TestCase -from reddwarfclient import auth -from mock import Mock - -from reddwarfclient import exceptions - -""" -Unit tests for the classes and functions in auth.py. -""" - - -def check_url_none(test_case, auth_class): - # url is None, it must throw exception - authObj = auth_class(url=None, type=auth_class, client=None, - username=None, password=None, tenant=None) - try: - authObj.authenticate() - test_case.fail("AuthUrlNotGiven exception expected") - except exceptions.AuthUrlNotGiven: - pass - - -class AuthenticatorTest(TestCase): - - def setUp(self): - super(AuthenticatorTest, self).setUp() - self.orig_load = auth.ServiceCatalog._load - self.orig__init = auth.ServiceCatalog.__init__ - - def tearDown(self): - super(AuthenticatorTest, self).tearDown() - auth.ServiceCatalog._load = self.orig_load - auth.ServiceCatalog.__init__ = self.orig__init - - def test_get_authenticator_cls(self): - class_list = (auth.KeyStoneV2Authenticator, - auth.RaxAuthenticator, - auth.Auth1_1, - auth.FakeAuth) - - for c in class_list: - self.assertEqual(c, auth.get_authenticator_cls(c)) - - class_names = {"keystone": auth.KeyStoneV2Authenticator, - "rax": auth.RaxAuthenticator, - "auth1.1": auth.Auth1_1, - "fake": auth.FakeAuth} - - for cn in class_names.keys(): - self.assertEqual(class_names[cn], auth.get_authenticator_cls(cn)) - - cls_or_name = "_unknown_" - self.assertRaises(ValueError, auth.get_authenticator_cls, cls_or_name) - - def test__authenticate(self): - authObj = auth.Authenticator(Mock(), auth.KeyStoneV2Authenticator, - Mock(), Mock(), Mock(), Mock()) - # test response code 200 - resp = Mock() - resp.status = 200 - body = "test_body" - - auth.ServiceCatalog._load = Mock(return_value=1) - authObj.client._time_request = Mock(return_value=(resp, body)) - - sc = authObj._authenticate(Mock(), Mock()) - self.assertEqual(body, sc.catalog) - - # test AmbiguousEndpoints exception - auth.ServiceCatalog.__init__ = \ - Mock(side_effect=exceptions.AmbiguousEndpoints) - self.assertRaises(exceptions.AmbiguousEndpoints, - authObj._authenticate, Mock(), Mock()) - - # test handling KeyError and raising AuthorizationFailure exception - auth.ServiceCatalog.__init__ = Mock(side_effect=KeyError) - self.assertRaises(exceptions.AuthorizationFailure, - authObj._authenticate, Mock(), Mock()) - - # test EndpointNotFound exception - mock = Mock(side_effect=exceptions.EndpointNotFound) - auth.ServiceCatalog.__init__ = mock - self.assertRaises(exceptions.EndpointNotFound, - authObj._authenticate, Mock(), Mock()) - mock.side_effect = None - - # test response code 305 - resp.__getitem__ = Mock(return_value='loc') - resp.status = 305 - body = "test_body" - authObj.client._time_request = Mock(return_value=(resp, body)) - - l = authObj._authenticate(Mock(), Mock()) - self.assertEqual('loc', l) - - # test any response code other than 200 and 305 - resp.status = 404 - exceptions.from_response = Mock(side_effect=ValueError) - self.assertRaises(ValueError, authObj._authenticate, Mock(), Mock()) - - def test_authenticate(self): - authObj = auth.Authenticator(Mock(), auth.KeyStoneV2Authenticator, - Mock(), Mock(), Mock(), Mock()) - self.assertRaises(NotImplementedError, authObj.authenticate) - - -class KeyStoneV2AuthenticatorTest(TestCase): - - def test_authenticate(self): - # url is None - check_url_none(self, auth.KeyStoneV2Authenticator) - - # url is not None, so it must not throw exception - url = "test_url" - cls_type = auth.KeyStoneV2Authenticator - authObj = auth.KeyStoneV2Authenticator(url=url, type=cls_type, - client=None, username=None, - password=None, tenant=None) - - def side_effect_func(url): - return url - - mock = Mock() - mock.side_effect = side_effect_func - authObj._v2_auth = mock - r = authObj.authenticate() - self.assertEqual(url, r) - - def test__v2_auth(self): - username = "reddwarf_user" - password = "reddwarf_password" - tenant = "tenant" - cls_type = auth.KeyStoneV2Authenticator - authObj = auth.KeyStoneV2Authenticator(url=None, type=cls_type, - client=None, - username=username, - password=password, - tenant=tenant) - - def side_effect_func(url, body): - return body - mock = Mock() - mock.side_effect = side_effect_func - authObj._authenticate = mock - body = authObj._v2_auth(Mock()) - self.assertEqual(username, - body['auth']['passwordCredentials']['username']) - self.assertEqual(password, - body['auth']['passwordCredentials']['password']) - self.assertEqual(tenant, body['auth']['tenantName']) - - -class Auth1_1Test(TestCase): - - def test_authenticate(self): - # handle when url is None - check_url_none(self, auth.Auth1_1) - - # url is not none - username = "reddwarf_user" - password = "reddwarf_password" - url = "test_url" - authObj = auth.Auth1_1(url=url, - type=auth.Auth1_1, - client=None, username=username, - password=password, tenant=None) - - def side_effect_func(auth_url, body, root_key): - return auth_url, body, root_key - - mock = Mock() - mock.side_effect = side_effect_func - authObj._authenticate = mock - auth_url, body, root_key = authObj.authenticate() - - self.assertEqual(username, body['credentials']['username']) - self.assertEqual(password, body['credentials']['key']) - self.assertEqual(auth_url, url) - self.assertEqual('auth', root_key) - - -class RaxAuthenticatorTest(TestCase): - - def test_authenticate(self): - # url is None - check_url_none(self, auth.RaxAuthenticator) - - # url is not None, so it must not throw exception - url = "test_url" - authObj = auth.RaxAuthenticator(url=url, - type=auth.RaxAuthenticator, - client=None, username=None, - password=None, tenant=None) - - def side_effect_func(url): - return url - - mock = Mock() - mock.side_effect = side_effect_func - authObj._rax_auth = mock - r = authObj.authenticate() - self.assertEqual(url, r) - - def test__rax_auth(self): - username = "reddwarf_user" - password = "reddwarf_password" - tenant = "tenant" - authObj = auth.RaxAuthenticator(url=None, - type=auth.RaxAuthenticator, - client=None, username=username, - password=password, tenant=tenant) - - def side_effect_func(url, body): - return body - - mock = Mock() - mock.side_effect = side_effect_func - authObj._authenticate = mock - body = authObj._rax_auth(Mock()) - - v = body['auth']['RAX-KSKEY:apiKeyCredentials']['username'] - self.assertEqual(username, v) - - v = body['auth']['RAX-KSKEY:apiKeyCredentials']['apiKey'] - self.assertEqual(password, v) - - v = body['auth']['RAX-KSKEY:apiKeyCredentials']['tenantName'] - self.assertEqual(tenant, v) - - -class FakeAuthTest(TestCase): - - def test_authenticate(self): - tenant = "tenant" - authObj = auth.FakeAuth(url=None, - type=auth.FakeAuth, - client=None, username=None, - password=None, tenant=tenant) - - fc = authObj.authenticate() - public_url = "%s/%s" % ('http://localhost:8779/v1.0', tenant) - self.assertEqual(public_url, fc.get_public_url()) - self.assertEqual(tenant, fc.get_token()) - - -class ServiceCatalogTest(TestCase): - - def setUp(self): - super(ServiceCatalogTest, self).setUp() - self.orig_url_for = auth.ServiceCatalog._url_for - self.orig__init__ = auth.ServiceCatalog.__init__ - auth.ServiceCatalog.__init__ = Mock(return_value=None) - self.test_url = "http://localhost:1234/test" - - def tearDown(self): - super(ServiceCatalogTest, self).tearDown() - auth.ServiceCatalog._url_for = self.orig_url_for - auth.ServiceCatalog.__init__ = self.orig__init__ - - def test__load(self): - url = "random_url" - auth.ServiceCatalog._url_for = Mock(return_value=url) - - # when service_url is None - scObj = auth.ServiceCatalog() - scObj.region = None - scObj.service_url = None - scObj._load() - self.assertEqual(url, scObj.public_url) - self.assertEqual(url, scObj.management_url) - - # service url is not None - service_url = "service_url" - scObj = auth.ServiceCatalog() - scObj.region = None - scObj.service_url = service_url - scObj._load() - self.assertEqual(service_url, scObj.public_url) - self.assertEqual(service_url, scObj.management_url) - - def test_get_token(self): - test_id = "test_id" - scObj = auth.ServiceCatalog() - scObj.root_key = "root_key" - scObj.catalog = dict() - scObj.catalog[scObj.root_key] = dict() - scObj.catalog[scObj.root_key]['token'] = dict() - scObj.catalog[scObj.root_key]['token']['id'] = test_id - self.assertEqual(test_id, scObj.get_token()) - - def test_get_management_url(self): - test_mng_url = "test_management_url" - scObj = auth.ServiceCatalog() - scObj.management_url = test_mng_url - self.assertEqual(test_mng_url, scObj.get_management_url()) - - def test_get_public_url(self): - test_public_url = "test_public_url" - scObj = auth.ServiceCatalog() - scObj.public_url = test_public_url - self.assertEqual(test_public_url, scObj.get_public_url()) - - def test__url_for(self): - scObj = auth.ServiceCatalog() - - # case for no endpoint found - self.case_no_endpoint_match(scObj) - - # case for empty service catalog - self.case_endpoing_with_empty_catalog(scObj) - - # more than one matching endpoints - self.case_ambiguous_endpoint(scObj) - - # happy case - self.case_unique_endpoint(scObj) - - # testing if-statements in for-loop to iterate services in catalog - self.case_iterating_services_in_catalog(scObj) - - def case_no_endpoint_match(self, scObj): - # empty endpoint list - scObj.catalog = dict() - scObj.catalog['endpoints'] = list() - self.assertRaises(exceptions.EndpointNotFound, scObj._url_for) - - def side_effect_func_ep(attr): - return "test_attr_value" - - # simulating dict - endpoint = Mock() - mock = Mock() - mock.side_effect = side_effect_func_ep - endpoint.__getitem__ = mock - scObj.catalog['endpoints'].append(endpoint) - - # not-empty list but not matching endpoint - filter_value = "not_matching_value" - self.assertRaises(exceptions.EndpointNotFound, scObj._url_for, - attr="test_attr", filter_value=filter_value) - - filter_value = "test_attr_value" # so that we have an endpoint match - scObj.root_key = "access" - scObj.catalog[scObj.root_key] = dict() - self.assertRaises(exceptions.EndpointNotFound, scObj._url_for, - attr="test_attr", filter_value=filter_value) - - def case_endpoing_with_empty_catalog(self, scObj): - # first, test with empty catalog, this should pass since - # there is already enpoint added - scObj.catalog[scObj.root_key]['serviceCatalog'] = list() - - endpoint = scObj.catalog['endpoints'][0] - endpoint.get = Mock(return_value=self.test_url) - r_url = scObj._url_for(attr="test_attr", - filter_value="test_attr_value") - self.assertEqual(self.test_url, r_url) - - def case_ambiguous_endpoint(self, scObj): - scObj.service_type = "reddwarf" - scObj.service_name = "test_service_name" - - def side_effect_func_service(key): - if key == "type": - return "reddwarf" - elif key == "name": - return "test_service_name" - return None - - mock1 = Mock() - mock1.side_effect = side_effect_func_service - service1 = Mock() - service1.get = mock1 - - endpoint2 = {"test_attr": "test_attr_value"} - service1.__getitem__ = Mock(return_value=[endpoint2]) - scObj.catalog[scObj.root_key]['serviceCatalog'] = [service1] - self.assertRaises(exceptions.AmbiguousEndpoints, scObj._url_for, - attr="test_attr", filter_value="test_attr_value") - - def case_unique_endpoint(self, scObj): - # changing the endpoint2 attribute to pass the filter - service1 = scObj.catalog[scObj.root_key]['serviceCatalog'][0] - endpoint2 = service1[0][0] - endpoint2["test_attr"] = "new value not matching filter" - r_url = scObj._url_for(attr="test_attr", - filter_value="test_attr_value") - self.assertEqual(self.test_url, r_url) - - def case_iterating_services_in_catalog(self, scObj): - service1 = scObj.catalog[scObj.root_key]['serviceCatalog'][0] - - scObj.catalog = dict() - scObj.root_key = "access" - scObj.catalog[scObj.root_key] = dict() - scObj.service_type = "no_match" - - scObj.catalog[scObj.root_key]['serviceCatalog'] = [service1] - self.assertRaises(exceptions.EndpointNotFound, scObj._url_for) - - scObj.service_type = "database" - scObj.service_name = "no_match" - self.assertRaises(exceptions.EndpointNotFound, scObj._url_for) - - # no endpoints and no 'serviceCatalog' in catalog => raise exception - scObj = auth.ServiceCatalog() - scObj.catalog = dict() - scObj.root_key = "access" - scObj.catalog[scObj.root_key] = dict() - scObj.catalog[scObj.root_key]['serviceCatalog'] = [] - self.assertRaises(exceptions.EndpointNotFound, scObj._url_for, - attr="test_attr", filter_value="test_attr_value") diff --git a/reddwarfclient/tests/test_base.py b/reddwarfclient/tests/test_base.py deleted file mode 100644 index 5cbd590..0000000 --- a/reddwarfclient/tests/test_base.py +++ /dev/null @@ -1,447 +0,0 @@ -import contextlib -import os - -from testtools import TestCase -from mock import Mock - -from reddwarfclient import base -from reddwarfclient import exceptions -from reddwarfclient import utils - -""" -Unit tests for base.py -""" - - -def obj_class(self, res, loaded=True): - return res - - -class BaseTest(TestCase): - - def test_getid(self): - obj = "test" - r = base.getid(obj) - self.assertEqual(obj, r) - - test_id = "test_id" - obj = Mock() - obj.id = test_id - r = base.getid(obj) - self.assertEqual(test_id, r) - - -class ManagerTest(TestCase): - - def setUp(self): - super(ManagerTest, self).setUp() - self.orig__init = base.Manager.__init__ - base.Manager.__init__ = Mock(return_value=None) - self.orig_os_makedirs = os.makedirs - - def tearDown(self): - super(ManagerTest, self).tearDown() - base.Manager.__init__ = self.orig__init - os.makedirs = self.orig_os_makedirs - - def test___init__(self): - api = Mock() - base.Manager.__init__ = self.orig__init - manager = base.Manager(api) - self.assertEqual(api, manager.api) - - def test_completion_cache(self): - manager = base.Manager() - - # handling exceptions - mode = "w" - cache_type = "unittest" - obj_class = Mock - with manager.completion_cache(cache_type, obj_class, mode): - pass - - os.makedirs = Mock(side_effect=OSError) - with manager.completion_cache(cache_type, obj_class, mode): - pass - - def test_write_to_completion_cache(self): - manager = base.Manager() - - # no cache object, nothing should happen - manager.write_to_completion_cache("non-exist", "val") - - def side_effect_func(val): - return val - - manager._mock_cache = Mock() - manager._mock_cache.write = Mock(return_value=None) - manager.write_to_completion_cache("mock", "val") - self.assertEqual(1, manager._mock_cache.write.call_count) - - def _get_mock(self): - manager = base.Manager() - manager.api = Mock() - manager.api.client = Mock() - - def side_effect_func(self, body, loaded=True): - return body - - manager.resource_class = Mock(side_effect=side_effect_func) - return manager - - def test__get_with_response_key_none(self): - manager = self._get_mock() - url_ = "test-url" - body_ = "test-body" - resp_ = "test-resp" - manager.api.client.get = Mock(return_value=(resp_, body_)) - r = manager._get(url=url_, response_key=None) - self.assertEqual(body_, r) - - def test__get_with_response_key(self): - manager = self._get_mock() - response_key = "response_key" - body_ = {response_key: "test-resp-key-body"} - url_ = "test_url_get" - manager.api.client.get = Mock(return_value=(url_, body_)) - r = manager._get(url=url_, response_key=response_key) - self.assertEqual(body_[response_key], r) - - def test__create(self): - manager = base.Manager() - manager.api = Mock() - manager.api.client = Mock() - - response_key = "response_key" - data_ = "test-data" - body_ = {response_key: data_} - url_ = "test_url_post" - manager.api.client.post = Mock(return_value=(url_, body_)) - - return_raw = True - r = manager._create(url_, body_, response_key, return_raw) - self.assertEqual(data_, r) - - return_raw = False - - @contextlib.contextmanager - def completion_cache_mock(*arg, **kwargs): - yield - - mock = Mock() - mock.side_effect = completion_cache_mock - manager.completion_cache = mock - - manager.resource_class = Mock(return_value="test-class") - r = manager._create(url_, body_, response_key, return_raw) - self.assertEqual("test-class", r) - - def get_mock_mng_api_client(self): - manager = base.Manager() - manager.api = Mock() - manager.api.client = Mock() - return manager - - def test__delete(self): - resp_ = "test-resp" - body_ = "test-body" - - manager = self.get_mock_mng_api_client() - manager.api.client.delete = Mock(return_value=(resp_, body_)) - # _delete just calls api.client.delete, and does nothing - # the correctness should be tested in api class - manager._delete("test-url") - pass - - def test__update(self): - resp_ = "test-resp" - body_ = "test-body" - - manager = self.get_mock_mng_api_client() - manager.api.client.put = Mock(return_value=(resp_, body_)) - body = manager._update("test-url", body_) - self.assertEqual(body_, body) - - -class ManagerListTest(ManagerTest): - - def setUp(self): - super(ManagerListTest, self).setUp() - - @contextlib.contextmanager - def completion_cache_mock(*arg, **kwargs): - yield - - self.manager = base.Manager() - self.manager.api = Mock() - self.manager.api.client = Mock() - - self.response_key = "response_key" - self.data_p = ["p1", "p2"] - self.body_p = {self.response_key: self.data_p} - self.url_p = "test_url_post" - self.manager.api.client.post = Mock(return_value=(self.url_p, - self.body_p)) - - self.data_g = ["g1", "g2", "g3"] - self.body_g = {self.response_key: self.data_g} - self.url_g = "test_url_get" - self.manager.api.client.get = Mock(return_value=(self.url_g, - self.body_g)) - - mock = Mock() - mock.side_effect = completion_cache_mock - self.manager.completion_cache = mock - - def tearDown(self): - super(ManagerListTest, self).tearDown() - - def obj_class(self, res, loaded=True): - return res - - def test_list_with_body_none(self): - body = None - l = self.manager._list("url", self.response_key, obj_class, body) - self.assertEqual(len(self.data_g), len(l)) - for i in range(0, len(l)): - self.assertEqual(self.data_g[i], l[i]) - - def test_list_body_not_none(self): - body = "something" - l = self.manager._list("url", self.response_key, obj_class, body) - self.assertEqual(len(self.data_p), len(l)) - for i in range(0, len(l)): - self.assertEqual(self.data_p[i], l[i]) - - def test_list_key_mapping(self): - data_ = {"values": ["p1", "p2"]} - body_ = {self.response_key: data_} - url_ = "test_url_post" - self.manager.api.client.post = Mock(return_value=(url_, body_)) - l = self.manager._list("url", self.response_key, - obj_class, "something") - data = data_["values"] - self.assertEqual(len(data), len(l)) - for i in range(0, len(l)): - self.assertEqual(data[i], l[i]) - - def test_list_without_key_mapping(self): - data_ = {"v1": "1", "v2": "2"} - body_ = {self.response_key: data_} - url_ = "test_url_post" - self.manager.api.client.post = Mock(return_value=(url_, body_)) - l = self.manager._list("url", self.response_key, - obj_class, "something") - self.assertEqual(len(data_), len(l)) - - -class ManagerWithFind(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() - - def tearDown(self): - super(ManagerWithFind, self).tearDown() - base.ManagerWithFind.__init__ = self.orig__init - - def test_find(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]) - self.manager.resource_class = Mock - - # exactly one match case - found = self.manager.find(attr1="v1", attr2="v2", attr3="v3") - self.assertEqual(obj1, found) - - # 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) - - -class ResourceTest(TestCase): - - def setUp(self): - super(ResourceTest, self).setUp() - self.orig___init__ = base.Resource.__init__ - - def tearDown(self): - super(ResourceTest, self).tearDown() - base.Resource.__init__ = self.orig___init__ - - def test___init__(self): - manager = Mock() - manager.write_to_completion_cache = Mock(return_value=None) - - info_ = {} - robj = base.Resource(manager, info_) - self.assertEqual(0, manager.write_to_completion_cache.call_count) - - info_ = {"id": "id-with-less-than-36-char"} - robj = base.Resource(manager, info_) - self.assertEqual(info_["id"], robj.id) - self.assertEqual(0, manager.write_to_completion_cache.call_count) - - id_ = "id-with-36-char-" - for i in range(36 - len(id_)): - id_ = id_ + "-" - info_ = {"id": id_} - robj = base.Resource(manager, info_) - self.assertEqual(info_["id"], robj.id) - self.assertEqual(1, manager.write_to_completion_cache.call_count) - - info_["name"] = "test-human-id" - # Resource.HUMAN_ID is False - robj = base.Resource(manager, info_) - self.assertEqual(info_["id"], robj.id) - self.assertEqual(None, robj.human_id) - self.assertEqual(2, manager.write_to_completion_cache.call_count) - - # base.Resource.HUMAN_ID = True - info_["HUMAN_ID"] = True - robj = base.Resource(manager, info_) - self.assertEqual(info_["id"], robj.id) - self.assertEqual(info_["name"], robj.human_id) - self.assertEqual(4, manager.write_to_completion_cache.call_count) - - def test_human_id(self): - manager = Mock() - manager.write_to_completion_cache = Mock(return_value=None) - - info_ = {"name": "test-human-id"} - robj = base.Resource(manager, info_) - self.assertEqual(None, robj.human_id) - - info_["HUMAN_ID"] = True - robj = base.Resource(manager, info_) - self.assertEqual(info_["name"], robj.human_id) - robj.name = "new-human-id" - self.assertEqual("new-human-id", robj.human_id) - - def get_mock_resource_obj(self): - base.Resource.__init__ = Mock(return_value=None) - robj = base.Resource() - return robj - - def test__add_details(self): - robj = self.get_mock_resource_obj() - info_ = {"name": "test-human-id", "test_attr": 5} - robj._add_details(info_) - self.assertEqual(info_["name"], robj.name) - self.assertEqual(info_["test_attr"], robj.test_attr) - - def test___getattr__(self): - robj = self.get_mock_resource_obj() - info_ = {"name": "test-human-id", "test_attr": 5} - robj._add_details(info_) - self.assertEqual(info_["test_attr"], robj.__getattr__("test_attr")) - - # TODO: looks like causing infinite recursive calls - #robj.__getattr__("test_non_exist_attr") - - def test___repr__(self): - robj = self.get_mock_resource_obj() - info_ = {"name": "test-human-id", "test_attr": 5} - robj._add_details(info_) - - expected = "" - self.assertEqual(expected, robj.__repr__()) - - def test_get(self): - robj = self.get_mock_resource_obj() - manager = Mock() - manager.get = None - - robj.manager = object() - robj.get() - - manager = Mock() - robj.manager = Mock() - - robj.id = "id" - new = Mock() - new._info = {"name": "test-human-id", "test_attr": 5} - robj.manager.get = Mock(return_value=new) - robj.get() - self.assertEqual("test-human-id", robj.name) - self.assertEqual(5, robj.test_attr) - - def tes___eq__(self): - robj = self.get_mock_resource_obj() - other = base.Resource() - - info_ = {"name": "test-human-id", "test_attr": 5} - robj._info = info_ - other._info = {} - self.assertNotTrue(robj.__eq__(other)) - - robj._info = info_ - self.assertTrue(robj.__eq__(other)) - - robj.id = "rid" - other.id = "oid" - self.assertNotTrue(robj.__eq__(other)) - - other.id = "rid" - self.assertTrue(robj.__eq__(other)) - - # not instance of the same class - other = Mock() - self.assertNotTrue(robj.__eq__(other)) - - def test_is_loaded(self): - robj = self.get_mock_resource_obj() - robj._loaded = True - self.assertTrue(robj.is_loaded()) - - robj._loaded = False - self.assertFalse(robj.is_loaded()) - - def test_set_loaded(self): - robj = self.get_mock_resource_obj() - robj.set_loaded(True) - self.assertTrue(robj._loaded) - - robj.set_loaded(False) - self.assertFalse(robj._loaded) diff --git a/reddwarfclient/tests/test_client.py b/reddwarfclient/tests/test_client.py deleted file mode 100644 index d52331f..0000000 --- a/reddwarfclient/tests/test_client.py +++ /dev/null @@ -1,322 +0,0 @@ -import contextlib -import os -import logging -import httplib2 -import time - -from testtools import TestCase -from mock import Mock - -from reddwarfclient import client -from reddwarfclient import exceptions -from reddwarfclient import utils - -""" -Unit tests for client.py -""" - - -class ClientTest(TestCase): - - def test_log_to_streamhandler(self): - client.log_to_streamhandler() - self.assertTrue(client._logger.level == logging.DEBUG) - - -class ReddwarfHTTPClientTest(TestCase): - - def setUp(self): - super(ReddwarfHTTPClientTest, self).setUp() - self.orig__init = client.ReddwarfHTTPClient.__init__ - client.ReddwarfHTTPClient.__init__ = Mock(return_value=None) - self.hc = client.ReddwarfHTTPClient() - 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(ReddwarfHTTPClientTest, self).tearDown() - client.ReddwarfHTTPClient.__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.ReddwarfHTTPClient.__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.ReddwarfHTTPClient, user, - password, tenant, auth_url, service_name) - - hc = client.ReddwarfHTTPClient(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.ReddwarfHTTPClient(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(ValueError, 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(ValueError, - 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.ReddwarfHTTPClient.__init__ - client.ReddwarfHTTPClient.__init__ = Mock(return_value=None) - self.dbaas = client.Dbaas("user", "api-key") - - def tearDown(self): - super(DbaasTest, self).tearDown() - client.ReddwarfHTTPClient.__init__ = self.orig__init - - def test___init__(self): - client.ReddwarfHTTPClient.__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) diff --git a/reddwarfclient/tests/test_common.py b/reddwarfclient/tests/test_common.py deleted file mode 100644 index b038a05..0000000 --- a/reddwarfclient/tests/test_common.py +++ /dev/null @@ -1,395 +0,0 @@ -import sys -import optparse -import json -import collections - -from testtools import TestCase -from mock import Mock - -from reddwarfclient import common -from reddwarfclient import client - -""" - unit tests for common.py -""" - - -class CommonTest(TestCase): - - def setUp(self): - super(CommonTest, self).setUp() - self.orig_sys_exit = sys.exit - sys.exit = Mock(return_value=None) - - def tearDown(self): - super(CommonTest, self).tearDown() - sys.exit = self.orig_sys_exit - - def test_methods_of(self): - class DummyClass: - def dummyMethod(self): - print("just for test") - - obj = DummyClass() - result = common.methods_of(obj) - self.assertEqual(1, len(result)) - method = result['dummyMethod'] - self.assertIsNotNone(method) - - def test_check_for_exceptions(self): - status = [400, 422, 500] - for s in status: - resp = Mock() - resp.status = s - self.assertRaises(ValueError, - common.check_for_exceptions, resp, "body") - - # a no-exception case - resp = Mock() - resp.status = 200 - common.check_for_exceptions(resp, "body") - - def test_print_actions(self): - cmd = "test-cmd" - actions = {"test": "test action", "help": "help action"} - common.print_actions(cmd, actions) - pass - - def test_print_commands(self): - commands = {"cmd-1": "cmd 1", "cmd-2": "cmd 2"} - common.print_commands(commands) - pass - - def test_limit_url(self): - url_ = "test-url" - limit_ = None - marker_ = None - self.assertEqual(url_, common.limit_url(url_)) - - limit_ = "test-limit" - marker_ = "test-marker" - expected = "test-url?marker=test-marker&limit=test-limit" - self.assertEqual(expected, - common.limit_url(url_, limit=limit_, marker=marker_)) - - -class CliOptionsTest(TestCase): - - def check_default_options(self, co): - self.assertEqual(None, co.username) - self.assertEqual(None, co.apikey) - self.assertEqual(None, co.tenant_id) - self.assertEqual(None, co.auth_url) - self.assertEqual('keystone', co.auth_type) - self.assertEqual('database', co.service_type) - self.assertEqual('reddwarf', co.service_name) - self.assertEqual('RegionOne', co.region) - self.assertEqual(None, co.service_url) - self.assertFalse(co.insecure) - self.assertFalse(co.verbose) - self.assertFalse(co.debug) - self.assertEqual(None, co.token) - self.assertEqual(None, co.xml) - - def check_option(self, oparser, option_name): - option = oparser.get_option("--%s" % option_name) - self.assertNotEqual(None, option) - if option_name in common.CliOptions.DEFAULT_VALUES: - self.assertEqual(common.CliOptions.DEFAULT_VALUES[option_name], - option.default) - - def test___init__(self): - co = common.CliOptions() - self.check_default_options(co) - - def test_deafult(self): - co = common.CliOptions.default() - self.check_default_options(co) - - def test_load_from_file(self): - co = common.CliOptions.load_from_file() - self.check_default_options(co) - - def test_create_optparser(self): - option_names = ["verbose", "debug", "auth_url", "username", "apikey", - "tenant_id", "auth_type", "service_type", - "service_name", "service_type", "service_name", - "service_url", "region", "insecure", "token", - "xml", "secure", "json", "terse", "hide-debug"] - - oparser = common.CliOptions.create_optparser(True) - for option_name in option_names: - self.check_option(oparser, option_name) - - oparser = common.CliOptions.create_optparser(False) - for option_name in option_names: - self.check_option(oparser, option_name) - - -class ArgumentRequiredTest(TestCase): - - def setUp(self): - super(ArgumentRequiredTest, self).setUp() - self.param = "test-param" - self.arg_req = common.ArgumentRequired(self.param) - - def test___init__(self): - self.assertEqual(self.param, self.arg_req.param) - - def test___str__(self): - expected = 'Argument "--%s" required.' % self.param - self.assertEqual(expected, self.arg_req.__str__()) - - -class CommandsBaseTest(TestCase): - - def setUp(self): - super(CommandsBaseTest, self).setUp() - self.orig_sys_exit = sys.exit - sys.exit = Mock(return_value=None) - parser = common.CliOptions().create_optparser(False) - self.cmd_base = common.CommandsBase(parser) - - def tearDown(self): - super(CommandsBaseTest, self).tearDown() - sys.exit = self.orig_sys_exit - - 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 - r = self.cmd_base._safe_exec(func) - self.assertEqual("test", r) - - self.cmd_base.debug = False - r = self.cmd_base._safe_exec(func) - self.assertEqual("test", r) - - func = Mock(side_effect=ValueError) # an arbitrary exception - r = self.cmd_base._safe_exec(func) - self.assertEqual(None, r) - - def test__prepare_parser(self): - parser = optparse.OptionParser() - common.CommandsBase.params = ["test_1", "test_2"] - self.cmd_base._prepare_parser(parser) - option = parser.get_option("--%s" % "test_1") - self.assertNotEqual(None, option) - option = parser.get_option("--%s" % "test_2") - self.assertNotEqual(None, option) - - def test__parse_options(self): - parser = optparse.OptionParser() - parser.add_option("--%s" % "test_1", default="test_1v") - parser.add_option("--%s" % "test_2", default="test_2v") - self.cmd_base._parse_options(parser) - self.assertEqual("test_1v", self.cmd_base.test_1) - self.assertEqual("test_2v", self.cmd_base.test_2) - - def test__require(self): - self.assertRaises(common.ArgumentRequired, - self.cmd_base._require, "attr_1") - self.cmd_base.attr_1 = None - self.assertRaises(common.ArgumentRequired, - self.cmd_base._require, "attr_1") - self.cmd_base.attr_1 = "attr_v1" - self.cmd_base._require("attr_1") - - def test__make_list(self): - self.assertRaises(AttributeError, self.cmd_base._make_list, "attr1") - self.cmd_base.attr1 = "v1,v2" - self.cmd_base._make_list("attr1") - self.assertEqual(["v1", "v2"], self.cmd_base.attr1) - self.cmd_base.attr1 = ["v3"] - self.cmd_base._make_list("attr1") - self.assertEqual(["v3"], self.cmd_base.attr1) - - def test__pretty_print(self): - func = Mock(return_value=None) - self.cmd_base.verbose = True - self.assertEqual(None, self.cmd_base._pretty_print(func)) - self.cmd_base.verbose = False - self.assertEqual(None, self.cmd_base._pretty_print(func)) - - def test__dumps(self): - json.dumps = Mock(return_value="test-dump") - self.assertEqual("test-dump", self.cmd_base._dumps("item")) - - def test__pretty_list(self): - func = Mock(return_value=None) - self.cmd_base.verbose = True - self.assertEqual(None, self.cmd_base._pretty_list(func)) - self.cmd_base.verbose = False - self.assertEqual(None, self.cmd_base._pretty_list(func)) - item = Mock(return_value="test") - item._info = "info" - func = Mock(return_value=[item]) - self.assertEqual(None, self.cmd_base._pretty_list(func)) - - def test__pretty_paged(self): - self.cmd_base.limit = "5" - func = Mock(return_value=None) - self.cmd_base.verbose = True - self.assertEqual(None, self.cmd_base._pretty_paged(func)) - - self.cmd_base.verbose = False - - class MockIterable(collections.Iterable): - links = ["item"] - count = 1 - - def __iter__(self): - return ["item1"] - - def __len__(self): - return count - - ret = MockIterable() - func = Mock(return_value=ret) - self.assertEqual(None, self.cmd_base._pretty_paged(func)) - - ret.count = 0 - self.assertEqual(None, self.cmd_base._pretty_paged(func)) - - func = Mock(side_effect=ValueError) - self.assertEqual(None, self.cmd_base._pretty_paged(func)) - self.cmd_base.debug = True - self.cmd_base.marker = Mock() - self.assertRaises(ValueError, self.cmd_base._pretty_paged, func) - - -class AuthTest(TestCase): - - def setUp(self): - super(AuthTest, self).setUp() - self.orig_sys_exit = sys.exit - sys.exit = Mock(return_value=None) - self.parser = common.CliOptions().create_optparser(False) - self.auth = common.Auth(self.parser) - - def tearDown(self): - super(AuthTest, self).tearDown() - sys.exit = self.orig_sys_exit - - def test___init__(self): - self.assertEqual(None, self.auth.dbaas) - self.assertEqual(None, self.auth.apikey) - - def test_login(self): - self.auth.username = "username" - self.auth.apikey = "apikey" - self.auth.tenant_id = "tenant_id" - self.auth.auth_url = "auth_url" - dbaas = Mock() - dbaas.authenticate = Mock(return_value=None) - dbaas.client = Mock() - dbaas.client.auth_token = Mock() - dbaas.client.service_url = Mock() - self.auth._get_client = Mock(return_value=dbaas) - self.auth.login() - - self.auth.debug = True - self.auth._get_client = Mock(side_effect=ValueError) - self.assertRaises(ValueError, self.auth.login) - - self.auth.debug = False - self.auth.login() - - -class AuthedCommandsBaseTest(TestCase): - - def setUp(self): - super(AuthedCommandsBaseTest, self).setUp() - self.orig_sys_exit = sys.exit - sys.exit = Mock(return_value=None) - - def tearDown(self): - super(AuthedCommandsBaseTest, self).tearDown() - sys.exit = self.orig_sys_exit - - def test___init__(self): - parser = common.CliOptions().create_optparser(False) - common.AuthedCommandsBase.debug = True - dbaas = Mock() - dbaas.authenticate = Mock(return_value=None) - dbaas.client = Mock() - dbaas.client.auth_token = Mock() - dbaas.client.service_url = Mock() - dbaas.client.authenticate_with_token = Mock() - common.AuthedCommandsBase._get_client = Mock(return_value=dbaas) - authed_cmd = common.AuthedCommandsBase(parser) - - -class PaginatedTest(TestCase): - - def setUp(self): - super(PaginatedTest, self).setUp() - self.items_ = ["item1", "item2"] - self.next_marker_ = "next-marker" - self.links_ = ["link1", "link2"] - self.pgn = common.Paginated(self.items_, self.next_marker_, - self.links_) - - def tearDown(self): - super(PaginatedTest, self).tearDown() - - def test___init__(self): - self.assertEqual(self.items_, self.pgn.items) - self.assertEqual(self.next_marker_, self.pgn.next) - self.assertEqual(self.links_, self.pgn.links) - - def test___len__(self): - self.assertEqual(len(self.items_), self.pgn.__len__()) - - def test___iter__(self): - itr_expected = self.items_.__iter__() - itr = self.pgn.__iter__() - self.assertEqual(itr_expected.next(), itr.next()) - self.assertEqual(itr_expected.next(), itr.next()) - self.assertRaises(StopIteration, itr_expected.next) - self.assertRaises(StopIteration, itr.next) - - def test___getitem__(self): - self.assertEqual(self.items_[0], self.pgn.__getitem__(0)) - - def test___setitem__(self): - self.pgn.__setitem__(0, "new-item") - self.assertEqual("new-item", self.pgn.items[0]) - - def test___delitem(self): - del self.pgn[0] - self.assertEqual(1, self.pgn.__len__()) - - def test___reversed__(self): - itr = self.pgn.__reversed__() - expected = ["item2", "item1"] - self.assertEqual("item2", itr.next()) - self.assertEqual("item1", itr.next()) - self.assertRaises(StopIteration, itr.next) - - def test___contains__(self): - self.assertTrue(self.pgn.__contains__("item1")) - self.assertTrue(self.pgn.__contains__("item2")) - self.assertFalse(self.pgn.__contains__("item3")) diff --git a/reddwarfclient/tests/test_instances.py b/reddwarfclient/tests/test_instances.py deleted file mode 100644 index 2131d26..0000000 --- a/reddwarfclient/tests/test_instances.py +++ /dev/null @@ -1,176 +0,0 @@ -from testtools import TestCase -from mock import Mock - -from reddwarfclient import instances -from reddwarfclient import base - -""" -Unit tests for instances.py -""" - - -class InstanceTest(TestCase): - - def setUp(self): - super(InstanceTest, self).setUp() - self.orig__init = instances.Instance.__init__ - instances.Instance.__init__ = Mock(return_value=None) - self.instance = instances.Instance() - self.instance.manager = Mock() - - def tearDown(self): - super(InstanceTest, self).tearDown() - instances.Instance.__init__ = self.orig__init - - def test___repr__(self): - self.instance.name = "instance-1" - self.assertEqual('', self.instance.__repr__()) - - def test_list_databases(self): - db_list = ['database1', 'database2'] - self.instance.manager.databases = Mock() - self.instance.manager.databases.list = Mock(return_value=db_list) - self.assertEqual(db_list, self.instance.list_databases()) - - def test_delete(self): - db_delete_mock = Mock(return_value=None) - self.instance.manager.delete = db_delete_mock - self.instance.delete() - self.assertEqual(1, db_delete_mock.call_count) - - def test_restart(self): - db_restart_mock = Mock(return_value=None) - self.instance.manager.restart = db_restart_mock - self.instance.id = 1 - self.instance.restart() - self.assertEqual(1, db_restart_mock.call_count) - - -class InstancesTest(TestCase): - - def setUp(self): - super(InstancesTest, self).setUp() - self.orig__init = instances.Instances.__init__ - instances.Instances.__init__ = Mock(return_value=None) - self.instances = instances.Instances() - self.instances.api = Mock() - self.instances.api.client = Mock() - self.instances.resource_class = Mock(return_value="instance-1") - - self.orig_base_getid = base.getid - base.getid = Mock(return_value="instance1") - - def tearDown(self): - super(InstancesTest, self).tearDown() - instances.Instances.__init__ = self.orig__init - base.getid = self.orig_base_getid - - def test_create(self): - def side_effect_func(path, body, inst): - return path, body, inst - - self.instances._create = Mock(side_effect=side_effect_func) - p, b, i = self.instances.create("test-name", 103, "test-volume", - ['db1', 'db2'], ['u1', 'u2']) - self.assertEqual("/instances", p) - self.assertEqual("instance", i) - self.assertEqual(['db1', 'db2'], b["instance"]["databases"]) - self.assertEqual(['u1', 'u2'], b["instance"]["users"]) - self.assertEqual("test-name", b["instance"]["name"]) - self.assertEqual("test-volume", b["instance"]["volume"]) - self.assertEqual(103, b["instance"]["flavorRef"]) - - def test__list(self): - self.instances.api.client.get = Mock(return_value=('resp', None)) - self.assertRaises(Exception, self.instances._list, "url", None) - - body = Mock() - body.get = Mock(return_value=[{'href': 'http://test.net/test_file', - 'rel': 'next'}]) - body.__getitem__ = Mock(return_value='instance1') - #self.instances.resource_class = Mock(return_value="instance-1") - self.instances.api.client.get = Mock(return_value=('resp', body)) - _expected = [{'href': 'http://test.net/test_file', 'rel': 'next'}] - self.assertEqual(_expected, self.instances._list("url", None).links) - - def test_list(self): - def side_effect_func(path, inst, limit, marker): - return path, inst, limit, marker - - self.instances._list = Mock(side_effect=side_effect_func) - limit_ = "test-limit" - marker_ = "test-marker" - expected = ("/instances", "instances", limit_, marker_) - self.assertEqual(expected, self.instances.list(limit_, marker_)) - - def test_get(self): - def side_effect_func(path, inst): - return path, inst - - self.instances._get = Mock(side_effect=side_effect_func) - self.assertEqual(('/instances/instance1', 'instance'), - self.instances.get(1)) - - def test_delete(self): - resp = Mock() - resp.status = 200 - body = None - self.instances.api.client.delete = Mock(return_value=(resp, body)) - self.instances.delete('instance1') - resp.status = 500 - self.assertRaises(ValueError, self.instances.delete, 'instance1') - - def test__action(self): - body = Mock() - resp = Mock() - resp.status = 200 - self.instances.api.client.post = Mock(return_value=(resp, body)) - self.assertEqual('instance-1', self.instances._action(1, body)) - - self.instances.api.client.post = Mock(return_value=(resp, None)) - self.assertEqual(None, self.instances._action(1, body)) - - def _set_action_mock(self): - def side_effect_func(instance_id, body): - self._instance_id = instance_id - self._body = body - - self._instance_id = None - self._body = None - self.instances._action = Mock(side_effect=side_effect_func) - - def test_resize_volume(self): - self._set_action_mock() - self.instances.resize_volume(152, 512) - self.assertEqual(152, self._instance_id) - self.assertEqual({"resize": {"volume": {"size": 512}}}, self._body) - - def test_resize_instance(self): - self._set_action_mock() - self.instances.resize_instance(4725, 103) - self.assertEqual(4725, self._instance_id) - self.assertEqual({"resize": {"flavorRef": 103}}, self._body) - - def test_restart(self): - self._set_action_mock() - self.instances.restart(253) - self.assertEqual(253, self._instance_id) - self.assertEqual({'restart': {}}, self._body) - - def test_reset_password(self): - self._set_action_mock() - self.instances.reset_password(634) - self.assertEqual(634, self._instance_id) - self.assertEqual({'reset-password': {}}, self._body) - - -class InstanceStatusTest(TestCase): - - def test_constants(self): - self.assertEqual("ACTIVE", instances.InstanceStatus.ACTIVE) - self.assertEqual("BLOCKED", instances.InstanceStatus.BLOCKED) - self.assertEqual("BUILD", instances.InstanceStatus.BUILD) - self.assertEqual("FAILED", instances.InstanceStatus.FAILED) - self.assertEqual("REBOOT", instances.InstanceStatus.REBOOT) - self.assertEqual("RESIZE", instances.InstanceStatus.RESIZE) - self.assertEqual("SHUTDOWN", instances.InstanceStatus.SHUTDOWN) diff --git a/reddwarfclient/tests/test_limits.py b/reddwarfclient/tests/test_limits.py deleted file mode 100644 index fda182c..0000000 --- a/reddwarfclient/tests/test_limits.py +++ /dev/null @@ -1,79 +0,0 @@ -from testtools import TestCase -from mock import Mock -from reddwarfclient import limits - - -class LimitsTest(TestCase): - """ - This class tests the calling code for the Limits API - """ - - def setUp(self): - super(LimitsTest, self).setUp() - self.limits = limits.Limits(Mock()) - self.limits.api.client = Mock() - - def tearDown(self): - super(LimitsTest, self).tearDown() - - def test_list(self): - resp = Mock() - resp.status = 200 - body = {"limits": - [ - {'maxTotalInstances': 55, - 'verb': 'ABSOLUTE', - 'maxTotalVolumes': 100}, - {'regex': '.*', - 'nextAvailable': '2011-07-21T18:17:06Z', - 'uri': '*', - 'value': 10, - 'verb': 'POST', - 'remaining': 2, 'unit': 'MINUTE'}, - {'regex': '.*', - 'nextAvailable': '2011-07-21T18:17:06Z', - 'uri': '*', - 'value': 10, - 'verb': 'PUT', - 'remaining': 2, - 'unit': 'MINUTE'}, - {'regex': '.*', - 'nextAvailable': '2011-07-21T18:17:06Z', - 'uri': '*', - 'value': 10, - 'verb': 'DELETE', - 'remaining': 2, - 'unit': 'MINUTE'}, - {'regex': '.*', - 'nextAvailable': '2011-07-21T18:17:06Z', - 'uri': '*', - 'value': 10, - 'verb': 'GET', - 'remaining': 2, 'unit': 'MINUTE'}]} - response = (resp, body) - - mock_get = Mock(return_value=response) - self.limits.api.client.get = mock_get - self.assertIsNotNone(self.limits.list()) - mock_get.assert_called_once_with("/limits") - - def test_list_errors(self): - status_list = [400, 401, 403, 404, 408, 409, 413, 500, 501] - for status_code in status_list: - self._check_error_response(status_code) - - def _check_error_response(self, status_code): - RESPONSE_KEY = "limits" - - resp = Mock() - resp.status = status_code - body = {RESPONSE_KEY: { - 'absolute': {}, - 'rate': [ - {'limit': [] - }]}} - response = (resp, body) - - mock_get = Mock(return_value=response) - self.limits.api.client.get = mock_get - self.assertRaises(Exception, self.limits.list) diff --git a/reddwarfclient/tests/test_management.py b/reddwarfclient/tests/test_management.py deleted file mode 100644 index 810961a..0000000 --- a/reddwarfclient/tests/test_management.py +++ /dev/null @@ -1,144 +0,0 @@ -from testtools import TestCase -from mock import Mock - -from reddwarfclient import management -from reddwarfclient import base - -""" -Unit tests for management.py -""" - - -class RootHistoryTest(TestCase): - - def setUp(self): - super(RootHistoryTest, self).setUp() - self.orig__init = management.RootHistory.__init__ - management.RootHistory.__init__ = Mock(return_value=None) - - def tearDown(self): - super(RootHistoryTest, self).tearDown() - management.RootHistory.__init__ = self.orig__init - - def test___repr__(self): - root_history = management.RootHistory() - root_history.id = "1" - root_history.created = "ct" - root_history.user = "tu" - self.assertEqual('', - root_history.__repr__()) - - -class ManagementTest(TestCase): - - def setUp(self): - super(ManagementTest, self).setUp() - self.orig__init = management.Management.__init__ - management.Management.__init__ = Mock(return_value=None) - self.management = management.Management() - self.management.api = Mock() - self.management.api.client = Mock() - - self.orig_hist__init = management.RootHistory.__init__ - self.orig_base_getid = base.getid - base.getid = Mock(return_value="instance1") - - def tearDown(self): - super(ManagementTest, self).tearDown() - management.Management.__init__ = self.orig__init - management.RootHistory.__init__ = self.orig_hist__init - base.getid = self.orig_base_getid - - def test__list(self): - self.management.api.client.get = Mock(return_value=('resp', None)) - self.assertRaises(Exception, self.management._list, "url", None) - - body = Mock() - body.get = Mock(return_value=[{'href': 'http://test.net/test_file', - 'rel': 'next'}]) - body.__getitem__ = Mock(return_value='instance1') - self.management.resource_class = Mock(return_value="instance-1") - self.management.api.client.get = Mock(return_value=('resp', body)) - _expected = [{'href': 'http://test.net/test_file', 'rel': 'next'}] - self.assertEqual(_expected, self.management._list("url", None).links) - - def test_show(self): - def side_effect_func(path, instance): - return path, instance - self.management._get = Mock(side_effect=side_effect_func) - p, i = self.management.show(1) - self.assertEqual(('/mgmt/instances/instance1', 'instance'), (p, i)) - - def test_index(self): - def side_effect_func(url, name, limit, marker): - return url - - self.management._list = Mock(side_effect=side_effect_func) - self.assertEqual('/mgmt/instances?deleted=true', - self.management.index(deleted=True)) - self.assertEqual('/mgmt/instances?deleted=false', - self.management.index(deleted=False)) - - def test_root_enabled_history(self): - self.management.api.client.get = Mock(return_value=('resp', None)) - self.assertRaises(Exception, - self.management.root_enabled_history, "instance") - body = {'root_history': 'rh'} - self.management.api.client.get = Mock(return_value=('resp', body)) - management.RootHistory.__init__ = Mock(return_value=None) - rh = self.management.root_enabled_history("instance") - self.assertTrue(isinstance(rh, management.RootHistory)) - - def test__action(self): - resp = Mock() - self.management.api.client.post = Mock(return_value=(resp, 'body')) - resp.status = 200 - self.management._action(1, 'body') - self.assertEqual(1, self.management.api.client.post.call_count) - resp.status = 400 - self.assertRaises(ValueError, self.management._action, 1, 'body') - self.assertEqual(2, self.management.api.client.post.call_count) - - def _mock_action(self): - self.body_ = "" - - def side_effect_func(instance_id, body): - self.body_ = body - self.management._action = Mock(side_effect=side_effect_func) - - def test_stop(self): - self._mock_action() - self.management.stop(1) - self.assertEqual(1, self.management._action.call_count) - self.assertEqual({'stop': {}}, self.body_) - - def test_reboot(self): - self._mock_action() - self.management.reboot(1) - self.assertEqual(1, self.management._action.call_count) - self.assertEqual({'reboot': {}}, self.body_) - - def test_migrate(self): - self._mock_action() - self.management.migrate(1) - self.assertEqual(1, self.management._action.call_count) - self.assertEqual({'migrate': {}}, self.body_) - - def test_migrate_to_host(self): - hostname = 'hostname2' - self._mock_action() - self.management.migrate(1, host=hostname) - self.assertEqual(1, self.management._action.call_count) - self.assertEqual({'migrate': {'host': hostname}}, self.body_) - - def test_update(self): - self._mock_action() - self.management.update(1) - self.assertEqual(1, self.management._action.call_count) - self.assertEqual({'update': {}}, self.body_) - - def test_reset_task_status(self): - self._mock_action() - self.management.reset_task_status(1) - self.assertEqual(1, self.management._action.call_count) - self.assertEqual({'reset-task-status': {}}, self.body_) diff --git a/reddwarfclient/tests/test_secgroups.py b/reddwarfclient/tests/test_secgroups.py deleted file mode 100644 index 25b4de8..0000000 --- a/reddwarfclient/tests/test_secgroups.py +++ /dev/null @@ -1,102 +0,0 @@ -from testtools import TestCase -from mock import Mock - -from reddwarfclient import security_groups -from reddwarfclient import base - -""" -Unit tests for security_groups.py -""" - - -class SecGroupTest(TestCase): - - def setUp(self): - super(SecGroupTest, self).setUp() - self.orig__init = security_groups.SecurityGroup.__init__ - security_groups.SecurityGroup.__init__ = Mock(return_value=None) - self.security_group = security_groups.SecurityGroup() - self.security_groups = security_groups.SecurityGroups(1) - - def tearDown(self): - super(SecGroupTest, self).tearDown() - security_groups.SecurityGroup.__init__ = self.orig__init - - def test___repr__(self): - self.security_group.name = "security_group-1" - self.assertEqual('', - self.security_group.__repr__()) - - def test_list(self): - sec_group_list = ['secgroup1', 'secgroup2'] - self.security_groups.list = Mock(return_value=sec_group_list) - self.assertEqual(sec_group_list, self.security_groups.list()) - - def test_get(self): - def side_effect_func(path, inst): - return path, inst - - self.security_groups._get = Mock(side_effect=side_effect_func) - self.security_group.id = 1 - self.assertEqual(('/security-groups/1', 'security_group'), - self.security_groups.get(self.security_group)) - - -class SecGroupRuleTest(TestCase): - - def setUp(self): - super(SecGroupRuleTest, self).setUp() - self.orig__init = security_groups.SecurityGroupRule.__init__ - security_groups.SecurityGroupRule.__init__ = Mock(return_value=None) - security_groups.SecurityGroupRules.__init__ = Mock(return_value=None) - self.security_group_rule = security_groups.SecurityGroupRule() - self.security_group_rules = security_groups.SecurityGroupRules() - - def tearDown(self): - super(SecGroupRuleTest, self).tearDown() - security_groups.SecurityGroupRule.__init__ = self.orig__init - - def test___repr__(self): - self.security_group_rule.group_id = 1 - self.security_group_rule.protocol = "tcp" - self.security_group_rule.from_port = 80 - self.security_group_rule.to_port = 80 - self.security_group_rule.cidr = "0.0.0.0//0" - representation = \ - "" % (1, "tcp", 80, 80, "0.0.0.0//0") - - self.assertEqual(representation, - self.security_group_rule.__repr__()) - - def test_create(self): - def side_effect_func(path, body, inst): - return path, body, inst - - self.security_group_rules._create = Mock(side_effect=side_effect_func) - p, b, i = self.security_group_rules.create(1, "tcp", - 80, 80, "0.0.0.0//0") - self.assertEqual("/security-group-rules", p) - self.assertEqual("security_group_rule", i) - self.assertEqual(1, b["security_group_rule"]["group_id"]) - self.assertEqual("tcp", b["security_group_rule"]["protocol"]) - self.assertEqual(80, b["security_group_rule"]["from_port"]) - self.assertEqual(80, b["security_group_rule"]["to_port"]) - self.assertEqual("0.0.0.0//0", b["security_group_rule"]["cidr"]) - - def test_delete(self): - resp = Mock() - resp.status = 200 - body = None - self.security_group_rules.api = Mock() - self.security_group_rules.api.client = Mock() - self.security_group_rules.api.client.delete = \ - Mock(return_value=(resp, body)) - self.security_group_rules.delete(self.id) - resp.status = 500 - self.assertRaises(ValueError, self.security_group_rules.delete, - self.id) diff --git a/reddwarfclient/tests/test_users.py b/reddwarfclient/tests/test_users.py deleted file mode 100644 index 80be05f..0000000 --- a/reddwarfclient/tests/test_users.py +++ /dev/null @@ -1,126 +0,0 @@ -from testtools import TestCase -from mock import Mock - -from reddwarfclient import users -from reddwarfclient import base - -""" -Unit tests for users.py -""" - - -class UserTest(TestCase): - - def setUp(self): - super(UserTest, self).setUp() - self.orig__init = users.User.__init__ - users.User.__init__ = Mock(return_value=None) - self.user = users.User() - - def tearDown(self): - super(UserTest, self).tearDown() - users.User.__init__ = self.orig__init - - def test___repr__(self): - self.user.name = "user-1" - self.assertEqual('', self.user.__repr__()) - - -class UsersTest(TestCase): - - def setUp(self): - super(UsersTest, self).setUp() - self.orig__init = users.Users.__init__ - users.Users.__init__ = Mock(return_value=None) - self.users = users.Users() - self.users.api = Mock() - self.users.api.client = Mock() - - self.orig_base_getid = base.getid - base.getid = Mock(return_value="instance1") - - def tearDown(self): - super(UsersTest, self).tearDown() - users.Users.__init__ = self.orig__init - base.getid = self.orig_base_getid - - def _get_mock_method(self): - self._resp = Mock() - self._body = None - self._url = None - - def side_effect_func(url, body=None): - self._body = body - self._url = url - return (self._resp, body) - - return Mock(side_effect=side_effect_func) - - def _build_fake_user(self, name, hostname=None, password=None, - databases=None): - return {'name': name, - 'password': password if password else 'password', - 'host': hostname, - 'databases': databases if databases else [], - } - - def test_create(self): - self.users.api.client.post = self._get_mock_method() - self._resp.status = 200 - user = self._build_fake_user('user1') - - self.users.create(23, [user]) - self.assertEqual('/instances/23/users', self._url) - self.assertEqual({"users": [user]}, self._body) - - # Even if host isn't supplied originally, - # the default is supplied. - del user['host'] - self.users.create(23, [user]) - self.assertEqual('/instances/23/users', self._url) - user['host'] = '%' - self.assertEqual({"users": [user]}, self._body) - - # If host is supplied, of course it's put into the body. - user['host'] = '127.0.0.1' - self.users.create(23, [user]) - self.assertEqual({"users": [user]}, self._body) - - # Make sure that response of 400 is recognized as an error. - user['host'] = '%' - self._resp.status = 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.users.delete(27, 'user1') - self.assertEqual('/instances/27/users/user1', self._url) - self._resp.status = 400 - self.assertRaises(Exception, self.users.delete, 34, 'user1') - - def test__list(self): - def side_effect_func(self, val): - return val - - key = 'key' - body = Mock() - body.get = Mock(return_value=[{'href': 'http://test.net/test_file', - 'rel': 'next'}]) - body.__getitem__ = Mock(return_value=["test-value"]) - - resp = Mock() - resp.status = 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) - - self.users.api.client.get = Mock(return_value=(resp, None)) - self.assertRaises(Exception, self.users._list, 'url', None) - - def test_list(self): - def side_effect_func(path, user, limit, marker): - return path - - self.users._list = Mock(side_effect=side_effect_func) - self.assertEqual('/instances/instance1/users', self.users.list(1)) diff --git a/reddwarfclient/tests/test_utils.py b/reddwarfclient/tests/test_utils.py deleted file mode 100644 index c3a000d..0000000 --- a/reddwarfclient/tests/test_utils.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -from testtools import TestCase -from reddwarfclient import utils -from reddwarfclient import versions - - -class UtilsTest(TestCase): - - def test_add_hookable_mixin(self): - def func(): - pass - - hook_type = "hook_type" - mixin = utils.HookableMixin() - mixin.add_hook(hook_type, func) - self.assertTrue(hook_type in mixin._hooks_map) - self.assertTrue(func in mixin._hooks_map[hook_type]) - - def test_run_hookable_mixin(self): - def func(): - pass - - hook_type = "hook_type" - mixin = utils.HookableMixin() - mixin.add_hook(hook_type, func) - mixin.run_hooks(hook_type) - - def test_environment(self): - self.assertEqual('', utils.env()) - self.assertEqual('passing', utils.env(default='passing')) - - os.environ['test_abc'] = 'passing' - self.assertEqual('passing', utils.env('test_abc')) - self.assertEqual('', utils.env('test_abcd')) - - def test_slugify(self): - import unicodedata - - self.assertEqual('not_unicode', utils.slugify('not_unicode')) - self.assertEqual('unicode', utils.slugify(unicode('unicode'))) - self.assertEqual('slugify-test', utils.slugify('SLUGIFY% test!')) diff --git a/reddwarfclient/tests/test_xml.py b/reddwarfclient/tests/test_xml.py deleted file mode 100644 index cda382b..0000000 --- a/reddwarfclient/tests/test_xml.py +++ /dev/null @@ -1,241 +0,0 @@ -from testtools import TestCase -from lxml import etree -from reddwarfclient import xml - - -class XmlTest(TestCase): - - ELEMENT = ''' - - - - - - - - - - ''' - ROOT = etree.fromstring(ELEMENT) - - JSON = {'instances': - {'instances': ['1', '2', '3']}, 'dummy': {'dict': True}} - - def test_element_ancestors_match_list(self): - # Test normal operation: - self.assertTrue(xml.element_ancestors_match_list(self.ROOT[0][0], - ['instance', - 'instances'])) - - # Test itr_elem is None: - self.assertTrue(xml.element_ancestors_match_list(self.ROOT, - ['instances'])) - - # Test that the first parent element does not match the first list - # element: - self.assertFalse(xml.element_ancestors_match_list(self.ROOT[0][0], - ['instances', - 'instance'])) - - def test_populate_element_from_dict(self): - # Test populate_element_from_dict with a None in the data - ele = ''' - - - - - - ''' - rt = etree.fromstring(ele) - - self.assertEqual(None, xml.populate_element_from_dict(rt, - {'size': None})) - - def test_element_must_be_list(self): - # Test for when name isn't in the dictionary - self.assertFalse(xml.element_must_be_list(self.ROOT, "not_in_list")) - - # Test when name is in the dictionary but list is empty - self.assertTrue(xml.element_must_be_list(self.ROOT, "accounts")) - - # Test when name is in the dictionary but list is not empty - self.assertTrue(xml.element_must_be_list(self.ROOT[0][0][0], "links")) - - def test_element_to_json(self): - # Test when element must be list: - self.assertEqual([{'flavor': {'links': [], 'value': {'value': '5'}}}], - xml.element_to_json("accounts", self.ROOT)) - - # Test when element must not be list: - exp = {'instance': {'flavor': {'links': [], 'value': {'value': '5'}}}} - self.assertEqual(exp, xml.element_to_json("not_in_list", self.ROOT)) - - def test_root_element_to_json(self): - # Test when element must be list: - exp = ([{'flavor': {'links': [], 'value': {'value': '5'}}}], None) - self.assertEqual(exp, xml.root_element_to_json("accounts", self.ROOT)) - - # Test when element must not be list: - exp = {'instance': {'flavor': {'links': [], 'value': {'value': '5'}}}} - self.assertEqual((exp, None), - xml.root_element_to_json("not_in_list", self.ROOT)) - - # Test rootEnabled True: - t_element = etree.fromstring(''' True ''') - self.assertEqual((True, None), - xml.root_element_to_json("rootEnabled", t_element)) - - # Test rootEnabled False: - f_element = etree.fromstring(''' False ''') - self.assertEqual((False, None), - xml.root_element_to_json("rootEnabled", f_element)) - - def test_element_to_list(self): - # Test w/ no child elements - self.assertEqual([], xml.element_to_list(self.ROOT[0][0][0])) - - # Test w/ no child elements and check_for_links = True - self.assertEqual(([], None), - xml.element_to_list(self.ROOT[0][0][0], - check_for_links=True)) - - # Test w/ child elements - self.assertEqual([{}, {'value': '5'}], - xml.element_to_list(self.ROOT[0][0])) - - # Test w/ child elements and check_for_links = True - self.assertEqual(([{'value': '5'}], []), - xml.element_to_list(self.ROOT[0][0], - check_for_links=True)) - - def test_element_to_dict(self): - # Test when there is not a None - exp = {'instance': {'flavor': {'links': [], 'value': {'value': '5'}}}} - self.assertEqual(exp, xml.element_to_dict(self.ROOT)) - - # Test when there is a None - element = ''' - - None - - ''' - rt = etree.fromstring(element) - self.assertEqual(None, xml.element_to_dict(rt)) - - def test_standarize_json(self): - xml.standardize_json_lists(self.JSON) - self.assertEqual({'instances': ['1', '2', '3'], - 'dummy': {'dict': True}}, self.JSON) - - def test_normalize_tag(self): - ELEMENT_NS = ''' - - - - - - - - - - ''' - ROOT_NS = etree.fromstring(ELEMENT_NS) - - # Test normalizing without namespace info - self.assertEqual('instances', xml.normalize_tag(self.ROOT)) - - # Test normalizing with namespace info - self.assertEqual('instances', xml.normalize_tag(ROOT_NS)) - - def test_create_root_xml_element(self): - # Test creating when name is not in REQUEST_AS_LIST - element = xml.create_root_xml_element("root", {"root": "value"}) - exp = '' - self.assertEqual(exp, etree.tostring(element)) - - # Test creating when name is in REQUEST_AS_LIST - element = xml.create_root_xml_element("users", []) - exp = '' - self.assertEqual(exp, etree.tostring(element)) - - def test_creating_subelements(self): - # Test creating a subelement as a dictionary - element = xml.create_root_xml_element("root", {"root": 5}) - xml.create_subelement(element, "subelement", {"subelement": "value"}) - exp = '' - self.assertEqual(exp, etree.tostring(element)) - - # Test creating a subelement as a list - element = xml.create_root_xml_element("root", - {"root": {"value": "nested"}}) - xml.create_subelement(element, "subelement", [{"subelement": "value"}]) - exp = '' \ - '' - self.assertEqual(exp, etree.tostring(element)) - - # Test creating a subelement as a string (should raise TypeError) - element = xml.create_root_xml_element("root", {"root": "value"}) - try: - xml.create_subelement(element, "subelement", ["value"]) - self.fail("TypeError exception expected") - except TypeError: - pass - - def test_modify_response_types(self): - TYPE_MAP = { - "Int": int, - "Bool": bool - } - #Is a string True - self.assertEqual(True, xml.modify_response_types('True', TYPE_MAP)) - - #Is a string False - self.assertEqual(False, xml.modify_response_types('False', TYPE_MAP)) - - #Is a dict - test_dict = {"Int": "5"} - test_dict = xml.modify_response_types(test_dict, TYPE_MAP) - self.assertEqual(int, test_dict["Int"].__class__) - - #Is a list - test_list = {"a_list": [{"Int": "5"}, {"Str": "A"}]} - test_list = xml.modify_response_types(test_list["a_list"], TYPE_MAP) - self.assertEqual([{'Int': 5}, {'Str': 'A'}], test_list) - - def test_reddwarfxmlclient(self): - from reddwarfclient import exceptions - - client = xml.ReddwarfXmlClient("user", "password", "tenant", - "auth_url", "service_name", - auth_strategy="fake") - request = {'headers': {}} - - # Test morph_request, no body - client.morph_request(request) - self.assertEqual('application/xml', request['headers']['Accept']) - self.assertEqual('application/xml', request['headers']['Content-Type']) - - # Test morph_request, with body - request['body'] = {'root': {'test': 'test'}} - client.morph_request(request) - body = '\n' - exp = {'body': body, - 'headers': {'Content-Type': 'application/xml', - 'Accept': 'application/xml'}} - self.assertEqual(exp, request) - - # Test morph_response_body - request = "" - result = client.morph_response_body(request) - self.assertEqual({'users': [], 'links': [{'href': 'value'}]}, result) - - # Test morph_response_body with improper input - try: - client.morph_response_body("value") - self.fail("ResponseFormatError exception expected") - except exceptions.ResponseFormatError: - pass diff --git a/reddwarfclient/users.py b/reddwarfclient/users.py deleted file mode 100644 index 344252c..0000000 --- a/reddwarfclient/users.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base -from reddwarfclient import databases -from reddwarfclient.common import check_for_exceptions -from reddwarfclient.common import limit_url -from reddwarfclient.common import Paginated -from reddwarfclient.common import quote_user_host -import exceptions -import urlparse - - -class User(base.Resource): - """ - A database user - """ - def __repr__(self): - return "" % self.name - - -class Users(base.ManagerWithFind): - """ - Manage :class:`Users` resources. - """ - resource_class = User - - def create(self, instance_id, users): - """ - Create users with permissions to the specified databases - """ - body = {"users": users} - url = "/instances/%s/users" % instance_id - resp, body = self.api.client.post(url, body=body) - check_for_exceptions(resp, body) - - def delete(self, instance_id, username, hostname=None): - """Delete an existing user in the specified instance""" - user = quote_user_host(username, hostname) - url = "/instances/%s/users/%s" % (instance_id, user) - resp, body = self.api.client.delete(url) - check_for_exceptions(resp, body) - - def _list(self, url, response_key, limit=None, marker=None): - resp, body = self.api.client.get(limit_url(url, limit, marker)) - check_for_exceptions(resp, body) - if not body: - raise Exception("Call to " + url + - " did not return a body.") - links = body.get('links', []) - next_links = [link['href'] for link in links if link['rel'] == 'next'] - next_marker = None - for link in next_links: - # Extract the marker from the url. - parsed_url = urlparse.urlparse(link) - query_dict = dict(urlparse.parse_qsl(parsed_url.query)) - next_marker = query_dict.get('marker', None) - users = [self.resource_class(self, res) for res in body[response_key]] - return Paginated(users, next_marker=next_marker, links=links) - - def list(self, instance, limit=None, marker=None): - """ - Get a list of all Users from the instance's Database. - - :rtype: list of :class:`User`. - """ - return self._list("/instances/%s/users" % base.getid(instance), - "users", limit, marker) - - def get(self, instance_id, username, hostname=None): - """ - Get a single User from the instance's Database. - - :rtype: :class:`User`. - """ - user = quote_user_host(username, hostname) - url = "/instances/%s/users/%s" % (instance_id, user) - return self._get(url, "user") - - def list_access(self, instance, username, hostname=None): - """Show all databases the given user has access to. """ - instance_id = base.getid(instance) - user = quote_user_host(username, hostname) - url = "/instances/%(instance_id)s/users/%(user)s/databases" - resp, body = self.api.client.get(url % locals()) - check_for_exceptions(resp, body) - if not body: - raise Exception("Call to %s did not return to a body" % url) - return [databases.Database(self, db) for db in body['databases']] - - def grant(self, instance, username, databases, hostname=None): - """Allow an existing user permissions to access a database.""" - instance_id = base.getid(instance) - user = quote_user_host(username, hostname) - url = "/instances/%(instance_id)s/users/%(user)s/databases" - dbs = {'databases': [{'name': db} for db in databases]} - resp, body = self.api.client.put(url % locals(), body=dbs) - check_for_exceptions(resp, body) - - def revoke(self, instance, username, database, hostname=None): - """Revoke from an existing user access permissions to a database.""" - instance_id = base.getid(instance) - user = quote_user_host(username, hostname) - url = ("/instances/%(instance_id)s/users/%(user)s/" - "databases/%(database)s") - resp, body = self.api.client.delete(url % locals()) - check_for_exceptions(resp, body) - - def change_passwords(self, instance, users): - """Change the password for one or more users.""" - instance_id = base.getid(instance) - user_dict = {"users": users} - url = "/instances/%s/users" % instance_id - resp, body = self.api.client.put(url, body=user_dict) - check_for_exceptions(resp, body) diff --git a/reddwarfclient/utils.py b/reddwarfclient/utils.py deleted file mode 100644 index 3deb806..0000000 --- a/reddwarfclient/utils.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2012 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import re -import sys - - -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} - - @classmethod - def add_hook(cls, hook_type, hook_func): - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] - - cls._hooks_map[hook_type].append(hook_func) - - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) - - -def env(*vars, **kwargs): - """ - returns the first environment variable set - if none are non-empty, defaults to '' or keyword arg default - """ - for v in vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - -_slugify_strip_re = re.compile(r'[^\w\s-]') -_slugify_hyphenate_re = re.compile(r'[-\s]+') - - -# http://code.activestate.com/recipes/ -# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ -def slugify(value): - """ - Normalizes string, converts to lowercase, removes non-alpha characters, - and converts spaces to hyphens. - - From Django's "django/template/defaultfilters.py". - """ - import unicodedata - if not isinstance(value, unicode): - value = unicode(value) - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = unicode(_slugify_strip_re.sub('', value).strip().lower()) - return _slugify_hyphenate_re.sub('-', value) diff --git a/reddwarfclient/versions.py b/reddwarfclient/versions.py deleted file mode 100644 index f7b52c4..0000000 --- a/reddwarfclient/versions.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from reddwarfclient import base - - -class Version(base.Resource): - """ - Version is an opaque instance used to hold version information. - """ - def __repr__(self): - return "" % self.id - - -class Versions(base.ManagerWithFind): - """ - Manage :class:`Versions` information. - """ - - resource_class = Version - - def index(self, url): - """ - Get a list of all versions. - - :rtype: list of :class:`Versions`. - """ - resp, body = self.api.client.request(url, "GET") - return [self.resource_class(self, res) for res in body['versions']] diff --git a/reddwarfclient/xml.py b/reddwarfclient/xml.py deleted file mode 100644 index fe94f19..0000000 --- a/reddwarfclient/xml.py +++ /dev/null @@ -1,293 +0,0 @@ -from lxml import etree -import json -from numbers import Number - -from reddwarfclient import exceptions -from reddwarfclient.client import ReddwarfHTTPClient - -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 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 - 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 ReddwarfXmlClient(ReddwarfHTTPClient): - - @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 diff --git a/run_local.sh b/run_local.sh index f4497bc..d496a68 100755 --- a/run_local.sh +++ b/run_local.sh @@ -9,9 +9,9 @@ me=${0##*/} function print_usage() { cat >&2 <=1.1.2 testrepository>=0.0.13 testtools>=0.9.29 -# This used to be but openstack/requirements is set to [below]. -# If this breaks we 1) fix tests to use [below] or -# 2) petition to use mock>=1.0.1 mock>=0.8.0 + diff --git a/troveclient/__init__.py b/troveclient/__init__.py new file mode 100644 index 0000000..e3506af --- /dev/null +++ b/troveclient/__init__.py @@ -0,0 +1,31 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from troveclient.accounts import Accounts +from troveclient.databases import Databases +from troveclient.flavors import Flavors +from troveclient.instances import Instances +from troveclient.hosts import Hosts +from troveclient.management import Management +from troveclient.management import RootHistory +from troveclient.root import Root +from troveclient.storage import StorageInfo +from troveclient.users import Users +from troveclient.versions import Versions +from troveclient.diagnostics import DiagnosticsInterrogator +from troveclient.diagnostics import HwInfoInterrogator +from troveclient.client import Dbaas +from troveclient.client import TroveHTTPClient diff --git a/troveclient/accounts.py b/troveclient/accounts.py new file mode 100644 index 0000000..5f60fa7 --- /dev/null +++ b/troveclient/accounts.py @@ -0,0 +1,67 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +from troveclient.common import check_for_exceptions + + +class Account(base.Resource): + """ + Account is an opaque instance used to hold account information. + """ + def __repr__(self): + return "" % self.name + + +class Accounts(base.ManagerWithFind): + """ + Manage :class:`Account` information. + """ + + resource_class = Account + + 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, body[response_key]) + + def index(self): + """Get a list of all accounts with non-deleted instances""" + + url = "/mgmt/accounts" + resp, body = self.api.client.get(url) + check_for_exceptions(resp, body) + if not body: + raise Exception("Call to " + url + " did not return a body.") + return base.Resource(self, body) + + def show(self, account): + """ + Get details of one account. + + :rtype: :class:`Account`. + """ + + acct_name = self._get_account_name(account) + return self._list("/mgmt/accounts/%s" % acct_name, 'account') + + @staticmethod + def _get_account_name(account): + try: + if account.name: + return account.name + except AttributeError: + return account diff --git a/troveclient/auth.py b/troveclient/auth.py new file mode 100644 index 0000000..909d45b --- /dev/null +++ b/troveclient/auth.py @@ -0,0 +1,269 @@ +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient 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') + + try: + print(resp_body) + self.auth_token = resp_body['auth']['token']['id'] + except KeyError: + raise nova_exceptions.AuthorizationFailure() + + catalog = resp_body['auth']['serviceCatalog'] + if 'cloudDatabases' not in catalog: + raise nova_exceptions.EndpointNotFound() + endpoints = catalog['cloudDatabases'] + for endpoint in endpoints: + if self.region_name is None or \ + endpoint['region'] == self.region_name: + self.management_url = endpoint['publicURL'] + return + raise nova_exceptions.EndpointNotFound() + + +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 not 'serviceCatalog' 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) diff --git a/troveclient/backups.py b/troveclient/backups.py new file mode 100644 index 0000000..036f027 --- /dev/null +++ b/troveclient/backups.py @@ -0,0 +1,71 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +import exceptions + + +class Backup(base.Resource): + """ + Backup is a resource used to hold backup information. + """ + def __repr__(self): + return "" % self.name + + +class Backups(base.ManagerWithFind): + """ + Manage :class:`Backups` information. + """ + + resource_class = Backup + + def get(self, backup): + """ + Get a specific backup. + + :rtype: :class:`Backups` + """ + return self._get("/backups/%s" % base.getid(backup), + "backup") + + def list(self, limit=None, marker=None): + """ + Get a list of all backups. + + :rtype: list of :class:`Backups`. + """ + return self._list("/backups", "backups", limit, marker) + + def create(self, name, instance, description=None): + """ + Create a new backup from the given instance. + """ + body = {"backup": { + "name": name, + "instance": instance, + "description": description, + }} + return self._create("/backups", body, "backup") + + def delete(self, backup_id): + """ + Delete the specified backup. + + :param backup_id: The backup id to delete + """ + resp, body = self.api.client.delete("/backups/%s" % backup_id) + if resp.status in (422, 500): + raise exceptions.from_response(resp, body) diff --git a/troveclient/base.py b/troveclient/base.py new file mode 100644 index 0000000..01cd19d --- /dev/null +++ b/troveclient/base.py @@ -0,0 +1,293 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Base utilities to build API operation managers and objects on top of. +""" + +import contextlib +import hashlib +import os +from troveclient import exceptions +from troveclient 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 diff --git a/troveclient/cli.py b/troveclient/cli.py new file mode 100644 index 0000000..487a45c --- /dev/null +++ b/troveclient/cli.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python + +# Copyright 2011 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Trove Command line tool +""" + +#TODO(tim.simpson): optparse is deprecated. Replace with argparse. +import optparse +import os +import sys + + +# If ../trove/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +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', + '__init__.py')): + sys.path.insert(0, possible_topdir) + + +from troveclient import common + + +class InstanceCommands(common.AuthedCommandsBase): + """Commands to perform various instances operations and actions""" + + params = [ + 'flavor', + 'id', + 'limit', + 'marker', + 'name', + 'size', + 'backup' + ] + + def create(self): + """Create a new instance""" + self._require('name', 'flavor') + volume = None + if self.size is not None: + volume = {"size": self.size} + restorePoint = None + if self.backup is not None: + restorePoint = {"backupRef": self.backup} + self._pretty_print(self.dbaas.instances.create, self.name, + self.flavor, volume, restorePoint=restorePoint) + + def delete(self): + """Delete the specified instance""" + self._require('id') + print self.dbaas.instances.delete(self.id) + + def get(self): + """Get details for the specified instance""" + self._require('id') + self._pretty_print(self.dbaas.instances.get, self.id) + + def backups(self): + """Get a list of backups for the specified instance""" + self._require('id') + self._pretty_list(self.dbaas.instances.backups, self.id) + + def list(self): + """List all instances for account""" + # limit and marker are not required. + limit = self.limit or None + if limit: + limit = int(limit, 10) + self._pretty_paged(self.dbaas.instances.list) + + def resize_volume(self): + """Resize an instance volume""" + self._require('id', 'size') + self._pretty_print(self.dbaas.instances.resize_volume, self.id, + self.size) + + def resize_instance(self): + """Resize an instance flavor""" + self._require('id', 'flavor') + self._pretty_print(self.dbaas.instances.resize_instance, self.id, + self.flavor) + + def restart(self): + """Restart the database""" + self._require('id') + self._pretty_print(self.dbaas.instances.restart, self.id) + + def reset_password(self): + """Reset the root user Password""" + self._require('id') + self._pretty_print(self.dbaas.instances.reset_password, self.id) + + +class FlavorsCommands(common.AuthedCommandsBase): + """Commands for listing Flavors""" + + params = [] + + def list(self): + """List the available flavors""" + self._pretty_list(self.dbaas.flavors.list) + + +class DatabaseCommands(common.AuthedCommandsBase): + """Database CRUD operations on an instance""" + + params = [ + 'name', + 'id', + 'limit', + 'marker', + ] + + def create(self): + """Create a database""" + self._require('id', 'name') + databases = [{'name': self.name}] + print self.dbaas.databases.create(self.id, databases) + + def delete(self): + """Delete a database""" + self._require('id', 'name') + print self.dbaas.databases.delete(self.id, self.name) + + def list(self): + """List the databases""" + self._require('id') + self._pretty_paged(self.dbaas.databases.list, self.id) + + +class UserCommands(common.AuthedCommandsBase): + """User CRUD operations on an instance""" + params = [ + 'id', + 'database', + 'databases', + 'hostname', + 'name', + 'password', + ] + + def create(self): + """Create a user in instance, with access to one or more databases""" + self._require('id', 'name', 'password', 'databases') + self._make_list('databases') + databases = [{'name': dbname} for dbname in self.databases] + users = [{'name': self.name, 'host': self.hostname, + 'password': self.password, 'databases': databases}] + self.dbaas.users.create(self.id, users) + + def delete(self): + """Delete the specified user""" + self._require('id', 'name') + self.dbaas.users.delete(self.id, self.name, self.hostname) + + def get(self): + """Get a single user.""" + self._require('id', 'name') + self._pretty_print(self.dbaas.users.get, self.id, + self.name, self.hostname) + + def list(self): + """List all the users for an instance""" + self._require('id') + self._pretty_paged(self.dbaas.users.list, self.id) + + def access(self): + """Show all databases the user has access to.""" + self._require('id', 'name') + self._pretty_list(self.dbaas.users.list_access, self.id, + self.name, self.hostname) + + def grant(self): + """Allow an existing user permissions to access one or more + databases.""" + self._require('id', 'name', 'databases') + self._make_list('databases') + self.dbaas.users.grant(self.id, self.name, self.databases, + self.hostname) + + def revoke(self): + """Revoke from an existing user access permissions to a database.""" + self._require('id', 'name', 'database') + self.dbaas.users.revoke(self.id, self.name, self.database, + self.hostname) + + def change_password(self): + """Change the password of a single user.""" + self._require('id', 'name', 'password') + users = [{'name': self.name, + 'host': self.hostname, + 'password': self.password}] + self.dbaas.users.change_passwords(self.id, users) + + +class RootCommands(common.AuthedCommandsBase): + """Root user related operations on an instance""" + + params = [ + 'id', + ] + + def create(self): + """Enable the instance's root user.""" + self._require('id') + try: + user, password = self.dbaas.root.create(self.id) + print "User:\t\t%s\nPassword:\t%s" % (user, password) + except: + print sys.exc_info()[1] + + def enabled(self): + """Check the instance for root access""" + self._require('id') + self._pretty_print(self.dbaas.root.is_root_enabled, self.id) + + +class VersionCommands(common.AuthedCommandsBase): + """List available versions""" + + params = [ + 'url', + ] + + def list(self): + """List all the supported versions""" + self._require('url') + self._pretty_list(self.dbaas.versions.index, self.url) + + +class LimitsCommands(common.AuthedCommandsBase): + """Show the rate limits and absolute limits""" + + def list(self): + """List the rate limits and absolute limits""" + self._pretty_list(self.dbaas.limits.list) + + +class BackupsCommands(common.AuthedCommandsBase): + """Command to manage and show backups""" + params = ['name', 'instance', 'description'] + + def get(self): + """Get details for the specified backup""" + self._require('id') + self._pretty_print(self.dbaas.backups.get, self.id) + + def list(self): + """List backups""" + self._pretty_list(self.dbaas.backups.list) + + def create(self): + """Create a new backup""" + self._require('name', 'instance') + self._pretty_print(self.dbaas.backups.create, self.name, + self.instance, self.description) + + def delete(self): + """Delete a backup""" + self._require('id') + self._pretty_print(self.dbaas.backups.delete, self.id) + + +class SecurityGroupCommands(common.AuthedCommandsBase): + """Commands to list and show Security Groups For an Instance and """ + """create and delete security group rules for them. """ + params = ['id', + 'secgroup_id', + 'protocol', + 'from_port', + 'to_port', + 'cidr' + ] + + def get(self): + """Get a security group associated with an instance.""" + self._require('id') + self._pretty_print(self.dbaas.security_groups.get, self.id) + + def list(self): + """List all the Security Groups and the rules""" + self._pretty_paged(self.dbaas.security_groups.list) + + def add_rule(self): + """Add a security group rule""" + self._require('secgroup_id', 'protocol', + 'from_port', 'to_port', 'cidr') + self.dbaas.security_group_rules.create(self.secgroup_id, self.protocol, + self.from_port, self.to_port, + self.cidr) + + def delete_rule(self): + """Delete a security group rule""" + self._require('id') + self.dbaas.security_group_rules.delete(self.id) + + +COMMANDS = {'auth': common.Auth, + 'instance': InstanceCommands, + 'flavor': FlavorsCommands, + 'database': DatabaseCommands, + 'limit': LimitsCommands, + 'backup': BackupsCommands, + 'user': UserCommands, + 'root': RootCommands, + 'version': VersionCommands, + 'secgroup': SecurityGroupCommands, + } + + +def main(): + # Parse arguments + load_file = True + for index, arg in enumerate(sys.argv): + if (arg == "auth" and len(sys.argv) > (index + 1) + and sys.argv[index + 1] == "login"): + load_file = False + + oparser = common.CliOptions.create_optparser(load_file) + for k, v in COMMANDS.items(): + v._prepare_parser(oparser) + (options, args) = oparser.parse_args() + + if not args: + common.print_commands(COMMANDS) + + if options.verbose: + os.environ['RDC_PP'] = "True" + os.environ['REDDWARFCLIENT_DEBUG'] = "True" + + # Pop the command and check if it's in the known commands + cmd = args.pop(0) + if cmd in COMMANDS: + fn = COMMANDS.get(cmd) + command_object = None + try: + command_object = fn(oparser) + except Exception as ex: + if options.debug: + raise + print(ex) + + # Get a list of supported actions for the command + actions = common.methods_of(command_object) + + if len(args) < 1: + common.print_actions(cmd, actions) + + # Check for a valid action and perform that action + action = args.pop(0) + if action in actions: + if not options.debug: + try: + getattr(command_object, action)() + except Exception as ex: + if options.debug: + raise + print ex + else: + getattr(command_object, action)() + else: + common.print_actions(cmd, actions) + else: + common.print_commands(COMMANDS) + + +if __name__ == '__main__': + main() diff --git a/troveclient/client.py b/troveclient/client.py new file mode 100644 index 0000000..1409448 --- /dev/null +++ b/troveclient/client.py @@ -0,0 +1,370 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +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 import auth +from troveclient 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): + from troveclient import common + 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) + + # 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='trove', + 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.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.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() diff --git a/troveclient/common.py b/troveclient/common.py new file mode 100644 index 0000000..d10fb22 --- /dev/null +++ b/troveclient/common.py @@ -0,0 +1,406 @@ +# Copyright 2011 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import json +import optparse +import os +import pickle +import sys + +from troveclient import client +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): + 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': 'trove', + '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] ", + 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 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 _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 diff --git a/troveclient/databases.py b/troveclient/databases.py new file mode 100644 index 0000000..09157d3 --- /dev/null +++ b/troveclient/databases.py @@ -0,0 +1,79 @@ +from troveclient import base +from troveclient.common import check_for_exceptions +from troveclient.common import limit_url +from troveclient.common import Paginated +import exceptions +import urlparse + + +class Database(base.Resource): + """ + According to Wikipedia, "A database is a system intended to organize, + store, and retrieve + large amounts of data easily." + """ + def __repr__(self): + return "" % self.name + + +class Databases(base.ManagerWithFind): + """ + Manage :class:`Databases` resources. + """ + resource_class = Database + + def create(self, instance_id, databases): + """ + Create new databases within the specified instance + """ + body = {"databases": databases} + url = "/instances/%s/databases" % instance_id + resp, body = self.api.client.post(url, body=body) + check_for_exceptions(resp, body) + + def delete(self, instance_id, dbname): + """Delete an existing database in the specified instance""" + url = "/instances/%s/databases/%s" % (instance_id, dbname) + resp, body = self.api.client.delete(url) + check_for_exceptions(resp, body) + + def _list(self, url, response_key, limit=None, marker=None): + resp, body = self.api.client.get(limit_url(url, limit, marker)) + check_for_exceptions(resp, body) + if not body: + raise Exception("Call to " + url + + " did not return a body.") + links = body.get('links', []) + next_links = [link['href'] for link in links if link['rel'] == 'next'] + next_marker = None + for link in next_links: + # Extract the marker from the url. + parsed_url = urlparse.urlparse(link) + query_dict = dict(urlparse.parse_qsl(parsed_url.query)) + next_marker = query_dict.get('marker', None) + databases = body[response_key] + databases = [self.resource_class(self, res) for res in databases] + return Paginated(databases, next_marker=next_marker, links=links) + + def list(self, instance, limit=None, marker=None): + """ + Get a list of all Databases from the instance. + + :rtype: list of :class:`Database`. + """ + return self._list("/instances/%s/databases" % base.getid(instance), + "databases", limit, marker) + +# def get(self, instance, database): +# """ +# Get a specific instances. +# +# :param flavor: The ID of the :class:`Database` to get. +# :rtype: :class:`Database` +# """ +# assert isinstance(instance, Instance) +# assert isinstance(database, (Database, int)) +# instance_id = base.getid(instance) +# db_id = base.getid(database) +# url = "/instances/%s/databases/%s" % (instance_id, db_id) +# return self._get(url, "database") diff --git a/troveclient/diagnostics.py b/troveclient/diagnostics.py new file mode 100644 index 0000000..311a7c9 --- /dev/null +++ b/troveclient/diagnostics.py @@ -0,0 +1,58 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +import exceptions + + +class Diagnostics(base.Resource): + """ + Account is an opaque instance used to hold account information. + """ + def __repr__(self): + return "" % self.version + + +class DiagnosticsInterrogator(base.ManagerWithFind): + """ + Manager class for Interrogator resource + """ + resource_class = Diagnostics + + def get(self, instance): + """ + Get the diagnostics of the guest on the instance. + """ + return self._get("/mgmt/instances/%s/diagnostics" % + base.getid(instance), "diagnostics") + + +class HwInfo(base.Resource): + + def __repr__(self): + return "" % self.version + + +class HwInfoInterrogator(base.ManagerWithFind): + """ + Manager class for HwInfo + """ + resource_class = HwInfo + + def get(self, instance): + """ + Get the hardware information of the instance. + """ + return self._get("/mgmt/instances/%s/hwinfo" % base.getid(instance)) diff --git a/troveclient/exceptions.py b/troveclient/exceptions.py new file mode 100644 index 0000000..88c3f7e --- /dev/null +++ b/troveclient/exceptions.py @@ -0,0 +1,179 @@ +# Copyright 2011 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +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) diff --git a/troveclient/flavors.py b/troveclient/flavors.py new file mode 100644 index 0000000..02fef33 --- /dev/null +++ b/troveclient/flavors.py @@ -0,0 +1,62 @@ +# Copyright (c) 2012 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from troveclient import base + +import exceptions + +from troveclient.common import check_for_exceptions + + +class Flavor(base.Resource): + """ + A Flavor is an Instance type, specifying among other things, RAM size. + """ + def __repr__(self): + return "" % self.name + + +class Flavors(base.ManagerWithFind): + """ + Manage :class:`Flavor` resources. + """ + resource_class = Flavor + + def __repr__(self): + return "" % 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. + + :rtype: list of :class:`Flavor`. + """ + return self._list("/flavors", "flavors") + + def get(self, flavor): + """ + Get a specific flavor. + + :rtype: :class:`Flavor` + """ + return self._get("/flavors/%s" % base.getid(flavor), + "flavor") diff --git a/troveclient/hosts.py b/troveclient/hosts.py new file mode 100644 index 0000000..3e7ecc2 --- /dev/null +++ b/troveclient/hosts.py @@ -0,0 +1,78 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base + +from troveclient.common import check_for_exceptions + + +class Host(base.Resource): + """ + A Hosts is an opaque instance used to store Host instances. + """ + def __repr__(self): + return "" % self.name + + +class Hosts(base.ManagerWithFind): + """ + Manage :class:`Host` resources. + """ + resource_class = Host + + 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 _action(self, host_id, body): + """ + Perform a host "action" -- update + """ + url = "/mgmt/hosts/%s/instances/action" % host_id + resp, body = self.api.client.post(url, body=body) + check_for_exceptions(resp, body) + + def update_all(self, host_id): + """ + Update all instances on a host. + """ + body = {'update': ''} + self._action(host_id, body) + + def index(self): + """ + Get a list of all hosts. + + :rtype: list of :class:`Hosts`. + """ + return self._list("/mgmt/hosts", "hosts") + + def get(self, host): + """ + Get a specific host. + + :rtype: :class:`host` + """ + return self._get("/mgmt/hosts/%s" % self._get_host_name(host), "host") + + @staticmethod + def _get_host_name(host): + try: + if host.name: + return host.name + except AttributeError: + return host diff --git a/troveclient/instances.py b/troveclient/instances.py new file mode 100644 index 0000000..16e8a6b --- /dev/null +++ b/troveclient/instances.py @@ -0,0 +1,185 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base + +import exceptions +import urlparse + +from troveclient.common import check_for_exceptions +from troveclient.common import limit_url +from troveclient.common import Paginated + + +REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' + + +class Instance(base.Resource): + """ + An Instance is an opaque instance used to store Database instances. + """ + def __repr__(self): + return "" % self.name + + def list_databases(self): + return self.manager.databases.list(self) + + def delete(self): + """ + Delete the instance. + """ + self.manager.delete(self) + + def restart(self): + """ + Restart the database instance + """ + self.manager.restart(self.id) + + +class Instances(base.ManagerWithFind): + """ + Manage :class:`Instance` resources. + """ + resource_class = Instance + + def create(self, name, flavor_id, volume=None, databases=None, users=None, + restorePoint=None): + """ + Create (boot) a new instance. + """ + body = {"instance": { + "name": name, + "flavorRef": flavor_id + }} + if volume: + body["instance"]["volume"] = volume + if databases: + body["instance"]["databases"] = databases + if users: + body["instance"]["users"] = users + if restorePoint: + body["instance"]["restorePoint"] = restorePoint + + return self._create("/instances", body, "instance") + + def _list(self, url, response_key, limit=None, marker=None): + resp, body = self.api.client.get(limit_url(url, limit, marker)) + if not body: + raise Exception("Call to " + url + " did not return a body.") + links = body.get('links', []) + next_links = [link['href'] for link in links if link['rel'] == 'next'] + next_marker = None + for link in next_links: + # Extract the marker from the url. + parsed_url = urlparse.urlparse(link) + query_dict = dict(urlparse.parse_qsl(parsed_url.query)) + next_marker = query_dict.get('marker', None) + instances = body[response_key] + instances = [self.resource_class(self, res) for res in instances] + return Paginated(instances, next_marker=next_marker, links=links) + + def list(self, limit=None, marker=None): + """ + Get a list of all instances. + + :rtype: list of :class:`Instance`. + """ + return self._list("/instances", "instances", limit, marker) + + def get(self, instance): + """ + Get a specific instances. + + :rtype: :class:`Instance` + """ + return self._get("/instances/%s" % base.getid(instance), + "instance") + + def backups(self, instance): + """ + Get the list of backups for a specific instance. + + :rtype: list of :class:`Backups`. + """ + return self._list("/instances/%s/backups" % base.getid(instance), + "backups") + + def delete(self, instance): + """ + Delete the specified instance. + + :param instance_id: The instance id to delete + """ + resp, body = self.api.client.delete("/instances/%s" % + base.getid(instance)) + if resp.status in (422, 500): + raise exceptions.from_response(resp, body) + + def _action(self, instance_id, body): + """ + Perform a server "action" -- reboot/rebuild/resize/etc. + """ + url = "/instances/%s/action" % instance_id + resp, body = self.api.client.post(url, body=body) + check_for_exceptions(resp, body) + if body: + return self.resource_class(self, body, loaded=True) + return body + + def resize_volume(self, instance_id, volume_size): + """ + Resize the volume on an existing instances + """ + body = {"resize": {"volume": {"size": volume_size}}} + self._action(instance_id, body) + + def resize_instance(self, instance_id, flavor_id): + """ + Resize the volume on an existing instances + """ + body = {"resize": {"flavorRef": flavor_id}} + self._action(instance_id, body) + + def restart(self, instance_id): + """ + Restart the database instance. + + :param instance_id: The :class:`Instance` (or its ID) to share onto. + """ + body = {'restart': {}} + self._action(instance_id, body) + + def reset_password(self, instance_id): + """ + Resets the database instance root password. + + :param instance_id: The :class:`Instance` (or its ID) to share onto. + """ + body = {'reset-password': {}} + return self._action(instance_id, body) + +Instances.resize_flavor = Instances.resize_instance + + +class InstanceStatus(object): + + ACTIVE = "ACTIVE" + BLOCKED = "BLOCKED" + BUILD = "BUILD" + FAILED = "FAILED" + REBOOT = "REBOOT" + RESIZE = "RESIZE" + SHUTDOWN = "SHUTDOWN" diff --git a/troveclient/limits.py b/troveclient/limits.py new file mode 100644 index 0000000..96d2fca --- /dev/null +++ b/troveclient/limits.py @@ -0,0 +1,50 @@ +# Copyright (c) 2013 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +import exceptions + + +class Limit(base.Resource): + + def __repr__(self): + return "" % self.verb + + +class Limits(base.ManagerWithFind): + """ + Manages :class `Limit` resources + """ + resource_class = Limit + + def __repr__(self): + return "" % id(self) + + def _list(self, url, response_key): + resp, body = self.api.client.get(url) + + if resp is None or resp.status != 200: + raise exceptions.from_response(resp, body) + + 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): + """ + Retrieve the limits + """ + return self._list("/limits", "limits") diff --git a/troveclient/management.py b/troveclient/management.py new file mode 100644 index 0000000..1e17f92 --- /dev/null +++ b/troveclient/management.py @@ -0,0 +1,136 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +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 + + +class RootHistory(base.Resource): + def __repr__(self): + return ("" + % (self.id, self.created, self.user)) + + +class Management(base.ManagerWithFind): + """ + Manage :class:`Instances` resources. + """ + resource_class = Instance + + def _list(self, url, response_key, limit=None, marker=None): + resp, body = self.api.client.get(limit_url(url, limit, marker)) + if not body: + raise Exception("Call to " + url + " did not return a body.") + links = body.get('links', []) + next_links = [link['href'] for link in links if link['rel'] == 'next'] + next_marker = None + for link in next_links: + # Extract the marker from the url. + parsed_url = urlparse.urlparse(link) + query_dict = dict(urlparse.parse_qsl(parsed_url.query)) + next_marker = query_dict.get('marker', None) + instances = body[response_key] + instances = [self.resource_class(self, res) for res in instances] + return Paginated(instances, next_marker=next_marker, links=links) + + def show(self, instance): + """ + Get details of one instance. + + :rtype: :class:`Instance`. + """ + + return self._get("/mgmt/instances/%s" % base.getid(instance), + 'instance') + + def index(self, deleted=None, limit=None, marker=None): + """ + Show an overview of all local instances. + Optionally, filter by deleted status. + + :rtype: list of :class:`Instance`. + """ + form = '' + if deleted is not None: + if deleted: + form = "?deleted=true" + else: + form = "?deleted=false" + + url = "/mgmt/instances%s" % form + return self._list(url, "instances", limit, marker) + + def root_enabled_history(self, instance): + """ + Get root access history of one instance. + + """ + url = "/mgmt/instances/%s/root" % base.getid(instance) + resp, body = self.api.client.get(url) + if not body: + raise Exception("Call to " + url + " did not return a body.") + return RootHistory(self, body['root_history']) + + def _action(self, instance_id, body): + """ + Perform a server "action" -- reboot/rebuild/resize/etc. + """ + url = "/mgmt/instances/%s/action" % instance_id + resp, body = self.api.client.post(url, body=body) + check_for_exceptions(resp, body) + + def stop(self, instance_id): + body = {'stop': {}} + self._action(instance_id, body) + + def reboot(self, instance_id): + """ + Reboot the underlying OS. + + :param instance_id: The :class:`Instance` (or its ID) to share onto. + """ + body = {'reboot': {}} + self._action(instance_id, body) + + def migrate(self, instance_id, host=None): + """ + Migrate the instance. + + :param instance_id: The :class:`Instance` (or its ID) to share onto. + """ + if host: + body = {'migrate': {'host': host}} + else: + body = {'migrate': {}} + self._action(instance_id, body) + + def update(self, instance_id): + """ + Update the guest agent via apt-get. + """ + body = {'update': {}} + self._action(instance_id, body) + + def reset_task_status(self, instance_id): + """ + Set the task status to NONE. + """ + body = {'reset-task-status': {}} + self._action(instance_id, body) diff --git a/troveclient/mcli.py b/troveclient/mcli.py new file mode 100644 index 0000000..87179c1 --- /dev/null +++ b/troveclient/mcli.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python + +# Copyright 2011 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Trove Management Command line tool +""" + +import json +import optparse +import os +import sys + + +# If ../trove/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +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', + '__init__.py')): + sys.path.insert(0, possible_topdir) + + +from troveclient import common + + +oparser = None + + +def _pretty_print(info): + print json.dumps(info, sort_keys=True, indent=4) + + +class HostCommands(common.AuthedCommandsBase): + """Commands to list info on hosts""" + + params = [ + 'name', + ] + + def update_all(self): + """Update all instances on a host""" + self._require('name') + self.dbaas.hosts.update_all(self.name) + + def get(self): + """List details for the specified host""" + self._require('name') + self._pretty_print(self.dbaas.hosts.get, self.name) + + def list(self): + """List all compute hosts""" + self._pretty_list(self.dbaas.hosts.index) + + +class QuotaCommands(common.AuthedCommandsBase): + """List and update quota limits for a tenant.""" + + params = ['id', + 'instances', + 'volumes', + 'backups'] + + def list(self): + """List all quotas for a tenant""" + self._require('id') + self._pretty_print(self.dbaas.quota.show, self.id) + + def update(self): + """Update quota limits for a tenant""" + self._require('id') + self._pretty_print(self.dbaas.quota.update, self.id, + dict((param, getattr(self, param)) + for param in self.params if param != 'id')) + + +class RootCommands(common.AuthedCommandsBase): + """List details about the root info for an instance.""" + + params = [ + 'id', + ] + + def history(self): + """List root history for the instance.""" + self._require('id') + self._pretty_print(self.dbaas.management.root_enabled_history, self.id) + + +class AccountCommands(common.AuthedCommandsBase): + """Commands to list account info""" + + params = [ + 'id', + ] + + def list(self): + """List all accounts with non-deleted instances""" + self._pretty_print(self.dbaas.accounts.index) + + def get(self): + """List details for the account provided""" + self._require('id') + self._pretty_print(self.dbaas.accounts.show, self.id) + + +class InstanceCommands(common.AuthedCommandsBase): + """List details about an instance.""" + + params = [ + 'deleted', + 'id', + 'limit', + 'marker', + 'host', + ] + + def get(self): + """List details for the instance.""" + self._require('id') + self._pretty_print(self.dbaas.management.show, self.id) + + def list(self): + """List all instances for account""" + deleted = None + if self.deleted is not None: + if self.deleted.lower() in ['true']: + deleted = True + elif self.deleted.lower() in ['false']: + deleted = False + self._pretty_paged(self.dbaas.management.index, deleted=deleted) + + def hwinfo(self): + """Show hardware information details about an instance.""" + self._require('id') + self._pretty_print(self.dbaas.hwinfo.get, self.id) + + def diagnostic(self): + """List diagnostic details about an instance.""" + self._require('id') + self._pretty_print(self.dbaas.diagnostics.get, self.id) + + def stop(self): + """Stop MySQL on the given instance.""" + self._require('id') + self._pretty_print(self.dbaas.management.stop, self.id) + + def reboot(self): + """Reboot the instance.""" + self._require('id') + self._pretty_print(self.dbaas.management.reboot, self.id) + + def migrate(self): + """Migrate the instance.""" + self._require('id') + self._pretty_print(self.dbaas.management.migrate, self.id, self.host) + + def reset_task_status(self): + """Set the instance's task status to NONE.""" + self._require('id') + self._pretty_print(self.dbaas.management.reset_task_status, self.id) + + +class StorageCommands(common.AuthedCommandsBase): + """Commands to list devices info""" + + params = [] + + def list(self): + """List details for the storage device""" + self._pretty_list(self.dbaas.storage.index) + + +def config_options(oparser): + oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", + help="Auth API endpoint URL with port and version. \ + Default: http://localhost:5000/v1.1") + + +COMMANDS = {'account': AccountCommands, + 'host': HostCommands, + 'instance': InstanceCommands, + 'root': RootCommands, + 'storage': StorageCommands, + 'quota': QuotaCommands, + } + + +def main(): + # Parse arguments + oparser = common.CliOptions.create_optparser(True) + for k, v in COMMANDS.items(): + v._prepare_parser(oparser) + (options, args) = oparser.parse_args() + + if not args: + common.print_commands(COMMANDS) + + # Pop the command and check if it's in the known commands + cmd = args.pop(0) + if cmd in COMMANDS: + fn = COMMANDS.get(cmd) + command_object = None + try: + command_object = fn(oparser) + except Exception as ex: + if options.debug: + raise + print(ex) + + # Get a list of supported actions for the command + actions = common.methods_of(command_object) + + if len(args) < 1: + common.print_actions(cmd, actions) + + # Check for a valid action and perform that action + action = args.pop(0) + if action in actions: + try: + getattr(command_object, action)() + except Exception as ex: + if options.debug: + raise + print ex + else: + common.print_actions(cmd, actions) + else: + common.print_commands(COMMANDS) + + +if __name__ == '__main__': + main() diff --git a/troveclient/quota.py b/troveclient/quota.py new file mode 100644 index 0000000..aca7ef8 --- /dev/null +++ b/troveclient/quota.py @@ -0,0 +1,51 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +from troveclient.common import check_for_exceptions + + +class Quotas(base.ManagerWithFind): + """ + Manage :class:`Quota` information. + """ + + resource_class = base.Resource + + def show(self, tenant_id): + """Get a list of all quotas for a tenant id""" + + url = "/mgmt/quotas/%s" % tenant_id + resp, body = self.api.client.get(url) + check_for_exceptions(resp, body) + if not body: + raise Exception("Call to " + url + " did not return a body.") + if 'quotas' not in body: + raise Exception("Missing key value 'quotas' in response body.") + return body['quotas'] + + def update(self, id, quotas): + """ + Set limits for quotas + """ + url = "/mgmt/quotas/%s" % id + body = {"quotas": quotas} + resp, body = self.api.client.put(url, body=body) + check_for_exceptions(resp, body) + if not body: + raise Exception("Call to " + url + " did not return a body.") + if 'quotas' not in body: + raise Exception("Missing key value 'quotas' in response body.") + return body['quotas'] diff --git a/troveclient/root.py b/troveclient/root.py new file mode 100644 index 0000000..e9768af --- /dev/null +++ b/troveclient/root.py @@ -0,0 +1,44 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base + +from troveclient import users +from troveclient.common import check_for_exceptions +import exceptions + + +class Root(base.ManagerWithFind): + """ + Manager class for Root resource + """ + resource_class = users.User + url = "/instances/%s/root" + + def create(self, instance_id): + """ + Enable the root user and return the root password for the + sepcified db instance + """ + resp, body = self.api.client.post(self.url % instance_id) + check_for_exceptions(resp, body) + return body['user']['name'], body['user']['password'] + + def is_root_enabled(self, instance_id): + """ Return True if root is enabled for the instance; + False otherwise""" + resp, body = self.api.client.get(self.url % instance_id) + check_for_exceptions(resp, body) + return body['rootEnabled'] diff --git a/troveclient/security_groups.py b/troveclient/security_groups.py new file mode 100644 index 0000000..caece79 --- /dev/null +++ b/troveclient/security_groups.py @@ -0,0 +1,120 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# 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 import base + +import exceptions +import urlparse + +from troveclient.common import limit_url +from troveclient.common import Paginated + + +class SecurityGroup(base.Resource): + """ + Security Group is a resource used to hold security group information. + """ + def __repr__(self): + return "" % self.name + + +class SecurityGroups(base.ManagerWithFind): + """ + Manage :class:`SecurityGroup` resources. + """ + resource_class = SecurityGroup + + def _list(self, url, response_key, limit=None, marker=None): + resp, body = self.api.client.get(limit_url(url, limit, marker)) + if not body: + raise Exception("Call to " + url + " did not return a body.") + links = body.get('links', []) + next_links = [link['href'] for link in links if link['rel'] == 'next'] + next_marker = None + for link in next_links: + # Extract the marker from the url. + parsed_url = urlparse.urlparse(link) + query_dict = dict(urlparse.parse_qsl(parsed_url.query)) + next_marker = query_dict.get('marker', None) + instances = body[response_key] + instances = [self.resource_class(self, res) for res in instances] + return Paginated(instances, next_marker=next_marker, links=links) + + def list(self, limit=None, marker=None): + """ + Get a list of all security groups. + + :rtype: list of :class:`SecurityGroup`. + """ + return self._list("/security-groups", "security_groups", limit, + marker) + + def get(self, security_group): + """ + Get a specific security group. + + :rtype: :class:`SecurityGroup` + """ + return self._get("/security-groups/%s" % base.getid(security_group), + "security_group") + + +class SecurityGroupRule(base.Resource): + """ + Security Group Rule is a resource used to hold security group + rule related information. + """ + def __repr__(self): + return \ + "" % (self.group_id, self.protocol, self.from_port, + self.to_port, self.cidr) + + +class SecurityGroupRules(base.ManagerWithFind): + """ + Manage :class:`SecurityGroupRules` resources. + """ + resource_class = SecurityGroupRule + + def create(self, group_id, protocol, from_port, to_port, cidr): + """ + Create a new security group rule. + """ + body = {"security_group_rule": { + "group_id": group_id, + "protocol": protocol, + "from_port": from_port, + "to_port": to_port, + "cidr": cidr + }} + return self._create("/security-group-rules", body, + "security_group_rule") + + def delete(self, security_group_rule): + """ + Delete the specified security group rule. + + :param security_group_rule: The security group rule to delete + """ + resp, body = self.api.client.delete("/security-group-rules/%s" % + base.getid(security_group_rule)) + if resp.status in (422, 500): + raise exceptions.from_response(resp, body) diff --git a/troveclient/storage.py b/troveclient/storage.py new file mode 100644 index 0000000..67d332b --- /dev/null +++ b/troveclient/storage.py @@ -0,0 +1,45 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base + + +class Device(base.Resource): + """ + Storage is an opaque instance used to hold storage information. + """ + def __repr__(self): + return "" % self.name + + +class StorageInfo(base.ManagerWithFind): + """ + Manage :class:`Storage` resources. + """ + resource_class = Device + + 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 index(self): + """ + Get a list of all storages. + + :rtype: list of :class:`Storages`. + """ + return self._list("/mgmt/storage", "devices") diff --git a/troveclient/tests/__init__.py b/troveclient/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/troveclient/tests/test_accounts.py b/troveclient/tests/test_accounts.py new file mode 100644 index 0000000..e2716fa --- /dev/null +++ b/troveclient/tests/test_accounts.py @@ -0,0 +1,84 @@ +from testtools import TestCase +from mock import Mock + +from troveclient import accounts +from troveclient import base + +""" +Unit tests for accounts.py +""" + + +class AccountTest(TestCase): + + def setUp(self): + super(AccountTest, self).setUp() + self.orig__init = accounts.Account.__init__ + accounts.Account.__init__ = Mock(return_value=None) + self.account = accounts.Account() + + def tearDown(self): + super(AccountTest, self).tearDown() + accounts.Account.__init__ = self.orig__init + + def test___repr__(self): + self.account.name = "account-1" + self.assertEqual('', self.account.__repr__()) + + +class AccountsTest(TestCase): + + def setUp(self): + super(AccountsTest, self).setUp() + self.orig__init = accounts.Accounts.__init__ + accounts.Accounts.__init__ = Mock(return_value=None) + self.accounts = accounts.Accounts() + self.accounts.api = Mock() + self.accounts.api.client = Mock() + + def tearDown(self): + super(AccountsTest, self).tearDown() + accounts.Accounts.__init__ = self.orig__init + + def test__list(self): + def side_effect_func(self, val): + return val + + self.accounts.resource_class = Mock(side_effect=side_effect_func) + key_ = 'key' + body_ = {key_: "test-value"} + self.accounts.api.client.get = Mock(return_value=('resp', body_)) + self.assertEqual("test-value", self.accounts._list('url', key_)) + + self.accounts.api.client.get = Mock(return_value=('resp', None)) + self.assertRaises(Exception, self.accounts._list, 'url', None) + + def test_index(self): + resp = Mock() + resp.status = 400 + body = {"Accounts": {}} + self.accounts.api.client.get = Mock(return_value=(resp, body)) + self.assertRaises(Exception, self.accounts.index) + resp.status = 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) + + def test_show(self): + def side_effect_func(acct_name, acct): + return acct_name, acct + + account_ = Mock() + account_.name = "test-account" + self.accounts._list = Mock(side_effect=side_effect_func) + self.assertEqual(('/mgmt/accounts/test-account', 'account'), + self.accounts.show(account_)) + + def test__get_account_name(self): + account_ = 'account with no name' + self.assertEqual(account_, + accounts.Accounts._get_account_name(account_)) + account_ = Mock() + account_.name = "account-name" + self.assertEqual("account-name", + accounts.Accounts._get_account_name(account_)) diff --git a/troveclient/tests/test_auth.py b/troveclient/tests/test_auth.py new file mode 100644 index 0000000..28cecb9 --- /dev/null +++ b/troveclient/tests/test_auth.py @@ -0,0 +1,414 @@ +import contextlib + +from testtools import TestCase +from troveclient import auth +from mock import Mock + +from troveclient import exceptions + +""" +Unit tests for the classes and functions in auth.py. +""" + + +def check_url_none(test_case, auth_class): + # url is None, it must throw exception + authObj = auth_class(url=None, type=auth_class, client=None, + username=None, password=None, tenant=None) + try: + authObj.authenticate() + test_case.fail("AuthUrlNotGiven exception expected") + except exceptions.AuthUrlNotGiven: + pass + + +class AuthenticatorTest(TestCase): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + self.orig_load = auth.ServiceCatalog._load + self.orig__init = auth.ServiceCatalog.__init__ + + def tearDown(self): + super(AuthenticatorTest, self).tearDown() + auth.ServiceCatalog._load = self.orig_load + auth.ServiceCatalog.__init__ = self.orig__init + + def test_get_authenticator_cls(self): + class_list = (auth.KeyStoneV2Authenticator, + auth.RaxAuthenticator, + auth.Auth1_1, + auth.FakeAuth) + + for c in class_list: + self.assertEqual(c, auth.get_authenticator_cls(c)) + + class_names = {"keystone": auth.KeyStoneV2Authenticator, + "rax": auth.RaxAuthenticator, + "auth1.1": auth.Auth1_1, + "fake": auth.FakeAuth} + + for cn in class_names.keys(): + self.assertEqual(class_names[cn], auth.get_authenticator_cls(cn)) + + cls_or_name = "_unknown_" + self.assertRaises(ValueError, auth.get_authenticator_cls, cls_or_name) + + def test__authenticate(self): + authObj = auth.Authenticator(Mock(), auth.KeyStoneV2Authenticator, + Mock(), Mock(), Mock(), Mock()) + # test response code 200 + resp = Mock() + resp.status = 200 + body = "test_body" + + auth.ServiceCatalog._load = Mock(return_value=1) + authObj.client._time_request = Mock(return_value=(resp, body)) + + sc = authObj._authenticate(Mock(), Mock()) + self.assertEqual(body, sc.catalog) + + # test AmbiguousEndpoints exception + auth.ServiceCatalog.__init__ = \ + Mock(side_effect=exceptions.AmbiguousEndpoints) + self.assertRaises(exceptions.AmbiguousEndpoints, + authObj._authenticate, Mock(), Mock()) + + # test handling KeyError and raising AuthorizationFailure exception + auth.ServiceCatalog.__init__ = Mock(side_effect=KeyError) + self.assertRaises(exceptions.AuthorizationFailure, + authObj._authenticate, Mock(), Mock()) + + # test EndpointNotFound exception + mock = Mock(side_effect=exceptions.EndpointNotFound) + auth.ServiceCatalog.__init__ = mock + self.assertRaises(exceptions.EndpointNotFound, + authObj._authenticate, Mock(), Mock()) + mock.side_effect = None + + # test response code 305 + resp.__getitem__ = Mock(return_value='loc') + resp.status = 305 + body = "test_body" + authObj.client._time_request = Mock(return_value=(resp, body)) + + l = authObj._authenticate(Mock(), Mock()) + self.assertEqual('loc', l) + + # test any response code other than 200 and 305 + resp.status = 404 + exceptions.from_response = Mock(side_effect=ValueError) + self.assertRaises(ValueError, authObj._authenticate, Mock(), Mock()) + + def test_authenticate(self): + authObj = auth.Authenticator(Mock(), auth.KeyStoneV2Authenticator, + Mock(), Mock(), Mock(), Mock()) + self.assertRaises(NotImplementedError, authObj.authenticate) + + +class KeyStoneV2AuthenticatorTest(TestCase): + + def test_authenticate(self): + # url is None + check_url_none(self, auth.KeyStoneV2Authenticator) + + # url is not None, so it must not throw exception + url = "test_url" + cls_type = auth.KeyStoneV2Authenticator + authObj = auth.KeyStoneV2Authenticator(url=url, type=cls_type, + client=None, username=None, + password=None, tenant=None) + + def side_effect_func(url): + return url + + mock = Mock() + mock.side_effect = side_effect_func + authObj._v2_auth = mock + r = authObj.authenticate() + self.assertEqual(url, r) + + def test__v2_auth(self): + username = "trove_user" + password = "trove_password" + tenant = "tenant" + cls_type = auth.KeyStoneV2Authenticator + authObj = auth.KeyStoneV2Authenticator(url=None, type=cls_type, + client=None, + username=username, + password=password, + tenant=tenant) + + def side_effect_func(url, body): + return body + mock = Mock() + mock.side_effect = side_effect_func + authObj._authenticate = mock + body = authObj._v2_auth(Mock()) + self.assertEqual(username, + body['auth']['passwordCredentials']['username']) + self.assertEqual(password, + body['auth']['passwordCredentials']['password']) + self.assertEqual(tenant, body['auth']['tenantName']) + + +class Auth1_1Test(TestCase): + + def test_authenticate(self): + # handle when url is None + check_url_none(self, auth.Auth1_1) + + # url is not none + username = "trove_user" + password = "trove_password" + url = "test_url" + authObj = auth.Auth1_1(url=url, + type=auth.Auth1_1, + client=None, username=username, + password=password, tenant=None) + + def side_effect_func(auth_url, body, root_key): + return auth_url, body, root_key + + mock = Mock() + mock.side_effect = side_effect_func + authObj._authenticate = mock + auth_url, body, root_key = authObj.authenticate() + + self.assertEqual(username, body['credentials']['username']) + self.assertEqual(password, body['credentials']['key']) + self.assertEqual(auth_url, url) + self.assertEqual('auth', root_key) + + +class RaxAuthenticatorTest(TestCase): + + def test_authenticate(self): + # url is None + check_url_none(self, auth.RaxAuthenticator) + + # url is not None, so it must not throw exception + url = "test_url" + authObj = auth.RaxAuthenticator(url=url, + type=auth.RaxAuthenticator, + client=None, username=None, + password=None, tenant=None) + + def side_effect_func(url): + return url + + mock = Mock() + mock.side_effect = side_effect_func + authObj._rax_auth = mock + r = authObj.authenticate() + self.assertEqual(url, r) + + def test__rax_auth(self): + username = "trove_user" + password = "trove_password" + tenant = "tenant" + authObj = auth.RaxAuthenticator(url=None, + type=auth.RaxAuthenticator, + client=None, username=username, + password=password, tenant=tenant) + + def side_effect_func(url, body): + return body + + mock = Mock() + mock.side_effect = side_effect_func + authObj._authenticate = mock + body = authObj._rax_auth(Mock()) + + v = body['auth']['RAX-KSKEY:apiKeyCredentials']['username'] + self.assertEqual(username, v) + + v = body['auth']['RAX-KSKEY:apiKeyCredentials']['apiKey'] + self.assertEqual(password, v) + + v = body['auth']['RAX-KSKEY:apiKeyCredentials']['tenantName'] + self.assertEqual(tenant, v) + + +class FakeAuthTest(TestCase): + + def test_authenticate(self): + tenant = "tenant" + authObj = auth.FakeAuth(url=None, + type=auth.FakeAuth, + client=None, username=None, + password=None, tenant=tenant) + + fc = authObj.authenticate() + public_url = "%s/%s" % ('http://localhost:8779/v1.0', tenant) + self.assertEqual(public_url, fc.get_public_url()) + self.assertEqual(tenant, fc.get_token()) + + +class ServiceCatalogTest(TestCase): + + def setUp(self): + super(ServiceCatalogTest, self).setUp() + self.orig_url_for = auth.ServiceCatalog._url_for + self.orig__init__ = auth.ServiceCatalog.__init__ + auth.ServiceCatalog.__init__ = Mock(return_value=None) + self.test_url = "http://localhost:1234/test" + + def tearDown(self): + super(ServiceCatalogTest, self).tearDown() + auth.ServiceCatalog._url_for = self.orig_url_for + auth.ServiceCatalog.__init__ = self.orig__init__ + + def test__load(self): + url = "random_url" + auth.ServiceCatalog._url_for = Mock(return_value=url) + + # when service_url is None + scObj = auth.ServiceCatalog() + scObj.region = None + scObj.service_url = None + scObj._load() + self.assertEqual(url, scObj.public_url) + self.assertEqual(url, scObj.management_url) + + # service url is not None + service_url = "service_url" + scObj = auth.ServiceCatalog() + scObj.region = None + scObj.service_url = service_url + scObj._load() + self.assertEqual(service_url, scObj.public_url) + self.assertEqual(service_url, scObj.management_url) + + def test_get_token(self): + test_id = "test_id" + scObj = auth.ServiceCatalog() + scObj.root_key = "root_key" + scObj.catalog = dict() + scObj.catalog[scObj.root_key] = dict() + scObj.catalog[scObj.root_key]['token'] = dict() + scObj.catalog[scObj.root_key]['token']['id'] = test_id + self.assertEqual(test_id, scObj.get_token()) + + def test_get_management_url(self): + test_mng_url = "test_management_url" + scObj = auth.ServiceCatalog() + scObj.management_url = test_mng_url + self.assertEqual(test_mng_url, scObj.get_management_url()) + + def test_get_public_url(self): + test_public_url = "test_public_url" + scObj = auth.ServiceCatalog() + scObj.public_url = test_public_url + self.assertEqual(test_public_url, scObj.get_public_url()) + + def test__url_for(self): + scObj = auth.ServiceCatalog() + + # case for no endpoint found + self.case_no_endpoint_match(scObj) + + # case for empty service catalog + self.case_endpoing_with_empty_catalog(scObj) + + # more than one matching endpoints + self.case_ambiguous_endpoint(scObj) + + # happy case + self.case_unique_endpoint(scObj) + + # testing if-statements in for-loop to iterate services in catalog + self.case_iterating_services_in_catalog(scObj) + + def case_no_endpoint_match(self, scObj): + # empty endpoint list + scObj.catalog = dict() + scObj.catalog['endpoints'] = list() + self.assertRaises(exceptions.EndpointNotFound, scObj._url_for) + + def side_effect_func_ep(attr): + return "test_attr_value" + + # simulating dict + endpoint = Mock() + mock = Mock() + mock.side_effect = side_effect_func_ep + endpoint.__getitem__ = mock + scObj.catalog['endpoints'].append(endpoint) + + # not-empty list but not matching endpoint + filter_value = "not_matching_value" + self.assertRaises(exceptions.EndpointNotFound, scObj._url_for, + attr="test_attr", filter_value=filter_value) + + filter_value = "test_attr_value" # so that we have an endpoint match + scObj.root_key = "access" + scObj.catalog[scObj.root_key] = dict() + self.assertRaises(exceptions.EndpointNotFound, scObj._url_for, + attr="test_attr", filter_value=filter_value) + + def case_endpoing_with_empty_catalog(self, scObj): + # first, test with empty catalog, this should pass since + # there is already enpoint added + scObj.catalog[scObj.root_key]['serviceCatalog'] = list() + + endpoint = scObj.catalog['endpoints'][0] + endpoint.get = Mock(return_value=self.test_url) + r_url = scObj._url_for(attr="test_attr", + filter_value="test_attr_value") + self.assertEqual(self.test_url, r_url) + + def case_ambiguous_endpoint(self, scObj): + scObj.service_type = "trove" + scObj.service_name = "test_service_name" + + def side_effect_func_service(key): + if key == "type": + return "trove" + elif key == "name": + return "test_service_name" + return None + + mock1 = Mock() + mock1.side_effect = side_effect_func_service + service1 = Mock() + service1.get = mock1 + + endpoint2 = {"test_attr": "test_attr_value"} + service1.__getitem__ = Mock(return_value=[endpoint2]) + scObj.catalog[scObj.root_key]['serviceCatalog'] = [service1] + self.assertRaises(exceptions.AmbiguousEndpoints, scObj._url_for, + attr="test_attr", filter_value="test_attr_value") + + def case_unique_endpoint(self, scObj): + # changing the endpoint2 attribute to pass the filter + service1 = scObj.catalog[scObj.root_key]['serviceCatalog'][0] + endpoint2 = service1[0][0] + endpoint2["test_attr"] = "new value not matching filter" + r_url = scObj._url_for(attr="test_attr", + filter_value="test_attr_value") + self.assertEqual(self.test_url, r_url) + + def case_iterating_services_in_catalog(self, scObj): + service1 = scObj.catalog[scObj.root_key]['serviceCatalog'][0] + + scObj.catalog = dict() + scObj.root_key = "access" + scObj.catalog[scObj.root_key] = dict() + scObj.service_type = "no_match" + + scObj.catalog[scObj.root_key]['serviceCatalog'] = [service1] + self.assertRaises(exceptions.EndpointNotFound, scObj._url_for) + + scObj.service_type = "database" + scObj.service_name = "no_match" + self.assertRaises(exceptions.EndpointNotFound, scObj._url_for) + + # no endpoints and no 'serviceCatalog' in catalog => raise exception + scObj = auth.ServiceCatalog() + scObj.catalog = dict() + scObj.root_key = "access" + scObj.catalog[scObj.root_key] = dict() + scObj.catalog[scObj.root_key]['serviceCatalog'] = [] + self.assertRaises(exceptions.EndpointNotFound, scObj._url_for, + attr="test_attr", filter_value="test_attr_value") diff --git a/troveclient/tests/test_base.py b/troveclient/tests/test_base.py new file mode 100644 index 0000000..ac7318b --- /dev/null +++ b/troveclient/tests/test_base.py @@ -0,0 +1,447 @@ +import contextlib +import os + +from testtools import TestCase +from mock import Mock + +from troveclient import base +from troveclient import exceptions +from troveclient import utils + +""" +Unit tests for base.py +""" + + +def obj_class(self, res, loaded=True): + return res + + +class BaseTest(TestCase): + + def test_getid(self): + obj = "test" + r = base.getid(obj) + self.assertEqual(obj, r) + + test_id = "test_id" + obj = Mock() + obj.id = test_id + r = base.getid(obj) + self.assertEqual(test_id, r) + + +class ManagerTest(TestCase): + + def setUp(self): + super(ManagerTest, self).setUp() + self.orig__init = base.Manager.__init__ + base.Manager.__init__ = Mock(return_value=None) + self.orig_os_makedirs = os.makedirs + + def tearDown(self): + super(ManagerTest, self).tearDown() + base.Manager.__init__ = self.orig__init + os.makedirs = self.orig_os_makedirs + + def test___init__(self): + api = Mock() + base.Manager.__init__ = self.orig__init + manager = base.Manager(api) + self.assertEqual(api, manager.api) + + def test_completion_cache(self): + manager = base.Manager() + + # handling exceptions + mode = "w" + cache_type = "unittest" + obj_class = Mock + with manager.completion_cache(cache_type, obj_class, mode): + pass + + os.makedirs = Mock(side_effect=OSError) + with manager.completion_cache(cache_type, obj_class, mode): + pass + + def test_write_to_completion_cache(self): + manager = base.Manager() + + # no cache object, nothing should happen + manager.write_to_completion_cache("non-exist", "val") + + def side_effect_func(val): + return val + + manager._mock_cache = Mock() + manager._mock_cache.write = Mock(return_value=None) + manager.write_to_completion_cache("mock", "val") + self.assertEqual(1, manager._mock_cache.write.call_count) + + def _get_mock(self): + manager = base.Manager() + manager.api = Mock() + manager.api.client = Mock() + + def side_effect_func(self, body, loaded=True): + return body + + manager.resource_class = Mock(side_effect=side_effect_func) + return manager + + def test__get_with_response_key_none(self): + manager = self._get_mock() + url_ = "test-url" + body_ = "test-body" + resp_ = "test-resp" + manager.api.client.get = Mock(return_value=(resp_, body_)) + r = manager._get(url=url_, response_key=None) + self.assertEqual(body_, r) + + def test__get_with_response_key(self): + manager = self._get_mock() + response_key = "response_key" + body_ = {response_key: "test-resp-key-body"} + url_ = "test_url_get" + manager.api.client.get = Mock(return_value=(url_, body_)) + r = manager._get(url=url_, response_key=response_key) + self.assertEqual(body_[response_key], r) + + def test__create(self): + manager = base.Manager() + manager.api = Mock() + manager.api.client = Mock() + + response_key = "response_key" + data_ = "test-data" + body_ = {response_key: data_} + url_ = "test_url_post" + manager.api.client.post = Mock(return_value=(url_, body_)) + + return_raw = True + r = manager._create(url_, body_, response_key, return_raw) + self.assertEqual(data_, r) + + return_raw = False + + @contextlib.contextmanager + def completion_cache_mock(*arg, **kwargs): + yield + + mock = Mock() + mock.side_effect = completion_cache_mock + manager.completion_cache = mock + + manager.resource_class = Mock(return_value="test-class") + r = manager._create(url_, body_, response_key, return_raw) + self.assertEqual("test-class", r) + + def get_mock_mng_api_client(self): + manager = base.Manager() + manager.api = Mock() + manager.api.client = Mock() + return manager + + def test__delete(self): + resp_ = "test-resp" + body_ = "test-body" + + manager = self.get_mock_mng_api_client() + manager.api.client.delete = Mock(return_value=(resp_, body_)) + # _delete just calls api.client.delete, and does nothing + # the correctness should be tested in api class + manager._delete("test-url") + pass + + def test__update(self): + resp_ = "test-resp" + body_ = "test-body" + + manager = self.get_mock_mng_api_client() + manager.api.client.put = Mock(return_value=(resp_, body_)) + body = manager._update("test-url", body_) + self.assertEqual(body_, body) + + +class ManagerListTest(ManagerTest): + + def setUp(self): + super(ManagerListTest, self).setUp() + + @contextlib.contextmanager + def completion_cache_mock(*arg, **kwargs): + yield + + self.manager = base.Manager() + self.manager.api = Mock() + self.manager.api.client = Mock() + + self.response_key = "response_key" + self.data_p = ["p1", "p2"] + self.body_p = {self.response_key: self.data_p} + self.url_p = "test_url_post" + self.manager.api.client.post = Mock(return_value=(self.url_p, + self.body_p)) + + self.data_g = ["g1", "g2", "g3"] + self.body_g = {self.response_key: self.data_g} + self.url_g = "test_url_get" + self.manager.api.client.get = Mock(return_value=(self.url_g, + self.body_g)) + + mock = Mock() + mock.side_effect = completion_cache_mock + self.manager.completion_cache = mock + + def tearDown(self): + super(ManagerListTest, self).tearDown() + + def obj_class(self, res, loaded=True): + return res + + def test_list_with_body_none(self): + body = None + l = self.manager._list("url", self.response_key, obj_class, body) + self.assertEqual(len(self.data_g), len(l)) + for i in range(0, len(l)): + self.assertEqual(self.data_g[i], l[i]) + + def test_list_body_not_none(self): + body = "something" + l = self.manager._list("url", self.response_key, obj_class, body) + self.assertEqual(len(self.data_p), len(l)) + for i in range(0, len(l)): + self.assertEqual(self.data_p[i], l[i]) + + def test_list_key_mapping(self): + data_ = {"values": ["p1", "p2"]} + body_ = {self.response_key: data_} + url_ = "test_url_post" + self.manager.api.client.post = Mock(return_value=(url_, body_)) + l = self.manager._list("url", self.response_key, + obj_class, "something") + data = data_["values"] + self.assertEqual(len(data), len(l)) + for i in range(0, len(l)): + self.assertEqual(data[i], l[i]) + + def test_list_without_key_mapping(self): + data_ = {"v1": "1", "v2": "2"} + body_ = {self.response_key: data_} + url_ = "test_url_post" + self.manager.api.client.post = Mock(return_value=(url_, body_)) + l = self.manager._list("url", self.response_key, + obj_class, "something") + self.assertEqual(len(data_), len(l)) + + +class ManagerWithFind(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() + + def tearDown(self): + super(ManagerWithFind, self).tearDown() + base.ManagerWithFind.__init__ = self.orig__init + + def test_find(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]) + self.manager.resource_class = Mock + + # exactly one match case + found = self.manager.find(attr1="v1", attr2="v2", attr3="v3") + self.assertEqual(obj1, found) + + # 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) + + +class ResourceTest(TestCase): + + def setUp(self): + super(ResourceTest, self).setUp() + self.orig___init__ = base.Resource.__init__ + + def tearDown(self): + super(ResourceTest, self).tearDown() + base.Resource.__init__ = self.orig___init__ + + def test___init__(self): + manager = Mock() + manager.write_to_completion_cache = Mock(return_value=None) + + info_ = {} + robj = base.Resource(manager, info_) + self.assertEqual(0, manager.write_to_completion_cache.call_count) + + info_ = {"id": "id-with-less-than-36-char"} + robj = base.Resource(manager, info_) + self.assertEqual(info_["id"], robj.id) + self.assertEqual(0, manager.write_to_completion_cache.call_count) + + id_ = "id-with-36-char-" + for i in range(36 - len(id_)): + id_ = id_ + "-" + info_ = {"id": id_} + robj = base.Resource(manager, info_) + self.assertEqual(info_["id"], robj.id) + self.assertEqual(1, manager.write_to_completion_cache.call_count) + + info_["name"] = "test-human-id" + # Resource.HUMAN_ID is False + robj = base.Resource(manager, info_) + self.assertEqual(info_["id"], robj.id) + self.assertEqual(None, robj.human_id) + self.assertEqual(2, manager.write_to_completion_cache.call_count) + + # base.Resource.HUMAN_ID = True + info_["HUMAN_ID"] = True + robj = base.Resource(manager, info_) + self.assertEqual(info_["id"], robj.id) + self.assertEqual(info_["name"], robj.human_id) + self.assertEqual(4, manager.write_to_completion_cache.call_count) + + def test_human_id(self): + manager = Mock() + manager.write_to_completion_cache = Mock(return_value=None) + + info_ = {"name": "test-human-id"} + robj = base.Resource(manager, info_) + self.assertEqual(None, robj.human_id) + + info_["HUMAN_ID"] = True + robj = base.Resource(manager, info_) + self.assertEqual(info_["name"], robj.human_id) + robj.name = "new-human-id" + self.assertEqual("new-human-id", robj.human_id) + + def get_mock_resource_obj(self): + base.Resource.__init__ = Mock(return_value=None) + robj = base.Resource() + return robj + + def test__add_details(self): + robj = self.get_mock_resource_obj() + info_ = {"name": "test-human-id", "test_attr": 5} + robj._add_details(info_) + self.assertEqual(info_["name"], robj.name) + self.assertEqual(info_["test_attr"], robj.test_attr) + + def test___getattr__(self): + robj = self.get_mock_resource_obj() + info_ = {"name": "test-human-id", "test_attr": 5} + robj._add_details(info_) + self.assertEqual(info_["test_attr"], robj.__getattr__("test_attr")) + + # TODO: looks like causing infinite recursive calls + #robj.__getattr__("test_non_exist_attr") + + def test___repr__(self): + robj = self.get_mock_resource_obj() + info_ = {"name": "test-human-id", "test_attr": 5} + robj._add_details(info_) + + expected = "" + self.assertEqual(expected, robj.__repr__()) + + def test_get(self): + robj = self.get_mock_resource_obj() + manager = Mock() + manager.get = None + + robj.manager = object() + robj.get() + + manager = Mock() + robj.manager = Mock() + + robj.id = "id" + new = Mock() + new._info = {"name": "test-human-id", "test_attr": 5} + robj.manager.get = Mock(return_value=new) + robj.get() + self.assertEqual("test-human-id", robj.name) + self.assertEqual(5, robj.test_attr) + + def tes___eq__(self): + robj = self.get_mock_resource_obj() + other = base.Resource() + + info_ = {"name": "test-human-id", "test_attr": 5} + robj._info = info_ + other._info = {} + self.assertNotTrue(robj.__eq__(other)) + + robj._info = info_ + self.assertTrue(robj.__eq__(other)) + + robj.id = "rid" + other.id = "oid" + self.assertNotTrue(robj.__eq__(other)) + + other.id = "rid" + self.assertTrue(robj.__eq__(other)) + + # not instance of the same class + other = Mock() + self.assertNotTrue(robj.__eq__(other)) + + def test_is_loaded(self): + robj = self.get_mock_resource_obj() + robj._loaded = True + self.assertTrue(robj.is_loaded()) + + robj._loaded = False + self.assertFalse(robj.is_loaded()) + + def test_set_loaded(self): + robj = self.get_mock_resource_obj() + robj.set_loaded(True) + self.assertTrue(robj._loaded) + + robj.set_loaded(False) + self.assertFalse(robj._loaded) diff --git a/troveclient/tests/test_client.py b/troveclient/tests/test_client.py new file mode 100644 index 0000000..263316b --- /dev/null +++ b/troveclient/tests/test_client.py @@ -0,0 +1,322 @@ +import contextlib +import os +import logging +import httplib2 +import time + +from testtools import TestCase +from mock import Mock + +from troveclient import client +from troveclient import exceptions +from troveclient import utils + +""" +Unit tests for client.py +""" + + +class ClientTest(TestCase): + + def test_log_to_streamhandler(self): + client.log_to_streamhandler() + self.assertTrue(client._logger.level == logging.DEBUG) + + +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) diff --git a/troveclient/tests/test_common.py b/troveclient/tests/test_common.py new file mode 100644 index 0000000..e1b488a --- /dev/null +++ b/troveclient/tests/test_common.py @@ -0,0 +1,395 @@ +import sys +import optparse +import json +import collections + +from testtools import TestCase +from mock import Mock + +from troveclient import common +from troveclient import client + +""" + unit tests for common.py +""" + + +class CommonTest(TestCase): + + def setUp(self): + super(CommonTest, self).setUp() + self.orig_sys_exit = sys.exit + sys.exit = Mock(return_value=None) + + def tearDown(self): + super(CommonTest, self).tearDown() + sys.exit = self.orig_sys_exit + + def test_methods_of(self): + class DummyClass: + def dummyMethod(self): + print("just for test") + + obj = DummyClass() + result = common.methods_of(obj) + self.assertEqual(1, len(result)) + method = result['dummyMethod'] + self.assertIsNotNone(method) + + def test_check_for_exceptions(self): + status = [400, 422, 500] + for s in status: + resp = Mock() + resp.status = s + self.assertRaises(Exception, + common.check_for_exceptions, resp, "body") + + # a no-exception case + resp = Mock() + resp.status = 200 + common.check_for_exceptions(resp, "body") + + def test_print_actions(self): + cmd = "test-cmd" + actions = {"test": "test action", "help": "help action"} + common.print_actions(cmd, actions) + pass + + def test_print_commands(self): + commands = {"cmd-1": "cmd 1", "cmd-2": "cmd 2"} + common.print_commands(commands) + pass + + def test_limit_url(self): + url_ = "test-url" + limit_ = None + marker_ = None + self.assertEqual(url_, common.limit_url(url_)) + + limit_ = "test-limit" + marker_ = "test-marker" + expected = "test-url?marker=test-marker&limit=test-limit" + self.assertEqual(expected, + common.limit_url(url_, limit=limit_, marker=marker_)) + + +class CliOptionsTest(TestCase): + + def check_default_options(self, co): + self.assertEqual(None, co.username) + self.assertEqual(None, co.apikey) + self.assertEqual(None, co.tenant_id) + self.assertEqual(None, co.auth_url) + self.assertEqual('keystone', co.auth_type) + self.assertEqual('database', co.service_type) + self.assertEqual('trove', co.service_name) + self.assertEqual('RegionOne', co.region) + self.assertEqual(None, co.service_url) + self.assertFalse(co.insecure) + self.assertFalse(co.verbose) + self.assertFalse(co.debug) + self.assertEqual(None, co.token) + self.assertEqual(None, co.xml) + + def check_option(self, oparser, option_name): + option = oparser.get_option("--%s" % option_name) + self.assertNotEqual(None, option) + if option_name in common.CliOptions.DEFAULT_VALUES: + self.assertEqual(common.CliOptions.DEFAULT_VALUES[option_name], + option.default) + + def test___init__(self): + co = common.CliOptions() + self.check_default_options(co) + + def test_deafult(self): + co = common.CliOptions.default() + self.check_default_options(co) + + def test_load_from_file(self): + co = common.CliOptions.load_from_file() + self.check_default_options(co) + + def test_create_optparser(self): + option_names = ["verbose", "debug", "auth_url", "username", "apikey", + "tenant_id", "auth_type", "service_type", + "service_name", "service_type", "service_name", + "service_url", "region", "insecure", "token", + "xml", "secure", "json", "terse", "hide-debug"] + + oparser = common.CliOptions.create_optparser(True) + for option_name in option_names: + self.check_option(oparser, option_name) + + oparser = common.CliOptions.create_optparser(False) + for option_name in option_names: + self.check_option(oparser, option_name) + + +class ArgumentRequiredTest(TestCase): + + def setUp(self): + super(ArgumentRequiredTest, self).setUp() + self.param = "test-param" + self.arg_req = common.ArgumentRequired(self.param) + + def test___init__(self): + self.assertEqual(self.param, self.arg_req.param) + + def test___str__(self): + expected = 'Argument "--%s" required.' % self.param + self.assertEqual(expected, self.arg_req.__str__()) + + +class CommandsBaseTest(TestCase): + + def setUp(self): + super(CommandsBaseTest, self).setUp() + self.orig_sys_exit = sys.exit + sys.exit = Mock(return_value=None) + parser = common.CliOptions().create_optparser(False) + self.cmd_base = common.CommandsBase(parser) + + def tearDown(self): + super(CommandsBaseTest, self).tearDown() + sys.exit = self.orig_sys_exit + + 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 + r = self.cmd_base._safe_exec(func) + self.assertEqual("test", r) + + self.cmd_base.debug = False + r = self.cmd_base._safe_exec(func) + self.assertEqual("test", r) + + func = Mock(side_effect=ValueError) # an arbitrary exception + r = self.cmd_base._safe_exec(func) + self.assertEqual(None, r) + + def test__prepare_parser(self): + parser = optparse.OptionParser() + common.CommandsBase.params = ["test_1", "test_2"] + self.cmd_base._prepare_parser(parser) + option = parser.get_option("--%s" % "test_1") + self.assertNotEqual(None, option) + option = parser.get_option("--%s" % "test_2") + self.assertNotEqual(None, option) + + def test__parse_options(self): + parser = optparse.OptionParser() + parser.add_option("--%s" % "test_1", default="test_1v") + parser.add_option("--%s" % "test_2", default="test_2v") + self.cmd_base._parse_options(parser) + self.assertEqual("test_1v", self.cmd_base.test_1) + self.assertEqual("test_2v", self.cmd_base.test_2) + + def test__require(self): + self.assertRaises(common.ArgumentRequired, + self.cmd_base._require, "attr_1") + self.cmd_base.attr_1 = None + self.assertRaises(common.ArgumentRequired, + self.cmd_base._require, "attr_1") + self.cmd_base.attr_1 = "attr_v1" + self.cmd_base._require("attr_1") + + def test__make_list(self): + self.assertRaises(AttributeError, self.cmd_base._make_list, "attr1") + self.cmd_base.attr1 = "v1,v2" + self.cmd_base._make_list("attr1") + self.assertEqual(["v1", "v2"], self.cmd_base.attr1) + self.cmd_base.attr1 = ["v3"] + self.cmd_base._make_list("attr1") + self.assertEqual(["v3"], self.cmd_base.attr1) + + def test__pretty_print(self): + func = Mock(return_value=None) + self.cmd_base.verbose = True + self.assertEqual(None, self.cmd_base._pretty_print(func)) + self.cmd_base.verbose = False + self.assertEqual(None, self.cmd_base._pretty_print(func)) + + def test__dumps(self): + json.dumps = Mock(return_value="test-dump") + self.assertEqual("test-dump", self.cmd_base._dumps("item")) + + def test__pretty_list(self): + func = Mock(return_value=None) + self.cmd_base.verbose = True + self.assertEqual(None, self.cmd_base._pretty_list(func)) + self.cmd_base.verbose = False + self.assertEqual(None, self.cmd_base._pretty_list(func)) + item = Mock(return_value="test") + item._info = "info" + func = Mock(return_value=[item]) + self.assertEqual(None, self.cmd_base._pretty_list(func)) + + def test__pretty_paged(self): + self.cmd_base.limit = "5" + func = Mock(return_value=None) + self.cmd_base.verbose = True + self.assertEqual(None, self.cmd_base._pretty_paged(func)) + + self.cmd_base.verbose = False + + class MockIterable(collections.Iterable): + links = ["item"] + count = 1 + + def __iter__(self): + return ["item1"] + + def __len__(self): + return count + + ret = MockIterable() + func = Mock(return_value=ret) + self.assertEqual(None, self.cmd_base._pretty_paged(func)) + + ret.count = 0 + self.assertEqual(None, self.cmd_base._pretty_paged(func)) + + func = Mock(side_effect=ValueError) + self.assertEqual(None, self.cmd_base._pretty_paged(func)) + self.cmd_base.debug = True + self.cmd_base.marker = Mock() + self.assertRaises(ValueError, self.cmd_base._pretty_paged, func) + + +class AuthTest(TestCase): + + def setUp(self): + super(AuthTest, self).setUp() + self.orig_sys_exit = sys.exit + sys.exit = Mock(return_value=None) + self.parser = common.CliOptions().create_optparser(False) + self.auth = common.Auth(self.parser) + + def tearDown(self): + super(AuthTest, self).tearDown() + sys.exit = self.orig_sys_exit + + def test___init__(self): + self.assertEqual(None, self.auth.dbaas) + self.assertEqual(None, self.auth.apikey) + + def test_login(self): + self.auth.username = "username" + self.auth.apikey = "apikey" + self.auth.tenant_id = "tenant_id" + self.auth.auth_url = "auth_url" + dbaas = Mock() + dbaas.authenticate = Mock(return_value=None) + dbaas.client = Mock() + dbaas.client.auth_token = Mock() + dbaas.client.service_url = Mock() + self.auth._get_client = Mock(return_value=dbaas) + self.auth.login() + + self.auth.debug = True + self.auth._get_client = Mock(side_effect=ValueError) + self.assertRaises(ValueError, self.auth.login) + + self.auth.debug = False + self.auth.login() + + +class AuthedCommandsBaseTest(TestCase): + + def setUp(self): + super(AuthedCommandsBaseTest, self).setUp() + self.orig_sys_exit = sys.exit + sys.exit = Mock(return_value=None) + + def tearDown(self): + super(AuthedCommandsBaseTest, self).tearDown() + sys.exit = self.orig_sys_exit + + def test___init__(self): + parser = common.CliOptions().create_optparser(False) + common.AuthedCommandsBase.debug = True + dbaas = Mock() + dbaas.authenticate = Mock(return_value=None) + dbaas.client = Mock() + dbaas.client.auth_token = Mock() + dbaas.client.service_url = Mock() + dbaas.client.authenticate_with_token = Mock() + common.AuthedCommandsBase._get_client = Mock(return_value=dbaas) + authed_cmd = common.AuthedCommandsBase(parser) + + +class PaginatedTest(TestCase): + + def setUp(self): + super(PaginatedTest, self).setUp() + self.items_ = ["item1", "item2"] + self.next_marker_ = "next-marker" + self.links_ = ["link1", "link2"] + self.pgn = common.Paginated(self.items_, self.next_marker_, + self.links_) + + def tearDown(self): + super(PaginatedTest, self).tearDown() + + def test___init__(self): + self.assertEqual(self.items_, self.pgn.items) + self.assertEqual(self.next_marker_, self.pgn.next) + self.assertEqual(self.links_, self.pgn.links) + + def test___len__(self): + self.assertEqual(len(self.items_), self.pgn.__len__()) + + def test___iter__(self): + itr_expected = self.items_.__iter__() + itr = self.pgn.__iter__() + self.assertEqual(itr_expected.next(), itr.next()) + self.assertEqual(itr_expected.next(), itr.next()) + self.assertRaises(StopIteration, itr_expected.next) + self.assertRaises(StopIteration, itr.next) + + def test___getitem__(self): + self.assertEqual(self.items_[0], self.pgn.__getitem__(0)) + + def test___setitem__(self): + self.pgn.__setitem__(0, "new-item") + self.assertEqual("new-item", self.pgn.items[0]) + + def test___delitem(self): + del self.pgn[0] + self.assertEqual(1, self.pgn.__len__()) + + def test___reversed__(self): + itr = self.pgn.__reversed__() + expected = ["item2", "item1"] + self.assertEqual("item2", itr.next()) + self.assertEqual("item1", itr.next()) + self.assertRaises(StopIteration, itr.next) + + def test___contains__(self): + self.assertTrue(self.pgn.__contains__("item1")) + self.assertTrue(self.pgn.__contains__("item2")) + self.assertFalse(self.pgn.__contains__("item3")) diff --git a/troveclient/tests/test_instances.py b/troveclient/tests/test_instances.py new file mode 100644 index 0000000..05e4019 --- /dev/null +++ b/troveclient/tests/test_instances.py @@ -0,0 +1,176 @@ +from testtools import TestCase +from mock import Mock + +from troveclient import instances +from troveclient import base + +""" +Unit tests for instances.py +""" + + +class InstanceTest(TestCase): + + def setUp(self): + super(InstanceTest, self).setUp() + self.orig__init = instances.Instance.__init__ + instances.Instance.__init__ = Mock(return_value=None) + self.instance = instances.Instance() + self.instance.manager = Mock() + + def tearDown(self): + super(InstanceTest, self).tearDown() + instances.Instance.__init__ = self.orig__init + + def test___repr__(self): + self.instance.name = "instance-1" + self.assertEqual('', self.instance.__repr__()) + + def test_list_databases(self): + db_list = ['database1', 'database2'] + self.instance.manager.databases = Mock() + self.instance.manager.databases.list = Mock(return_value=db_list) + self.assertEqual(db_list, self.instance.list_databases()) + + def test_delete(self): + db_delete_mock = Mock(return_value=None) + self.instance.manager.delete = db_delete_mock + self.instance.delete() + self.assertEqual(1, db_delete_mock.call_count) + + def test_restart(self): + db_restart_mock = Mock(return_value=None) + self.instance.manager.restart = db_restart_mock + self.instance.id = 1 + self.instance.restart() + self.assertEqual(1, db_restart_mock.call_count) + + +class InstancesTest(TestCase): + + def setUp(self): + super(InstancesTest, self).setUp() + self.orig__init = instances.Instances.__init__ + instances.Instances.__init__ = Mock(return_value=None) + self.instances = instances.Instances() + self.instances.api = Mock() + self.instances.api.client = Mock() + self.instances.resource_class = Mock(return_value="instance-1") + + self.orig_base_getid = base.getid + base.getid = Mock(return_value="instance1") + + def tearDown(self): + super(InstancesTest, self).tearDown() + instances.Instances.__init__ = self.orig__init + base.getid = self.orig_base_getid + + def test_create(self): + def side_effect_func(path, body, inst): + return path, body, inst + + self.instances._create = Mock(side_effect=side_effect_func) + p, b, i = self.instances.create("test-name", 103, "test-volume", + ['db1', 'db2'], ['u1', 'u2']) + self.assertEqual("/instances", p) + self.assertEqual("instance", i) + self.assertEqual(['db1', 'db2'], b["instance"]["databases"]) + self.assertEqual(['u1', 'u2'], b["instance"]["users"]) + self.assertEqual("test-name", b["instance"]["name"]) + self.assertEqual("test-volume", b["instance"]["volume"]) + self.assertEqual(103, b["instance"]["flavorRef"]) + + def test__list(self): + self.instances.api.client.get = Mock(return_value=('resp', None)) + self.assertRaises(Exception, self.instances._list, "url", None) + + body = Mock() + body.get = Mock(return_value=[{'href': 'http://test.net/test_file', + 'rel': 'next'}]) + body.__getitem__ = Mock(return_value='instance1') + #self.instances.resource_class = Mock(return_value="instance-1") + self.instances.api.client.get = Mock(return_value=('resp', body)) + _expected = [{'href': 'http://test.net/test_file', 'rel': 'next'}] + self.assertEqual(_expected, self.instances._list("url", None).links) + + def test_list(self): + def side_effect_func(path, inst, limit, marker): + return path, inst, limit, marker + + self.instances._list = Mock(side_effect=side_effect_func) + limit_ = "test-limit" + marker_ = "test-marker" + expected = ("/instances", "instances", limit_, marker_) + self.assertEqual(expected, self.instances.list(limit_, marker_)) + + def test_get(self): + def side_effect_func(path, inst): + return path, inst + + self.instances._get = Mock(side_effect=side_effect_func) + self.assertEqual(('/instances/instance1', 'instance'), + self.instances.get(1)) + + def test_delete(self): + resp = Mock() + resp.status = 200 + body = None + self.instances.api.client.delete = Mock(return_value=(resp, body)) + self.instances.delete('instance1') + resp.status = 500 + self.assertRaises(Exception, self.instances.delete, 'instance1') + + def test__action(self): + body = Mock() + resp = Mock() + resp.status = 200 + self.instances.api.client.post = Mock(return_value=(resp, body)) + self.assertEqual('instance-1', self.instances._action(1, body)) + + self.instances.api.client.post = Mock(return_value=(resp, None)) + self.assertEqual(None, self.instances._action(1, body)) + + def _set_action_mock(self): + def side_effect_func(instance_id, body): + self._instance_id = instance_id + self._body = body + + self._instance_id = None + self._body = None + self.instances._action = Mock(side_effect=side_effect_func) + + def test_resize_volume(self): + self._set_action_mock() + self.instances.resize_volume(152, 512) + self.assertEqual(152, self._instance_id) + self.assertEqual({"resize": {"volume": {"size": 512}}}, self._body) + + def test_resize_instance(self): + self._set_action_mock() + self.instances.resize_instance(4725, 103) + self.assertEqual(4725, self._instance_id) + self.assertEqual({"resize": {"flavorRef": 103}}, self._body) + + def test_restart(self): + self._set_action_mock() + self.instances.restart(253) + self.assertEqual(253, self._instance_id) + self.assertEqual({'restart': {}}, self._body) + + def test_reset_password(self): + self._set_action_mock() + self.instances.reset_password(634) + self.assertEqual(634, self._instance_id) + self.assertEqual({'reset-password': {}}, self._body) + + +class InstanceStatusTest(TestCase): + + def test_constants(self): + self.assertEqual("ACTIVE", instances.InstanceStatus.ACTIVE) + self.assertEqual("BLOCKED", instances.InstanceStatus.BLOCKED) + self.assertEqual("BUILD", instances.InstanceStatus.BUILD) + self.assertEqual("FAILED", instances.InstanceStatus.FAILED) + self.assertEqual("REBOOT", instances.InstanceStatus.REBOOT) + self.assertEqual("RESIZE", instances.InstanceStatus.RESIZE) + self.assertEqual("SHUTDOWN", instances.InstanceStatus.SHUTDOWN) diff --git a/troveclient/tests/test_limits.py b/troveclient/tests/test_limits.py new file mode 100644 index 0000000..d40b646 --- /dev/null +++ b/troveclient/tests/test_limits.py @@ -0,0 +1,79 @@ +from testtools import TestCase +from mock import Mock +from troveclient import limits + + +class LimitsTest(TestCase): + """ + This class tests the calling code for the Limits API + """ + + def setUp(self): + super(LimitsTest, self).setUp() + self.limits = limits.Limits(Mock()) + self.limits.api.client = Mock() + + def tearDown(self): + super(LimitsTest, self).tearDown() + + def test_list(self): + resp = Mock() + resp.status = 200 + body = {"limits": + [ + {'maxTotalInstances': 55, + 'verb': 'ABSOLUTE', + 'maxTotalVolumes': 100}, + {'regex': '.*', + 'nextAvailable': '2011-07-21T18:17:06Z', + 'uri': '*', + 'value': 10, + 'verb': 'POST', + 'remaining': 2, 'unit': 'MINUTE'}, + {'regex': '.*', + 'nextAvailable': '2011-07-21T18:17:06Z', + 'uri': '*', + 'value': 10, + 'verb': 'PUT', + 'remaining': 2, + 'unit': 'MINUTE'}, + {'regex': '.*', + 'nextAvailable': '2011-07-21T18:17:06Z', + 'uri': '*', + 'value': 10, + 'verb': 'DELETE', + 'remaining': 2, + 'unit': 'MINUTE'}, + {'regex': '.*', + 'nextAvailable': '2011-07-21T18:17:06Z', + 'uri': '*', + 'value': 10, + 'verb': 'GET', + 'remaining': 2, 'unit': 'MINUTE'}]} + response = (resp, body) + + mock_get = Mock(return_value=response) + self.limits.api.client.get = mock_get + self.assertIsNotNone(self.limits.list()) + mock_get.assert_called_once_with("/limits") + + def test_list_errors(self): + status_list = [400, 401, 403, 404, 408, 409, 413, 500, 501] + for status_code in status_list: + self._check_error_response(status_code) + + def _check_error_response(self, status_code): + RESPONSE_KEY = "limits" + + resp = Mock() + resp.status = status_code + body = {RESPONSE_KEY: { + 'absolute': {}, + 'rate': [ + {'limit': [] + }]}} + response = (resp, body) + + mock_get = Mock(return_value=response) + self.limits.api.client.get = mock_get + self.assertRaises(Exception, self.limits.list) diff --git a/troveclient/tests/test_management.py b/troveclient/tests/test_management.py new file mode 100644 index 0000000..c04e216 --- /dev/null +++ b/troveclient/tests/test_management.py @@ -0,0 +1,144 @@ +from testtools import TestCase +from mock import Mock + +from troveclient import management +from troveclient import base + +""" +Unit tests for management.py +""" + + +class RootHistoryTest(TestCase): + + def setUp(self): + super(RootHistoryTest, self).setUp() + self.orig__init = management.RootHistory.__init__ + management.RootHistory.__init__ = Mock(return_value=None) + + def tearDown(self): + super(RootHistoryTest, self).tearDown() + management.RootHistory.__init__ = self.orig__init + + def test___repr__(self): + root_history = management.RootHistory() + root_history.id = "1" + root_history.created = "ct" + root_history.user = "tu" + self.assertEqual('', + root_history.__repr__()) + + +class ManagementTest(TestCase): + + def setUp(self): + super(ManagementTest, self).setUp() + self.orig__init = management.Management.__init__ + management.Management.__init__ = Mock(return_value=None) + self.management = management.Management() + self.management.api = Mock() + self.management.api.client = Mock() + + self.orig_hist__init = management.RootHistory.__init__ + self.orig_base_getid = base.getid + base.getid = Mock(return_value="instance1") + + def tearDown(self): + super(ManagementTest, self).tearDown() + management.Management.__init__ = self.orig__init + management.RootHistory.__init__ = self.orig_hist__init + base.getid = self.orig_base_getid + + def test__list(self): + self.management.api.client.get = Mock(return_value=('resp', None)) + self.assertRaises(Exception, self.management._list, "url", None) + + body = Mock() + body.get = Mock(return_value=[{'href': 'http://test.net/test_file', + 'rel': 'next'}]) + body.__getitem__ = Mock(return_value='instance1') + self.management.resource_class = Mock(return_value="instance-1") + self.management.api.client.get = Mock(return_value=('resp', body)) + _expected = [{'href': 'http://test.net/test_file', 'rel': 'next'}] + self.assertEqual(_expected, self.management._list("url", None).links) + + def test_show(self): + def side_effect_func(path, instance): + return path, instance + self.management._get = Mock(side_effect=side_effect_func) + p, i = self.management.show(1) + self.assertEqual(('/mgmt/instances/instance1', 'instance'), (p, i)) + + def test_index(self): + def side_effect_func(url, name, limit, marker): + return url + + self.management._list = Mock(side_effect=side_effect_func) + self.assertEqual('/mgmt/instances?deleted=true', + self.management.index(deleted=True)) + self.assertEqual('/mgmt/instances?deleted=false', + self.management.index(deleted=False)) + + def test_root_enabled_history(self): + self.management.api.client.get = Mock(return_value=('resp', None)) + self.assertRaises(Exception, + self.management.root_enabled_history, "instance") + body = {'root_history': 'rh'} + self.management.api.client.get = Mock(return_value=('resp', body)) + management.RootHistory.__init__ = Mock(return_value=None) + rh = self.management.root_enabled_history("instance") + self.assertTrue(isinstance(rh, management.RootHistory)) + + def test__action(self): + resp = Mock() + self.management.api.client.post = Mock(return_value=(resp, 'body')) + resp.status = 200 + self.management._action(1, 'body') + self.assertEqual(1, self.management.api.client.post.call_count) + resp.status = 400 + self.assertRaises(Exception, self.management._action, 1, 'body') + self.assertEqual(2, self.management.api.client.post.call_count) + + def _mock_action(self): + self.body_ = "" + + def side_effect_func(instance_id, body): + self.body_ = body + self.management._action = Mock(side_effect=side_effect_func) + + def test_stop(self): + self._mock_action() + self.management.stop(1) + self.assertEqual(1, self.management._action.call_count) + self.assertEqual({'stop': {}}, self.body_) + + def test_reboot(self): + self._mock_action() + self.management.reboot(1) + self.assertEqual(1, self.management._action.call_count) + self.assertEqual({'reboot': {}}, self.body_) + + def test_migrate(self): + self._mock_action() + self.management.migrate(1) + self.assertEqual(1, self.management._action.call_count) + self.assertEqual({'migrate': {}}, self.body_) + + def test_migrate_to_host(self): + hostname = 'hostname2' + self._mock_action() + self.management.migrate(1, host=hostname) + self.assertEqual(1, self.management._action.call_count) + self.assertEqual({'migrate': {'host': hostname}}, self.body_) + + def test_update(self): + self._mock_action() + self.management.update(1) + self.assertEqual(1, self.management._action.call_count) + self.assertEqual({'update': {}}, self.body_) + + def test_reset_task_status(self): + self._mock_action() + self.management.reset_task_status(1) + self.assertEqual(1, self.management._action.call_count) + self.assertEqual({'reset-task-status': {}}, self.body_) diff --git a/troveclient/tests/test_secgroups.py b/troveclient/tests/test_secgroups.py new file mode 100644 index 0000000..779d01e --- /dev/null +++ b/troveclient/tests/test_secgroups.py @@ -0,0 +1,102 @@ +from testtools import TestCase +from mock import Mock + +from troveclient import security_groups +from troveclient import base + +""" +Unit tests for security_groups.py +""" + + +class SecGroupTest(TestCase): + + def setUp(self): + super(SecGroupTest, self).setUp() + self.orig__init = security_groups.SecurityGroup.__init__ + security_groups.SecurityGroup.__init__ = Mock(return_value=None) + self.security_group = security_groups.SecurityGroup() + self.security_groups = security_groups.SecurityGroups(1) + + def tearDown(self): + super(SecGroupTest, self).tearDown() + security_groups.SecurityGroup.__init__ = self.orig__init + + def test___repr__(self): + self.security_group.name = "security_group-1" + self.assertEqual('', + self.security_group.__repr__()) + + def test_list(self): + sec_group_list = ['secgroup1', 'secgroup2'] + self.security_groups.list = Mock(return_value=sec_group_list) + self.assertEqual(sec_group_list, self.security_groups.list()) + + def test_get(self): + def side_effect_func(path, inst): + return path, inst + + self.security_groups._get = Mock(side_effect=side_effect_func) + self.security_group.id = 1 + self.assertEqual(('/security-groups/1', 'security_group'), + self.security_groups.get(self.security_group)) + + +class SecGroupRuleTest(TestCase): + + def setUp(self): + super(SecGroupRuleTest, self).setUp() + self.orig__init = security_groups.SecurityGroupRule.__init__ + security_groups.SecurityGroupRule.__init__ = Mock(return_value=None) + security_groups.SecurityGroupRules.__init__ = Mock(return_value=None) + self.security_group_rule = security_groups.SecurityGroupRule() + self.security_group_rules = security_groups.SecurityGroupRules() + + def tearDown(self): + super(SecGroupRuleTest, self).tearDown() + security_groups.SecurityGroupRule.__init__ = self.orig__init + + def test___repr__(self): + self.security_group_rule.group_id = 1 + self.security_group_rule.protocol = "tcp" + self.security_group_rule.from_port = 80 + self.security_group_rule.to_port = 80 + self.security_group_rule.cidr = "0.0.0.0//0" + representation = \ + "" % (1, "tcp", 80, 80, "0.0.0.0//0") + + self.assertEqual(representation, + self.security_group_rule.__repr__()) + + def test_create(self): + def side_effect_func(path, body, inst): + return path, body, inst + + self.security_group_rules._create = Mock(side_effect=side_effect_func) + p, b, i = self.security_group_rules.create(1, "tcp", + 80, 80, "0.0.0.0//0") + self.assertEqual("/security-group-rules", p) + self.assertEqual("security_group_rule", i) + self.assertEqual(1, b["security_group_rule"]["group_id"]) + self.assertEqual("tcp", b["security_group_rule"]["protocol"]) + self.assertEqual(80, b["security_group_rule"]["from_port"]) + self.assertEqual(80, b["security_group_rule"]["to_port"]) + self.assertEqual("0.0.0.0//0", b["security_group_rule"]["cidr"]) + + def test_delete(self): + resp = Mock() + resp.status = 200 + body = None + self.security_group_rules.api = Mock() + self.security_group_rules.api.client = Mock() + self.security_group_rules.api.client.delete = \ + Mock(return_value=(resp, body)) + self.security_group_rules.delete(self.id) + resp.status = 500 + self.assertRaises(Exception, self.security_group_rules.delete, + self.id) diff --git a/troveclient/tests/test_users.py b/troveclient/tests/test_users.py new file mode 100644 index 0000000..0fc32f6 --- /dev/null +++ b/troveclient/tests/test_users.py @@ -0,0 +1,126 @@ +from testtools import TestCase +from mock import Mock + +from troveclient import users +from troveclient import base + +""" +Unit tests for users.py +""" + + +class UserTest(TestCase): + + def setUp(self): + super(UserTest, self).setUp() + self.orig__init = users.User.__init__ + users.User.__init__ = Mock(return_value=None) + self.user = users.User() + + def tearDown(self): + super(UserTest, self).tearDown() + users.User.__init__ = self.orig__init + + def test___repr__(self): + self.user.name = "user-1" + self.assertEqual('', self.user.__repr__()) + + +class UsersTest(TestCase): + + def setUp(self): + super(UsersTest, self).setUp() + self.orig__init = users.Users.__init__ + users.Users.__init__ = Mock(return_value=None) + self.users = users.Users() + self.users.api = Mock() + self.users.api.client = Mock() + + self.orig_base_getid = base.getid + base.getid = Mock(return_value="instance1") + + def tearDown(self): + super(UsersTest, self).tearDown() + users.Users.__init__ = self.orig__init + base.getid = self.orig_base_getid + + def _get_mock_method(self): + self._resp = Mock() + self._body = None + self._url = None + + def side_effect_func(url, body=None): + self._body = body + self._url = url + return (self._resp, body) + + return Mock(side_effect=side_effect_func) + + def _build_fake_user(self, name, hostname=None, password=None, + databases=None): + return {'name': name, + 'password': password if password else 'password', + 'host': hostname, + 'databases': databases if databases else [], + } + + def test_create(self): + self.users.api.client.post = self._get_mock_method() + self._resp.status = 200 + user = self._build_fake_user('user1') + + self.users.create(23, [user]) + self.assertEqual('/instances/23/users', self._url) + self.assertEqual({"users": [user]}, self._body) + + # Even if host isn't supplied originally, + # the default is supplied. + del user['host'] + self.users.create(23, [user]) + self.assertEqual('/instances/23/users', self._url) + user['host'] = '%' + self.assertEqual({"users": [user]}, self._body) + + # If host is supplied, of course it's put into the body. + user['host'] = '127.0.0.1' + self.users.create(23, [user]) + self.assertEqual({"users": [user]}, self._body) + + # Make sure that response of 400 is recognized as an error. + user['host'] = '%' + self._resp.status = 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.users.delete(27, 'user1') + self.assertEqual('/instances/27/users/user1', self._url) + self._resp.status = 400 + self.assertRaises(Exception, self.users.delete, 34, 'user1') + + def test__list(self): + def side_effect_func(self, val): + return val + + key = 'key' + body = Mock() + body.get = Mock(return_value=[{'href': 'http://test.net/test_file', + 'rel': 'next'}]) + body.__getitem__ = Mock(return_value=["test-value"]) + + resp = Mock() + resp.status = 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) + + self.users.api.client.get = Mock(return_value=(resp, None)) + self.assertRaises(Exception, self.users._list, 'url', None) + + def test_list(self): + def side_effect_func(path, user, limit, marker): + return path + + self.users._list = Mock(side_effect=side_effect_func) + self.assertEqual('/instances/instance1/users', self.users.list(1)) diff --git a/troveclient/tests/test_utils.py b/troveclient/tests/test_utils.py new file mode 100644 index 0000000..12ee9d0 --- /dev/null +++ b/troveclient/tests/test_utils.py @@ -0,0 +1,41 @@ +import os +from testtools import TestCase +from troveclient import utils +from troveclient import versions + + +class UtilsTest(TestCase): + + def test_add_hookable_mixin(self): + def func(): + pass + + hook_type = "hook_type" + mixin = utils.HookableMixin() + mixin.add_hook(hook_type, func) + self.assertTrue(hook_type in mixin._hooks_map) + self.assertTrue(func in mixin._hooks_map[hook_type]) + + def test_run_hookable_mixin(self): + def func(): + pass + + hook_type = "hook_type" + mixin = utils.HookableMixin() + mixin.add_hook(hook_type, func) + mixin.run_hooks(hook_type) + + def test_environment(self): + self.assertEqual('', utils.env()) + self.assertEqual('passing', utils.env(default='passing')) + + os.environ['test_abc'] = 'passing' + self.assertEqual('passing', utils.env('test_abc')) + self.assertEqual('', utils.env('test_abcd')) + + def test_slugify(self): + import unicodedata + + self.assertEqual('not_unicode', utils.slugify('not_unicode')) + self.assertEqual('unicode', utils.slugify(unicode('unicode'))) + self.assertEqual('slugify-test', utils.slugify('SLUGIFY% test!')) diff --git a/troveclient/tests/test_xml.py b/troveclient/tests/test_xml.py new file mode 100644 index 0000000..96490ce --- /dev/null +++ b/troveclient/tests/test_xml.py @@ -0,0 +1,241 @@ +from testtools import TestCase +from lxml import etree +from troveclient import xml + + +class XmlTest(TestCase): + + ELEMENT = ''' + + + + + + + + + + ''' + ROOT = etree.fromstring(ELEMENT) + + JSON = {'instances': + {'instances': ['1', '2', '3']}, 'dummy': {'dict': True}} + + def test_element_ancestors_match_list(self): + # Test normal operation: + self.assertTrue(xml.element_ancestors_match_list(self.ROOT[0][0], + ['instance', + 'instances'])) + + # Test itr_elem is None: + self.assertTrue(xml.element_ancestors_match_list(self.ROOT, + ['instances'])) + + # Test that the first parent element does not match the first list + # element: + self.assertFalse(xml.element_ancestors_match_list(self.ROOT[0][0], + ['instances', + 'instance'])) + + def test_populate_element_from_dict(self): + # Test populate_element_from_dict with a None in the data + ele = ''' + + + + + + ''' + rt = etree.fromstring(ele) + + self.assertEqual(None, xml.populate_element_from_dict(rt, + {'size': None})) + + def test_element_must_be_list(self): + # Test for when name isn't in the dictionary + self.assertFalse(xml.element_must_be_list(self.ROOT, "not_in_list")) + + # Test when name is in the dictionary but list is empty + self.assertTrue(xml.element_must_be_list(self.ROOT, "accounts")) + + # Test when name is in the dictionary but list is not empty + self.assertTrue(xml.element_must_be_list(self.ROOT[0][0][0], "links")) + + def test_element_to_json(self): + # Test when element must be list: + self.assertEqual([{'flavor': {'links': [], 'value': {'value': '5'}}}], + xml.element_to_json("accounts", self.ROOT)) + + # Test when element must not be list: + exp = {'instance': {'flavor': {'links': [], 'value': {'value': '5'}}}} + self.assertEqual(exp, xml.element_to_json("not_in_list", self.ROOT)) + + def test_root_element_to_json(self): + # Test when element must be list: + exp = ([{'flavor': {'links': [], 'value': {'value': '5'}}}], None) + self.assertEqual(exp, xml.root_element_to_json("accounts", self.ROOT)) + + # Test when element must not be list: + exp = {'instance': {'flavor': {'links': [], 'value': {'value': '5'}}}} + self.assertEqual((exp, None), + xml.root_element_to_json("not_in_list", self.ROOT)) + + # Test rootEnabled True: + t_element = etree.fromstring(''' True ''') + self.assertEqual((True, None), + xml.root_element_to_json("rootEnabled", t_element)) + + # Test rootEnabled False: + f_element = etree.fromstring(''' False ''') + self.assertEqual((False, None), + xml.root_element_to_json("rootEnabled", f_element)) + + def test_element_to_list(self): + # Test w/ no child elements + self.assertEqual([], xml.element_to_list(self.ROOT[0][0][0])) + + # Test w/ no child elements and check_for_links = True + self.assertEqual(([], None), + xml.element_to_list(self.ROOT[0][0][0], + check_for_links=True)) + + # Test w/ child elements + self.assertEqual([{}, {'value': '5'}], + xml.element_to_list(self.ROOT[0][0])) + + # Test w/ child elements and check_for_links = True + self.assertEqual(([{'value': '5'}], []), + xml.element_to_list(self.ROOT[0][0], + check_for_links=True)) + + def test_element_to_dict(self): + # Test when there is not a None + exp = {'instance': {'flavor': {'links': [], 'value': {'value': '5'}}}} + self.assertEqual(exp, xml.element_to_dict(self.ROOT)) + + # Test when there is a None + element = ''' + + None + + ''' + rt = etree.fromstring(element) + self.assertEqual(None, xml.element_to_dict(rt)) + + def test_standarize_json(self): + xml.standardize_json_lists(self.JSON) + self.assertEqual({'instances': ['1', '2', '3'], + 'dummy': {'dict': True}}, self.JSON) + + def test_normalize_tag(self): + ELEMENT_NS = ''' + + + + + + + + + + ''' + ROOT_NS = etree.fromstring(ELEMENT_NS) + + # Test normalizing without namespace info + self.assertEqual('instances', xml.normalize_tag(self.ROOT)) + + # Test normalizing with namespace info + self.assertEqual('instances', xml.normalize_tag(ROOT_NS)) + + def test_create_root_xml_element(self): + # Test creating when name is not in REQUEST_AS_LIST + element = xml.create_root_xml_element("root", {"root": "value"}) + exp = '' + self.assertEqual(exp, etree.tostring(element)) + + # Test creating when name is in REQUEST_AS_LIST + element = xml.create_root_xml_element("users", []) + exp = '' + self.assertEqual(exp, etree.tostring(element)) + + def test_creating_subelements(self): + # Test creating a subelement as a dictionary + element = xml.create_root_xml_element("root", {"root": 5}) + xml.create_subelement(element, "subelement", {"subelement": "value"}) + exp = '' + self.assertEqual(exp, etree.tostring(element)) + + # Test creating a subelement as a list + element = xml.create_root_xml_element("root", + {"root": {"value": "nested"}}) + xml.create_subelement(element, "subelement", [{"subelement": "value"}]) + exp = '' \ + '' + self.assertEqual(exp, etree.tostring(element)) + + # Test creating a subelement as a string (should raise TypeError) + element = xml.create_root_xml_element("root", {"root": "value"}) + try: + xml.create_subelement(element, "subelement", ["value"]) + self.fail("TypeError exception expected") + except TypeError: + pass + + def test_modify_response_types(self): + TYPE_MAP = { + "Int": int, + "Bool": bool + } + #Is a string True + self.assertEqual(True, xml.modify_response_types('True', TYPE_MAP)) + + #Is a string False + self.assertEqual(False, xml.modify_response_types('False', TYPE_MAP)) + + #Is a dict + test_dict = {"Int": "5"} + test_dict = xml.modify_response_types(test_dict, TYPE_MAP) + self.assertEqual(int, test_dict["Int"].__class__) + + #Is a list + test_list = {"a_list": [{"Int": "5"}, {"Str": "A"}]} + test_list = xml.modify_response_types(test_list["a_list"], TYPE_MAP) + self.assertEqual([{'Int': 5}, {'Str': 'A'}], test_list) + + def test_trovexmlclient(self): + from troveclient import exceptions + + client = xml.TroveXmlClient("user", "password", "tenant", + "auth_url", "service_name", + auth_strategy="fake") + request = {'headers': {}} + + # Test morph_request, no body + client.morph_request(request) + self.assertEqual('application/xml', request['headers']['Accept']) + self.assertEqual('application/xml', request['headers']['Content-Type']) + + # Test morph_request, with body + request['body'] = {'root': {'test': 'test'}} + client.morph_request(request) + body = '\n' + exp = {'body': body, + 'headers': {'Content-Type': 'application/xml', + 'Accept': 'application/xml'}} + self.assertEqual(exp, request) + + # Test morph_response_body + request = "" + result = client.morph_response_body(request) + self.assertEqual({'users': [], 'links': [{'href': 'value'}]}, result) + + # Test morph_response_body with improper input + try: + client.morph_response_body("value") + self.fail("ResponseFormatError exception expected") + except exceptions.ResponseFormatError: + pass diff --git a/troveclient/users.py b/troveclient/users.py new file mode 100644 index 0000000..acf99ca --- /dev/null +++ b/troveclient/users.py @@ -0,0 +1,127 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +from troveclient import databases +from troveclient.common import check_for_exceptions +from troveclient.common import limit_url +from troveclient.common import Paginated +from troveclient.common import quote_user_host +import exceptions +import urlparse + + +class User(base.Resource): + """ + A database user + """ + def __repr__(self): + return "" % self.name + + +class Users(base.ManagerWithFind): + """ + Manage :class:`Users` resources. + """ + resource_class = User + + def create(self, instance_id, users): + """ + Create users with permissions to the specified databases + """ + body = {"users": users} + url = "/instances/%s/users" % instance_id + resp, body = self.api.client.post(url, body=body) + check_for_exceptions(resp, body) + + def delete(self, instance_id, username, hostname=None): + """Delete an existing user in the specified instance""" + user = quote_user_host(username, hostname) + url = "/instances/%s/users/%s" % (instance_id, user) + resp, body = self.api.client.delete(url) + check_for_exceptions(resp, body) + + def _list(self, url, response_key, limit=None, marker=None): + resp, body = self.api.client.get(limit_url(url, limit, marker)) + check_for_exceptions(resp, body) + if not body: + raise Exception("Call to " + url + + " did not return a body.") + links = body.get('links', []) + next_links = [link['href'] for link in links if link['rel'] == 'next'] + next_marker = None + for link in next_links: + # Extract the marker from the url. + parsed_url = urlparse.urlparse(link) + query_dict = dict(urlparse.parse_qsl(parsed_url.query)) + next_marker = query_dict.get('marker', None) + users = [self.resource_class(self, res) for res in body[response_key]] + return Paginated(users, next_marker=next_marker, links=links) + + def list(self, instance, limit=None, marker=None): + """ + Get a list of all Users from the instance's Database. + + :rtype: list of :class:`User`. + """ + return self._list("/instances/%s/users" % base.getid(instance), + "users", limit, marker) + + def get(self, instance_id, username, hostname=None): + """ + Get a single User from the instance's Database. + + :rtype: :class:`User`. + """ + user = quote_user_host(username, hostname) + url = "/instances/%s/users/%s" % (instance_id, user) + return self._get(url, "user") + + def list_access(self, instance, username, hostname=None): + """Show all databases the given user has access to. """ + instance_id = base.getid(instance) + user = quote_user_host(username, hostname) + url = "/instances/%(instance_id)s/users/%(user)s/databases" + resp, body = self.api.client.get(url % locals()) + check_for_exceptions(resp, body) + if not body: + raise Exception("Call to %s did not return to a body" % url) + return [databases.Database(self, db) for db in body['databases']] + + def grant(self, instance, username, databases, hostname=None): + """Allow an existing user permissions to access a database.""" + instance_id = base.getid(instance) + user = quote_user_host(username, hostname) + url = "/instances/%(instance_id)s/users/%(user)s/databases" + dbs = {'databases': [{'name': db} for db in databases]} + resp, body = self.api.client.put(url % locals(), body=dbs) + check_for_exceptions(resp, body) + + def revoke(self, instance, username, database, hostname=None): + """Revoke from an existing user access permissions to a database.""" + instance_id = base.getid(instance) + user = quote_user_host(username, hostname) + url = ("/instances/%(instance_id)s/users/%(user)s/" + "databases/%(database)s") + resp, body = self.api.client.delete(url % locals()) + check_for_exceptions(resp, body) + + def change_passwords(self, instance, users): + """Change the password for one or more users.""" + instance_id = base.getid(instance) + user_dict = {"users": users} + url = "/instances/%s/users" % instance_id + resp, body = self.api.client.put(url, body=user_dict) + check_for_exceptions(resp, body) diff --git a/troveclient/utils.py b/troveclient/utils.py new file mode 100644 index 0000000..3deb806 --- /dev/null +++ b/troveclient/utils.py @@ -0,0 +1,68 @@ +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import re +import sys + + +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +def env(*vars, **kwargs): + """ + returns the first environment variable set + if none are non-empty, defaults to '' or keyword arg default + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +_slugify_strip_re = re.compile(r'[^\w\s-]') +_slugify_hyphenate_re = re.compile(r'[-\s]+') + + +# http://code.activestate.com/recipes/ +# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ +def slugify(value): + """ + Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. + + From Django's "django/template/defaultfilters.py". + """ + import unicodedata + if not isinstance(value, unicode): + value = unicode(value) + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicode(_slugify_strip_re.sub('', value).strip().lower()) + return _slugify_hyphenate_re.sub('-', value) diff --git a/troveclient/versions.py b/troveclient/versions.py new file mode 100644 index 0000000..4813a37 --- /dev/null +++ b/troveclient/versions.py @@ -0,0 +1,41 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base + + +class Version(base.Resource): + """ + Version is an opaque instance used to hold version information. + """ + def __repr__(self): + return "" % self.id + + +class Versions(base.ManagerWithFind): + """ + Manage :class:`Versions` information. + """ + + resource_class = Version + + def index(self, url): + """ + Get a list of all versions. + + :rtype: list of :class:`Versions`. + """ + resp, body = self.api.client.request(url, "GET") + return [self.resource_class(self, res) for res in body['versions']] diff --git a/troveclient/xml.py b/troveclient/xml.py new file mode 100644 index 0000000..d4a7451 --- /dev/null +++ b/troveclient/xml.py @@ -0,0 +1,293 @@ +from lxml import etree +import json +from numbers import Number + +from troveclient import exceptions +from troveclient.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 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 + 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 -- cgit v1.2.1