From 424c649ce3c95fa3d421342e2c4e8c0965334126 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Wed, 7 Mar 2012 08:48:47 -0600 Subject: [PATCH 01/42] first commit --- README.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9feeab5 --- /dev/null +++ b/README.rst @@ -0,0 +1,19 @@ +Python bindings to the Reddwarf 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. + +.. contents:: Contents: + :local: + +Command-line API +---------------- + +TODO: Add docs + +Python API +---------- + +TODO: Add docs From 17911cb64f1c23479511e4936c72b4e336873271 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Wed, 7 Mar 2012 08:04:53 -0600 Subject: [PATCH 02/42] Initial copy of reddwarfclient from reddwarf project. --- reddwarfclient/__init__.py | 132 ++++++++++++++++++++++++++++++ reddwarfclient/accounts.py | 55 +++++++++++++ reddwarfclient/base.py | 15 ++++ reddwarfclient/common.py | 113 ++++++++++++++++++++++++++ reddwarfclient/config.py | 73 +++++++++++++++++ reddwarfclient/databases.py | 65 +++++++++++++++ reddwarfclient/diagnostics.py | 37 +++++++++ reddwarfclient/exceptions.py | 50 ++++++++++++ reddwarfclient/hosts.py | 61 ++++++++++++++ reddwarfclient/instances.py | 147 ++++++++++++++++++++++++++++++++++ reddwarfclient/management.py | 94 ++++++++++++++++++++++ reddwarfclient/root.py | 43 ++++++++++ reddwarfclient/storage.py | 37 +++++++++ reddwarfclient/users.py | 65 +++++++++++++++ reddwarfclient/versions.py | 39 +++++++++ 15 files changed, 1026 insertions(+) create mode 100644 reddwarfclient/__init__.py create mode 100644 reddwarfclient/accounts.py create mode 100644 reddwarfclient/base.py create mode 100644 reddwarfclient/common.py create mode 100644 reddwarfclient/config.py create mode 100644 reddwarfclient/databases.py create mode 100644 reddwarfclient/diagnostics.py create mode 100644 reddwarfclient/exceptions.py create mode 100644 reddwarfclient/hosts.py create mode 100644 reddwarfclient/instances.py create mode 100644 reddwarfclient/management.py create mode 100644 reddwarfclient/root.py create mode 100644 reddwarfclient/storage.py create mode 100644 reddwarfclient/users.py create mode 100644 reddwarfclient/versions.py diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py new file mode 100644 index 0000000..59cc039 --- /dev/null +++ b/reddwarfclient/__init__.py @@ -0,0 +1,132 @@ +# 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 time +import urlparse + +try: + import json +except ImportError: + import simplejson as json + + +from novaclient.client import HTTPClient +from novaclient.v1_1.client import Client + + +# To write this test from an end user perspective, we have to create a client +# similar to the CloudServers one. +# For now we will work on it here. + + +class ReddwarfHTTPClient(HTTPClient): + """ + Class for overriding the HTTP authenticate call and making it specific to + reddwarf + """ + + def __init__(self, user, apikey, tenant, auth_url, service_name, + service_url=None, timeout=None): + super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, + auth_url, timeout=timeout) + self.tenant = tenant + self.service = service_name + self.management_url = service_url + + def authenticate(self): + scheme, netloc, path, query, frag = urlparse.urlsplit(self.auth_url) + path_parts = path.split('/') + for part in path_parts: + if len(part) > 0 and part[0] == 'v': + self.version = part + break + + # Auth against Keystone version 2.0 + if self.version == "v2.0": + req_body = {'passwordCredentials': {'username': self.user, + 'password': self.apikey, + 'tenantId': self.tenant}} + self._get_token("/v2.0/tokens", req_body) + # Auth against Keystone version 1.1 + elif self.version == "v1.1": + req_body = {'credentials': {'username': self.user, + 'key': self.apikey}} + self._get_token("/v1.1/auth", req_body) + else: + raise NotImplementedError("Version %s is not supported" + % self.version) + + def _get_token(self, path, req_body): + """Set the management url and auth token""" + token_url = urlparse.urljoin(self.auth_url, path) + resp, body = self.request(token_url, "POST", body=req_body) + try: + if not self.management_url: + self.management_url = body['auth']['serviceCatalog'] \ + [self.service][0]['publicURL'] + self.auth_token = body['auth']['token']['id'] + except KeyError: + raise NotImplementedError("Service: %s is not available" + % self.service) + + +class Dbaas(Client): + """ + 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, apikey, tenant=None, auth_url=None, + service_name='reddwarf', service_url=None): + super(Dbaas, self).__init__(self, username, apikey, tenant, auth_url) + self.client = ReddwarfHTTPClient(username, apikey, tenant, auth_url, + service_name, service_url) + self.versions = Versions(self) + self.databases = Databases(self) + self.instances = Instances(self) + self.users = Users(self) + self.root = Root(self) + self.hosts = Hosts(self) + self.storage = StorageInfo(self) + self.management = Management(self) + self.accounts = Accounts(self) + self.configs = Configs(self) + self.diagnostics = Interrogator(self) + + +from reddwarfclient.accounts import Accounts +from reddwarfclient.config import Configs +from reddwarfclient.databases import Databases +from reddwarfclient.instances import Instances +from reddwarfclient.hosts import Hosts +from reddwarfclient.management import Management +from reddwarfclient.root import Root +from reddwarfclient.storage import StorageInfo +from reddwarfclient.users import Users +from reddwarfclient.versions import Versions +from reddwarfclient.diagnostics import Interrogator diff --git a/reddwarfclient/accounts.py b/reddwarfclient/accounts.py new file mode 100644 index 0000000..6d0e1dd --- /dev/null +++ b/reddwarfclient/accounts.py @@ -0,0 +1,55 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + +class 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 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/base.py b/reddwarfclient/base.py new file mode 100644 index 0000000..eceabef --- /dev/null +++ b/reddwarfclient/base.py @@ -0,0 +1,15 @@ + +def isid(obj): + """ + Returns true if the given object can be converted to an ID, false otherwise. + """ + if hasattr(obj, "id"): + return True + else: + try: + int(obj) + except ValueError: + return False + else: + return True + diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py new file mode 100644 index 0000000..eee5bc2 --- /dev/null +++ b/reddwarfclient/common.py @@ -0,0 +1,113 @@ +# 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 os +import pickle +import sys + + +from reddwarfclient import Dbaas +import exceptions + + +APITOKEN = os.path.expanduser("~/.apitoken") + + +def get_client(): + """Load an existing apitoken if available""" + try: + with open(APITOKEN, 'rb') as token: + apitoken = pickle.load(token) + dbaas = Dbaas(apitoken._user, apitoken._apikey, apitoken._tenant, + apitoken._auth_url, apitoken._service_name, + apitoken._service_url) + dbaas.client.auth_token = apitoken._token + return dbaas + except IOError: + print "ERROR: You need to login first and get an auth token\n" + sys.exit(1) + except: + print "ERROR: There was an error using your existing auth token, " \ + "please login again.\n" + sys.exit(1) + + +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) + + +class APIToken(object): + """A token object containing the user, apikey and token which + is pickleable.""" + + def __init__(self, user, apikey, tenant, token, auth_url, service_name, + service_url): + self._user = user + self._apikey = apikey + self._tenant = tenant + self._token = token + self._auth_url = auth_url + self._service_name = service_name + self._service_url = service_url + + +class Auth(object): + """Authenticate with your username and api key""" + + def __init__(self): + pass + + def login(self, user, apikey, tenant="dbaas", auth_url="http://localhost:5000/v1.1", + service_name="reddwarf", service_url=None): + """Login to retrieve an auth token to use for other api calls""" + try: + dbaas = Dbaas(user, apikey, tenant, auth_url=auth_url, + service_name=service_name, service_url=service_url) + dbaas.authenticate() + apitoken = APIToken(user, apikey, tenant, dbaas.client.auth_token, + auth_url, service_name, service_url) + + with open(APITOKEN, 'wb') as token: + pickle.dump(apitoken, token, protocol=2) + print apitoken._token + except: + print sys.exc_info()[1] diff --git a/reddwarfclient/config.py b/reddwarfclient/config.py new file mode 100644 index 0000000..c0e8b52 --- /dev/null +++ b/reddwarfclient/config.py @@ -0,0 +1,73 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + + +class Config(base.Resource): + """ + A configuration entry + """ + def __repr__(self): + return "" % self.key + + +class Configs(base.ManagerWithFind): + """ + Manage :class:`Configs` resources. + """ + resource_class = Config + + def create(self, configs): + """ + Create the configuration entries + """ + body = {"configs": configs} + url = "/mgmt/configs" + resp, body = self.api.client.post(url, body=body) + + def delete(self, config): + """ + Delete an existing configuration + """ + url = "/mgmt/configs/%s"% config + self._delete(url) + + def list(self): + """ + Get a list of all configuration entries + """ + resp, body = self.api.client.get("/mgmt/configs") + if not body: + raise Exception("Call to /mgmt/configs did not return a body.") + return [self.resource_class(self, res) for res in body['configs']] + + def get(self, config): + """ + Get the specified configuration entry + """ + url = "/mgmt/configs/%s" % config + resp, body = self.api.client.get(url) + if not body: + raise Exception("Call to %s did not return a body." % url) + return self.resource_class(self, body['config']) + + def update(self, config): + """ + Update the configuration entries + """ + body = {"config": config} + url = "/mgmt/configs/%s" % config['key'] + resp, body = self.api.client.put(url, body=body) diff --git a/reddwarfclient/databases.py b/reddwarfclient/databases.py new file mode 100644 index 0000000..a88090a --- /dev/null +++ b/reddwarfclient/databases.py @@ -0,0 +1,65 @@ +from novaclient import base +from reddwarfclient.common import check_for_exceptions +import exceptions + + +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): + 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 [self.resource_class(self, res) for res in body[response_key]] + + def list(self, instance): + """ + Get a list of all Databases from the instance. + + :rtype: list of :class:`Database`. + """ + return self._list("/instances/%s/databases" % base.getid(instance), + "databases") + +# 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 new file mode 100644 index 0000000..9dd3906 --- /dev/null +++ b/reddwarfclient/diagnostics.py @@ -0,0 +1,37 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base +import exceptions + +class Diagnostics(base.Resource): + """ + Account is an opaque instance used to hold account information. + """ + def __repr__(self): + return "" % self.version + +class Interrogator(base.ManagerWithFind): + """ + Manager class for Interrogator resource + """ + resource_class = Diagnostics + url = "/mgmt/instances/%s/diagnostics" + + def get(self, instance_id): + """ + Get the diagnostics of the guest on the instance. + """ + return self._get(self.url % base.getid(instance_id), "diagnostics") diff --git a/reddwarfclient/exceptions.py b/reddwarfclient/exceptions.py new file mode 100644 index 0000000..7a0efba --- /dev/null +++ b/reddwarfclient/exceptions.py @@ -0,0 +1,50 @@ +# 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. + +from novaclient import exceptions + + +class UnprocessableEntity(exceptions.ClientException): + """ + HTTP 422 - Unprocessable Entity: The request cannot be processed. + """ + http_status = 422 + message = "Unprocessable Entity" + + +_code_map = dict((c.http_status, c) for c in [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, exceptions.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: + return cls(code=response.status) diff --git a/reddwarfclient/hosts.py b/reddwarfclient/hosts.py new file mode 100644 index 0000000..5f047c3 --- /dev/null +++ b/reddwarfclient/hosts.py @@ -0,0 +1,61 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + + +class 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 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 new file mode 100644 index 0000000..aa42fdf --- /dev/null +++ b/reddwarfclient/instances.py @@ -0,0 +1,147 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + +import exceptions + +from reddwarfclient.common import check_for_exceptions + + +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, databases=None): + """ + Create (boot) a new instance. + """ + body = {"instance": { + "name": name, + "flavorRef": flavor_id, + "volume": volume + }} + if databases: + body["instance"]["databases"] = databases + + return self._create("/instances", body, "instance") + + 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 instances. + + :rtype: list of :class:`Instance`. + """ + return self._list("/instances/detail", "instances") + + def index(self): + """ + Get a list of all instances. + + :rtype: list of :class:`Instance`. + """ + return self._list("/instances", "instances") + + def details(self): + """ + Get details of all instances. + + :rtype: list of :class:`Instance`. + """ + return self._list("/instances/detail", "instances") + + def get(self, instance): + """ + Get a specific instances. + + :rtype: :class:`Instance` + """ + return self._get("/instances/%s" % base.getid(instance), + "instance") + + 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) + + 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) diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py new file mode 100644 index 0000000..aec1eb9 --- /dev/null +++ b/reddwarfclient/management.py @@ -0,0 +1,94 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + +from reddwarfclient.common import check_for_exceptions +from reddwarfclient.instances import Instance + +class RootHistory(base.Resource): + def __repr__(self): + return ("" + % (self.id, self.root_enabled_at, self.root_enabled_by)) + +class Management(base.ManagerWithFind): + """ + Manage :class:`Instances` resources. + """ + resource_class = Instance + + 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 show(self, instance): + """ + Get details of one instance. + + :rtype: :class:`Instance`. + """ + + return self._list("/mgmt/instances/%s" % base.getid(instance), + 'instance') + + def index(self, deleted=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 + 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, instance) for instance in body['instances']] + + 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_enabled_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 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) diff --git a/reddwarfclient/root.py b/reddwarfclient/root.py new file mode 100644 index 0000000..0aa16f9 --- /dev/null +++ b/reddwarfclient/root.py @@ -0,0 +1,43 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + +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'] \ No newline at end of file diff --git a/reddwarfclient/storage.py b/reddwarfclient/storage.py new file mode 100644 index 0000000..ab16bf6 --- /dev/null +++ b/reddwarfclient/storage.py @@ -0,0 +1,37 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + +class 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 index(self): + """ + Get a list of all storages. + + :rtype: list of :class:`Storages`. + """ + return self._list("/mgmt/storage", "devices") diff --git a/reddwarfclient/users.py b/reddwarfclient/users.py new file mode 100644 index 0000000..2b59b68 --- /dev/null +++ b/reddwarfclient/users.py @@ -0,0 +1,65 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base +from reddwarfclient.common import check_for_exceptions +import exceptions + + +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, user): + """Delete an existing user in the specified instance""" + 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): + 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 [self.resource_class(self, res) for res in body[response_key]] + + def list(self, instance): + """ + 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") \ No newline at end of file diff --git a/reddwarfclient/versions.py b/reddwarfclient/versions.py new file mode 100644 index 0000000..6f09f1b --- /dev/null +++ b/reddwarfclient/versions.py @@ -0,0 +1,39 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import base + +class 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']] From 1faea31c95da39c7df2769815eb8ff37648d371a Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Wed, 7 Mar 2012 15:14:22 -0600 Subject: [PATCH 03/42] Added setup.py. * Added the CLI tool from reddwarf to this repo, made it available as a script. * Added a gitignore. --- .gitignore | 2 + reddwarfclient/cli.py | 313 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 55 ++++++++ 3 files changed, 370 insertions(+) create mode 100644 .gitignore create mode 100644 reddwarfclient/cli.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1f6c47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +python_reddwarfclient.egg* diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py new file mode 100644 index 0000000..dcb9870 --- /dev/null +++ b/reddwarfclient/cli.py @@ -0,0 +1,313 @@ +#!/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 +""" + +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) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + + +from reddwarfclient import common + + +oparser = None + + +def _pretty_print(info): + print json.dumps(info, sort_keys=True, indent=4) + + +class InstanceCommands(object): + """Commands to perform various instances operations and actions""" + + def __init__(self): + pass + + def create(self, name, volume_size, + flavorRef="http://localhost:8775/v1.0/flavors/1"): + """Create a new instance""" + dbaas = common.get_client() + volume = {"size": volume_size} + try: + result = dbaas.instances.create(name, flavorRef, volume) + _pretty_print(result._info) + except: + print sys.exc_info()[1] + + def delete(self, id): + """Delete the specified instance""" + dbaas = common.get_client() + try: + result = dbaas.instances.delete(id) + if result: + print result + except: + print sys.exc_info()[1] + + def get(self, id): + """Get details for the specified instance""" + dbaas = common.get_client() + try: + _pretty_print(dbaas.instances.get(id)._info) + except: + print sys.exc_info()[1] + + def list(self): + """List all instances for account""" + dbaas = common.get_client() + try: + for instance in dbaas.instances.list(): + _pretty_print(instance._info) + except: + print sys.exc_info()[1] + + def resize_volume(self, id, size): + """Resize an instance volume""" + dbaas = common.get_client() + try: + result = dbaas.instances.resize_volume(id, size) + if result: + print result + except: + print sys.exc_info()[1] + + def resize_instance(self, id, flavor_id): + """Resize an instance flavor""" + dbaas = common.get_client() + try: + result = dbaas.instances.resize_instance(id, flavor_id) + if result: + print result + except: + print sys.exc_info()[1] + + def restart(self, id): + """Restart the database""" + dbaas = common.get_client() + try: + result = dbaas.instances.restart(id) + if result: + print result + except: + print sys.exc_info()[1] + + +class FlavorsCommands(object): + """Commands for listing Flavors""" + + def __init__(self): + pass + + def list(self): + """List the available flavors""" + dbaas = common.get_client() + try: + for flavor in dbaas.flavors.list(): + _pretty_print(flavor._info) + except: + print sys.exc_info()[1] + + +class DatabaseCommands(object): + """Database CRUD operations on an instance""" + + def __init__(self): + pass + + def create(self, id, dbname): + """Create a database""" + dbaas = common.get_client() + try: + databases = [{'name': dbname}] + dbaas.databases.create(id, databases) + except: + print sys.exc_info()[1] + + def delete(self, id, dbname): + """Delete a database""" + dbaas = common.get_client() + try: + dbaas.databases.delete(id, dbname) + except: + print sys.exc_info()[1] + + def list(self, id): + """List the databases""" + dbaas = common.get_client() + try: + for database in dbaas.databases.list(id): + _pretty_print(database._info) + except: + print sys.exc_info()[1] + + +class UserCommands(object): + """User CRUD operations on an instance""" + + def __init__(self): + pass + + def create(self, id, username, password, dbname, *args): + """Create a user in instance, with access to one or more databases""" + dbaas = common.get_client() + try: + databases = [{'name': dbname}] + [databases.append({"name": db}) for db in args] + users = [{'name': username, 'password': password, + 'databases': databases}] + dbaas.users.create(id, users) + except: + print sys.exc_info()[1] + + def delete(self, id, user): + """Delete the specified user""" + dbaas = common.get_client() + try: + dbaas.users.delete(id, user) + except: + print sys.exc_info()[1] + + def list(self, id): + """List all the users for an instance""" + dbaas = common.get_client() + try: + for user in dbaas.users.list(id): + _pretty_print(user._info) + except: + print sys.exc_info()[1] + + +class RootCommands(object): + """Root user related operations on an instance""" + + def __init__(self): + pass + + def create(self, id): + """Enable the instance's root user.""" + dbaas = common.get_client() + try: + user, password = dbaas.root.create(id) + print "User:\t\t%s\nPassword:\t%s" % (user, password) + except: + print sys.exc_info()[1] + + def enabled(self, id): + """Check the instance for root access""" + dbaas = common.get_client() + try: + _pretty_print(dbaas.root.is_root_enabled(id)) + except: + print sys.exc_info()[1] + + +class VersionCommands(object): + """List available versions""" + + def __init__(self): + pass + + def list(self, url): + """List all the supported versions""" + dbaas = common.get_client() + try: + versions = dbaas.versions.index(url) + for version in versions: + _pretty_print(version._info) + except: + print sys.exc_info()[1] + + +def config_options(): + global 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 = {'auth': common.Auth, + 'instance': InstanceCommands, + 'flavor': FlavorsCommands, + 'database': DatabaseCommands, + 'user': UserCommands, + 'root': RootCommands, + 'version': VersionCommands} + + +def main(): + # Parse arguments + global oparser + oparser = optparse.OptionParser("%prog [options] ", + version='1.0') + config_options() + (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 = fn() + + # 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: + fn = actions.get(action) + + try: + fn(*args) + sys.exit(0) + except TypeError as err: + print "Possible wrong number of arguments supplied." + print "%s %s: %s" % (cmd, action, fn.__doc__) + print "\t\t", [fn.func_code.co_varnames[i] for i in + range(fn.func_code.co_argcount)] + print "ERROR: %s" % err + except Exception: + print "Command failed, please check the log for more info." + raise + else: + common.print_actions(cmd, actions) + else: + common.print_commands(COMMANDS) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3bd7acc --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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 os +import setuptools +import sys + + +requirements = ["python-novaclient"] + + +def read_file(file_name): + return open(os.path.join(os.path.dirname(__file__), file_name)).read() + + +setuptools.setup( + name="python-reddwarfclient", + version="2012.3", + author="Rackspace", + description="Rich client bindings for Reddwarf REST API.", + long_description=read_file("README.rst"), + license="Apache License, Version 2.0", + url="https://github.com/openstack/python-reddwarfclient", + packages=["reddwarfclient"], + install_requires=requirements, + tests_require=["nose", "mock"], + test_suite="nose.collector", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python" + ], + entry_points={ + "console_scripts": ["reddwarf-cli = reddwarfclient.cli:main"] + } +) From 3590d985536200a8f2623a3dbb2bb0ea37ec186d Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Fri, 9 Mar 2012 16:54:05 -0600 Subject: [PATCH 04/42] Updated client to work with newer keystone version. --- reddwarfclient/__init__.py | 40 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py index 59cc039..0272c2b 100644 --- a/reddwarfclient/__init__.py +++ b/reddwarfclient/__init__.py @@ -41,6 +41,7 @@ def __init__(self, user, apikey, tenant, auth_url, service_name, service_url=None, timeout=None): super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, auth_url, timeout=timeout) + self.api_key = apikey self.tenant = tenant self.service = service_name self.management_url = service_url @@ -55,14 +56,15 @@ def authenticate(self): # Auth against Keystone version 2.0 if self.version == "v2.0": - req_body = {'passwordCredentials': {'username': self.user, - 'password': self.apikey, - 'tenantId': self.tenant}} + req_body = {'auth': {'passwordCredentials': + {'username': self.user, + 'password': self.api_key}, + 'tenantName': self.tenant }} self._get_token("/v2.0/tokens", req_body) # Auth against Keystone version 1.1 elif self.version == "v1.1": req_body = {'credentials': {'username': self.user, - 'key': self.apikey}} + 'key': self.api_key}} self._get_token("/v1.1/auth", req_body) else: raise NotImplementedError("Version %s is not supported" @@ -72,14 +74,24 @@ def _get_token(self, path, req_body): """Set the management url and auth token""" token_url = urlparse.urljoin(self.auth_url, path) resp, body = self.request(token_url, "POST", body=req_body) - try: + if 'access' in body: if not self.management_url: - self.management_url = body['auth']['serviceCatalog'] \ - [self.service][0]['publicURL'] - self.auth_token = body['auth']['token']['id'] - except KeyError: - raise NotImplementedError("Service: %s is not available" - % self.service) + # Assume the new Keystone lite: + catalog = body['access']['serviceCatalog'] + for service in catalog: + if service['name'] == self.service: + self.management_url = service['adminURL'] + self.auth_token = body['access']['token']['id'] + else: + # Assume pre-Keystone Light: + try: + if not self.management_url: + self.management_url = body['auth']['serviceCatalog'] \ + [self.service][0]['publicURL'] + self.auth_token = body['auth']['token']['id'] + except KeyError: + raise NotImplementedError("Service: %s is not available" + % self.service) class Dbaas(Client): @@ -101,10 +113,10 @@ class Dbaas(Client): &c. """ - def __init__(self, username, apikey, tenant=None, auth_url=None, + def __init__(self, username, api_key, tenant=None, auth_url=None, service_name='reddwarf', service_url=None): - super(Dbaas, self).__init__(self, username, apikey, tenant, auth_url) - self.client = ReddwarfHTTPClient(username, apikey, tenant, auth_url, + super(Dbaas, self).__init__(self, username, api_key, tenant, auth_url) + self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url, service_name, service_url) self.versions = Versions(self) self.databases = Databases(self) From 88704a541599da2106e43d37318ed288b5c1fa3b Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Thu, 15 Mar 2012 11:18:35 -0500 Subject: [PATCH 05/42] Updated python-reddwarfclient to work with the newer version of Keystone. * Changed the authenticate function. --- reddwarfclient/__init__.py | 36 +++++++++--------------------------- reddwarfclient/instances.py | 4 +++- reddwarfclient/management.py | 7 +++++++ 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py index 0272c2b..29d6db0 100644 --- a/reddwarfclient/__init__.py +++ b/reddwarfclient/__init__.py @@ -38,37 +38,16 @@ class ReddwarfHTTPClient(HTTPClient): """ def __init__(self, user, apikey, tenant, auth_url, service_name, - service_url=None, timeout=None): + service_type=None, service_url=None, timeout=None): super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, - auth_url, timeout=timeout) + auth_url, + service_type=service_type, + timeout=timeout) self.api_key = apikey self.tenant = tenant self.service = service_name self.management_url = service_url - def authenticate(self): - scheme, netloc, path, query, frag = urlparse.urlsplit(self.auth_url) - path_parts = path.split('/') - for part in path_parts: - if len(part) > 0 and part[0] == 'v': - self.version = part - break - - # Auth against Keystone version 2.0 - if self.version == "v2.0": - req_body = {'auth': {'passwordCredentials': - {'username': self.user, - 'password': self.api_key}, - 'tenantName': self.tenant }} - self._get_token("/v2.0/tokens", req_body) - # Auth against Keystone version 1.1 - elif self.version == "v1.1": - req_body = {'credentials': {'username': self.user, - 'key': self.api_key}} - self._get_token("/v1.1/auth", req_body) - else: - raise NotImplementedError("Version %s is not supported" - % self.version) def _get_token(self, path, req_body): """Set the management url and auth token""" @@ -114,10 +93,13 @@ class Dbaas(Client): """ def __init__(self, username, api_key, tenant=None, auth_url=None, - service_name='reddwarf', service_url=None): + service_type='reddwarf', service_name='Reddwarf Service', + service_url=None): super(Dbaas, self).__init__(self, username, api_key, tenant, auth_url) self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url, - service_name, service_url) + service_type=service_type, + service_name=service_name, + service_url=service_url) self.versions = Versions(self) self.databases = Databases(self) self.instances = Instances(self) diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index aa42fdf..7336751 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -52,7 +52,7 @@ class Instances(base.ManagerWithFind): """ resource_class = Instance - def create(self, name, flavor_id, volume, databases=None): + def create(self, name, flavor_id, volume, databases=None, users=None): """ Create (boot) a new instance. """ @@ -63,6 +63,8 @@ def create(self, name, flavor_id, volume, databases=None): }} if databases: body["instance"]["databases"] = databases + if users: + body["instance"]["users"] = users return self._create("/instances", body, "instance") diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py index aec1eb9..142285c 100644 --- a/reddwarfclient/management.py +++ b/reddwarfclient/management.py @@ -92,3 +92,10 @@ def reboot(self, instance_id): """ body = {'reboot': {}} self._action(instance_id, body) + + def update(self, instance_id): + """ + Update the guest agent via apt-get. + """ + body = {'update': {}} + self._action(instance_id, body) From 3ec9bc32cf861c19dd276f49ffbe301f7655d43c Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Wed, 21 Mar 2012 17:53:24 -0500 Subject: [PATCH 06/42] Adding Flavors to the client --- reddwarfclient/__init__.py | 2 + reddwarfclient/flavors.py | 79 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 reddwarfclient/flavors.py diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py index 29d6db0..01dee13 100644 --- a/reddwarfclient/__init__.py +++ b/reddwarfclient/__init__.py @@ -102,6 +102,7 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, service_url=service_url) self.versions = Versions(self) self.databases = Databases(self) + self.flavors = Flavors(self) self.instances = Instances(self) self.users = Users(self) self.root = Root(self) @@ -116,6 +117,7 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, from reddwarfclient.accounts import Accounts from reddwarfclient.config import Configs from reddwarfclient.databases import Databases +from reddwarfclient.flavors import Flavors from reddwarfclient.instances import Instances from reddwarfclient.hosts import Hosts from reddwarfclient.management import Management diff --git a/reddwarfclient/flavors.py b/reddwarfclient/flavors.py new file mode 100644 index 0000000..8d183f4 --- /dev/null +++ b/reddwarfclient/flavors.py @@ -0,0 +1,79 @@ +# 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 novaclient 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.detail() + + def index(self): + """ + Get a list of all flavors. + + :rtype: list of :class:`Flavor`. + """ + return self._list("/flavors", "flavors") + + def detail(self): + """ + Get the details of all flavors. + + :rtype: list of :class:`Flavor`. + """ + #return self._get("/flavors/detail", "flavors") + return self._list("/flavors/detail", "flavors") + + def get(self, flavor): + """ + Get a specific flavor. + + :rtype: :class:`Flavor` + """ + return self._get("/flavors/%s" % base.getid(flavor), + "flavor") + From ece8b9e2d11a6c29c115c5e18fcb86de557eafaf Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Thu, 29 Mar 2012 16:43:26 -0500 Subject: [PATCH 07/42] Fixed a 1/N problem with the client --- reddwarfclient/flavors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reddwarfclient/flavors.py b/reddwarfclient/flavors.py index 8d183f4..fcaa646 100644 --- a/reddwarfclient/flavors.py +++ b/reddwarfclient/flavors.py @@ -65,7 +65,6 @@ def detail(self): :rtype: list of :class:`Flavor`. """ - #return self._get("/flavors/detail", "flavors") return self._list("/flavors/detail", "flavors") def get(self, flavor): From b1eeaa4b9550388a3d289cb09a695a67845c1fcd Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Wed, 2 May 2012 07:40:13 -0500 Subject: [PATCH 08/42] Changing Reddwarf client to use its own request function. * Nova client changed in a way that broke our client, so copying the code from there is necessary. * Adding InstanceStatus class with the status strings. * Moved the Dbaas and ReddwarfHTTPClient into their own module. * Changed exceptions module to check Nova's exception map after first looking in Reddwarf's. --- reddwarfclient/__init__.py | 102 +---------------------- reddwarfclient/client.py | 152 +++++++++++++++++++++++++++++++++++ reddwarfclient/common.py | 3 +- reddwarfclient/exceptions.py | 5 +- reddwarfclient/instances.py | 11 +++ 5 files changed, 170 insertions(+), 103 deletions(-) create mode 100644 reddwarfclient/client.py diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py index 01dee13..e981aa5 100644 --- a/reddwarfclient/__init__.py +++ b/reddwarfclient/__init__.py @@ -13,106 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import time -import urlparse - -try: - import json -except ImportError: - import simplejson as json - - -from novaclient.client import HTTPClient -from novaclient.v1_1.client import Client - - -# To write this test from an end user perspective, we have to create a client -# similar to the CloudServers one. -# For now we will work on it here. - - -class ReddwarfHTTPClient(HTTPClient): - """ - Class for overriding the HTTP authenticate call and making it specific to - reddwarf - """ - - def __init__(self, user, apikey, tenant, auth_url, service_name, - service_type=None, service_url=None, timeout=None): - super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, - auth_url, - service_type=service_type, - timeout=timeout) - self.api_key = apikey - self.tenant = tenant - self.service = service_name - self.management_url = service_url - - - def _get_token(self, path, req_body): - """Set the management url and auth token""" - token_url = urlparse.urljoin(self.auth_url, path) - resp, body = self.request(token_url, "POST", body=req_body) - if 'access' in body: - if not self.management_url: - # Assume the new Keystone lite: - catalog = body['access']['serviceCatalog'] - for service in catalog: - if service['name'] == self.service: - self.management_url = service['adminURL'] - self.auth_token = body['access']['token']['id'] - else: - # Assume pre-Keystone Light: - try: - if not self.management_url: - self.management_url = body['auth']['serviceCatalog'] \ - [self.service][0]['publicURL'] - self.auth_token = body['auth']['token']['id'] - except KeyError: - raise NotImplementedError("Service: %s is not available" - % self.service) - - -class Dbaas(Client): - """ - 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='reddwarf', service_name='Reddwarf Service', - service_url=None): - super(Dbaas, self).__init__(self, username, api_key, tenant, auth_url) - self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url, - service_type=service_type, - service_name=service_name, - service_url=service_url) - self.versions = Versions(self) - self.databases = Databases(self) - self.flavors = Flavors(self) - self.instances = Instances(self) - self.users = Users(self) - self.root = Root(self) - self.hosts = Hosts(self) - self.storage = StorageInfo(self) - self.management = Management(self) - self.accounts = Accounts(self) - self.configs = Configs(self) - self.diagnostics = Interrogator(self) - from reddwarfclient.accounts import Accounts from reddwarfclient.config import Configs @@ -126,3 +26,5 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, from reddwarfclient.users import Users from reddwarfclient.versions import Versions from reddwarfclient.diagnostics import Interrogator +from reddwarfclient.client import Dbaas +from reddwarfclient.client import ReddwarfHTTPClient diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py new file mode 100644 index 0000000..09e86f9 --- /dev/null +++ b/reddwarfclient/client.py @@ -0,0 +1,152 @@ +# 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 time +import urlparse + +try: + import json +except ImportError: + import simplejson as json + + +from novaclient.client import HTTPClient +from novaclient.v1_1.client import Client + +from reddwarfclient import exceptions + +class ReddwarfHTTPClient(HTTPClient): + """ + Class for overriding the HTTP authenticate call and making it specific to + reddwarf + """ + + def __init__(self, user, apikey, tenant, auth_url, service_name, + service_type=None, service_url=None, timeout=None): + super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, + auth_url, + service_type=service_type, + timeout=timeout) + self.api_key = apikey + self.tenant = tenant + self.service = service_name + self.management_url = service_url + + + def _get_token(self, path, req_body): + """Set the management url and auth token""" + token_url = urlparse.urljoin(self.auth_url, path) + resp, body = self.request(token_url, "POST", body=req_body) + if 'access' in body: + if not self.management_url: + # Assume the new Keystone lite: + catalog = body['access']['serviceCatalog'] + for service in catalog: + if service['name'] == self.service: + self.management_url = service['adminURL'] + self.auth_token = body['access']['token']['id'] + else: + # Assume pre-Keystone Light: + try: + if not self.management_url: + self.management_url = body['auth']['serviceCatalog'] \ + [self.service][0]['publicURL'] + self.auth_token = body['auth']['token']['id'] + except KeyError: + raise NotImplementedError("Service: %s is not available" + % self.service) + + def request(self, *args, **kwargs): + #TODO(tim.simpson): Copy and pasted from novaclient, since we raise + # extra exception subclasses not raised there. + kwargs.setdefault('headers', kwargs.get('headers', {})) + kwargs['headers']['User-Agent'] = self.USER_AGENT + kwargs['headers']['Accept'] = 'application/json' + if 'body' in kwargs: + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['body'] = json.dumps(kwargs['body']) + + resp, body = super(HTTPClient, self).request(*args, **kwargs) + + self.http_log(args, kwargs, resp, body) + + if body: + try: + body = json.loads(body) + except ValueError: + pass + else: + body = None + + if resp.status in (400, 401, 403, 404, 408, 409, 413, 500, 501): + raise exceptions.from_response(resp, body) + + return resp, body + + +class Dbaas(Client): + """ + 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='reddwarf', service_name='Reddwarf Service', + service_url=None): + from reddwarfclient.versions import Versions + from reddwarfclient.databases import Databases + from reddwarfclient.flavors import Flavors + from reddwarfclient.instances import Instances + from reddwarfclient.users import Users + from reddwarfclient.root import Root + from reddwarfclient.hosts import Hosts + from reddwarfclient.storage import StorageInfo + from reddwarfclient.management import Management + from reddwarfclient.accounts import Accounts + from reddwarfclient.config import Configs + from reddwarfclient.diagnostics import Interrogator + + super(Dbaas, self).__init__(self, username, api_key, tenant, auth_url) + self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url, + service_type=service_type, + service_name=service_name, + service_url=service_url) + self.versions = Versions(self) + self.databases = Databases(self) + self.flavors = Flavors(self) + self.instances = Instances(self) + self.users = Users(self) + self.root = Root(self) + self.hosts = Hosts(self) + self.storage = StorageInfo(self) + self.management = Management(self) + self.accounts = Accounts(self) + self.configs = Configs(self) + self.diagnostics = Interrogator(self) + + diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index eee5bc2..5a3c616 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -16,8 +16,7 @@ import pickle import sys - -from reddwarfclient import Dbaas +from reddwarfclient.client import Dbaas import exceptions diff --git a/reddwarfclient/exceptions.py b/reddwarfclient/exceptions.py index 7a0efba..8f0d8f1 100644 --- a/reddwarfclient/exceptions.py +++ b/reddwarfclient/exceptions.py @@ -37,7 +37,10 @@ def from_response(response, body): if resp.status != 200: raise exception_from_response(resp, body) """ - cls = _code_map.get(response.status, exceptions.ClientException) + cls = _code_map.get(response.status, None) + if not cls: + cls = exceptions._code_map.get(response.status, + exceptions.ClientException) if body: message = "n/a" details = "n/a" diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index 7336751..73645dd 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -147,3 +147,14 @@ def restart(self, instance_id): """ body = {'restart': {}} self._action(instance_id, body) + + +class InstanceStatus(object): + + ACTIVE = "ACTIVE" + BLOCKED = "BLOCKED" + BUILD = "BUILD" + FAILED = "FAILED" + REBOOT = "REBOOT" + RESIZE = "RESIZE" + SHUTDOWN = "SHUTDOWN" From 3e92682e3a8a7a1cdeee838dfa5f82a5539beb88 Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Thu, 19 Apr 2012 17:39:59 -0500 Subject: [PATCH 09/42] Updated the root history property names. --- reddwarfclient/__init__.py | 1 + reddwarfclient/management.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py index e981aa5..2914c33 100644 --- a/reddwarfclient/__init__.py +++ b/reddwarfclient/__init__.py @@ -21,6 +21,7 @@ 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 diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py index 142285c..ca1e24e 100644 --- a/reddwarfclient/management.py +++ b/reddwarfclient/management.py @@ -21,7 +21,7 @@ class RootHistory(base.Resource): def __repr__(self): return ("" - % (self.id, self.root_enabled_at, self.root_enabled_by)) + % (self.id, self.created, self.user)) class Management(base.ManagerWithFind): """ From 01313e56fe446ea3eaa459b0cc9dcf89c043de7d Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Mon, 23 Apr 2012 12:58:21 -0500 Subject: [PATCH 10/42] PEP8 fixes --- reddwarfclient/accounts.py | 7 ++++--- reddwarfclient/base.py | 5 ++--- reddwarfclient/common.py | 3 ++- reddwarfclient/config.py | 2 +- reddwarfclient/databases.py | 3 ++- reddwarfclient/diagnostics.py | 2 ++ reddwarfclient/flavors.py | 2 +- reddwarfclient/instances.py | 3 ++- reddwarfclient/management.py | 21 ++++++++++++--------- reddwarfclient/root.py | 5 +++-- reddwarfclient/storage.py | 2 ++ reddwarfclient/users.py | 4 ++-- reddwarfclient/versions.py | 4 +++- 13 files changed, 38 insertions(+), 25 deletions(-) diff --git a/reddwarfclient/accounts.py b/reddwarfclient/accounts.py index 6d0e1dd..8c5db21 100644 --- a/reddwarfclient/accounts.py +++ b/reddwarfclient/accounts.py @@ -15,6 +15,7 @@ from novaclient import base + class Account(base.Resource): """ Account is an opaque instance used to hold account information. @@ -22,13 +23,14 @@ class Account(base.Resource): 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: @@ -41,7 +43,7 @@ def show(self, account): :rtype: :class:`Account`. """ - + acct_name = self._get_account_name(account) return self._list("/mgmt/accounts/%s" % acct_name, 'account') @@ -52,4 +54,3 @@ def _get_account_name(account): return account.name except AttributeError: return account - diff --git a/reddwarfclient/base.py b/reddwarfclient/base.py index eceabef..db627a1 100644 --- a/reddwarfclient/base.py +++ b/reddwarfclient/base.py @@ -1,7 +1,7 @@ - def isid(obj): """ - Returns true if the given object can be converted to an ID, false otherwise. + Returns true if the given object can be converted to an ID, + false otherwise. """ if hasattr(obj, "id"): return True @@ -12,4 +12,3 @@ def isid(obj): return False else: return True - diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index 5a3c616..48bfdb7 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -95,7 +95,8 @@ class Auth(object): def __init__(self): pass - def login(self, user, apikey, tenant="dbaas", auth_url="http://localhost:5000/v1.1", + def login(self, user, apikey, tenant="dbaas", + auth_url="http://localhost:5000/v1.1", service_name="reddwarf", service_url=None): """Login to retrieve an auth token to use for other api calls""" try: diff --git a/reddwarfclient/config.py b/reddwarfclient/config.py index c0e8b52..e342932 100644 --- a/reddwarfclient/config.py +++ b/reddwarfclient/config.py @@ -42,7 +42,7 @@ def delete(self, config): """ Delete an existing configuration """ - url = "/mgmt/configs/%s"% config + url = "/mgmt/configs/%s" % config self._delete(url) def list(self): diff --git a/reddwarfclient/databases.py b/reddwarfclient/databases.py index a88090a..2c721f3 100644 --- a/reddwarfclient/databases.py +++ b/reddwarfclient/databases.py @@ -5,7 +5,8 @@ class Database(base.Resource): """ - According to Wikipedia, "A database is a system intended to organize, store, and retrieve + According to Wikipedia, "A database is a system intended to organize, + store, and retrieve large amounts of data easily." """ def __repr__(self): diff --git a/reddwarfclient/diagnostics.py b/reddwarfclient/diagnostics.py index 9dd3906..934ebcf 100644 --- a/reddwarfclient/diagnostics.py +++ b/reddwarfclient/diagnostics.py @@ -16,6 +16,7 @@ from novaclient import base import exceptions + class Diagnostics(base.Resource): """ Account is an opaque instance used to hold account information. @@ -23,6 +24,7 @@ class Diagnostics(base.Resource): def __repr__(self): return "" % self.version + class Interrogator(base.ManagerWithFind): """ Manager class for Interrogator resource diff --git a/reddwarfclient/flavors.py b/reddwarfclient/flavors.py index fcaa646..a205fcc 100644 --- a/reddwarfclient/flavors.py +++ b/reddwarfclient/flavors.py @@ -20,6 +20,7 @@ from reddwarfclient.common import check_for_exceptions + class Flavor(base.Resource): """ A Flavor is an Instance type, specifying among other things, RAM size. @@ -75,4 +76,3 @@ def get(self, flavor): """ return self._get("/flavors/%s" % base.getid(flavor), "flavor") - diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index 73645dd..e918d48 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -113,7 +113,8 @@ def delete(self, instance): :param instance_id: The instance id to delete """ - resp, body = self.api.client.delete("/instances/%s" % base.getid(instance)) + resp, body = self.api.client.delete("/instances/%s" % + base.getid(instance)) if resp.status in (422, 500): raise exceptions.from_response(resp, body) diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py index ca1e24e..ca234dc 100644 --- a/reddwarfclient/management.py +++ b/reddwarfclient/management.py @@ -18,10 +18,12 @@ from reddwarfclient.common import check_for_exceptions from reddwarfclient.instances import Instance + class RootHistory(base.Resource): def __repr__(self): - return ("" - % (self.id, self.created, self.user)) + return ("" + % (self.id, self.created, self.user)) + class Management(base.ManagerWithFind): """ @@ -41,7 +43,7 @@ def show(self, instance): :rtype: :class:`Instance`. """ - + return self._list("/mgmt/instances/%s" % base.getid(instance), 'instance') @@ -63,7 +65,8 @@ def index(self, deleted=None): 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, instance) for instance in body['instances']] + return [self.resource_class(self, instance) + for instance in body['instances']] def root_enabled_history(self, instance): """ @@ -94,8 +97,8 @@ def reboot(self, instance_id): self._action(instance_id, body) def update(self, instance_id): - """ - Update the guest agent via apt-get. - """ - body = {'update': {}} - self._action(instance_id, body) + """ + Update the guest agent via apt-get. + """ + body = {'update': {}} + self._action(instance_id, body) diff --git a/reddwarfclient/root.py b/reddwarfclient/root.py index 0aa16f9..a71b200 100644 --- a/reddwarfclient/root.py +++ b/reddwarfclient/root.py @@ -19,6 +19,7 @@ from reddwarfclient.common import check_for_exceptions import exceptions + class Root(base.ManagerWithFind): """ Manager class for Root resource @@ -37,7 +38,7 @@ def create(self, instance_id): def is_root_enabled(self, instance_id): """ Return True if root is enabled for the instance; - False otherwise""" + False otherwise""" resp, body = self.api.client.get(self.url % instance_id) check_for_exceptions(resp, body) - return body['rootEnabled'] \ No newline at end of file + return body['rootEnabled'] diff --git a/reddwarfclient/storage.py b/reddwarfclient/storage.py index ab16bf6..5edeebb 100644 --- a/reddwarfclient/storage.py +++ b/reddwarfclient/storage.py @@ -15,6 +15,7 @@ from novaclient import base + class Device(base.Resource): """ Storage is an opaque instance used to hold storage information. @@ -22,6 +23,7 @@ class Device(base.Resource): def __repr__(self): return "" % self.name + class StorageInfo(base.ManagerWithFind): """ Manage :class:`Storage` resources. diff --git a/reddwarfclient/users.py b/reddwarfclient/users.py index 2b59b68..f3a49a6 100644 --- a/reddwarfclient/users.py +++ b/reddwarfclient/users.py @@ -43,7 +43,7 @@ def create(self, instance_id, users): def delete(self, instance_id, user): """Delete an existing user in the specified instance""" - url = "/instances/%s/users/%s"% (instance_id, user) + url = "/instances/%s/users/%s" % (instance_id, user) resp, body = self.api.client.delete(url) check_for_exceptions(resp, body) @@ -62,4 +62,4 @@ def list(self, instance): :rtype: list of :class:`User`. """ return self._list("/instances/%s/users" % base.getid(instance), - "users") \ No newline at end of file + "users") diff --git a/reddwarfclient/versions.py b/reddwarfclient/versions.py index 6f09f1b..b666e6a 100644 --- a/reddwarfclient/versions.py +++ b/reddwarfclient/versions.py @@ -15,6 +15,7 @@ from novaclient import base + class Version(base.Resource): """ Version is an opaque instance used to hold version information. @@ -22,13 +23,14 @@ class Version(base.Resource): 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. From ecdb185461620aa621f30b4b7564166578e5dcd9 Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Thu, 10 May 2012 11:49:26 -0500 Subject: [PATCH 11/42] root_enabled_history shortened to root_history. --- reddwarfclient/cli.py | 3 +- reddwarfclient/client.py | 15 +++-- reddwarfclient/management.py | 2 +- reddwarfclient/mcli.py | 123 +++++++++++++++++++++++++++++++++++ setup.py | 4 +- 5 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 reddwarfclient/mcli.py diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index dcb9870..c908cb8 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -260,7 +260,8 @@ def config_options(): 'database': DatabaseCommands, 'user': UserCommands, 'root': RootCommands, - 'version': VersionCommands} + 'version': VersionCommands, + } def main(): diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 09e86f9..693e268 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -27,6 +27,7 @@ from reddwarfclient import exceptions + class ReddwarfHTTPClient(HTTPClient): """ Class for overriding the HTTP authenticate call and making it specific to @@ -44,7 +45,6 @@ def __init__(self, user, apikey, tenant, auth_url, service_name, self.service = service_name self.management_url = service_url - def _get_token(self, path, req_body): """Set the management url and auth token""" token_url = urlparse.urljoin(self.auth_url, path) @@ -61,8 +61,15 @@ def _get_token(self, path, req_body): # Assume pre-Keystone Light: try: if not self.management_url: - self.management_url = body['auth']['serviceCatalog'] \ - [self.service][0]['publicURL'] + keys = ['auth', + 'serviceCatalog', + self.service, + 0, + 'publicURL'] + url = body + for key in keys: + url = url[key] + self.management_url = url self.auth_token = body['auth']['token']['id'] except KeyError: raise NotImplementedError("Service: %s is not available" @@ -148,5 +155,3 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, self.accounts = Accounts(self) self.configs = Configs(self) self.diagnostics = Interrogator(self) - - diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py index ca234dc..4f31b4b 100644 --- a/reddwarfclient/management.py +++ b/reddwarfclient/management.py @@ -77,7 +77,7 @@ def root_enabled_history(self, 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_enabled_history']) + return RootHistory(self, body['root_history']) def _action(self, instance_id, body): """ diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py new file mode 100644 index 0000000..fe1ecbd --- /dev/null +++ b/reddwarfclient/mcli.py @@ -0,0 +1,123 @@ +#!/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) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + + +from reddwarfclient import common + + +oparser = None + + +def _pretty_print(info): + print json.dumps(info, sort_keys=True, indent=4) + + +class RootCommands(object): + """List details about the root info for an instance.""" + + def __init__(self): + pass + + def history(self, id): + """List root history for the instance.""" + dbaas = common.get_client() + try: + result = dbaas.management.root_enabled_history(id) + _pretty_print(result._info) + except: + print sys.exc_info()[1] + + +def config_options(): + global 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 = {'root': RootCommands, + } + + +def main(): + # Parse arguments + global oparser + oparser = optparse.OptionParser("%prog [options] ", + version='1.0') + config_options() + (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 = fn() + + # 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: + fn = actions.get(action) + + try: + fn(*args) + sys.exit(0) + except TypeError as err: + print "Possible wrong number of arguments supplied." + print "%s %s: %s" % (cmd, action, fn.__doc__) + print "\t\t", [fn.func_code.co_varnames[i] for i in + range(fn.func_code.co_argcount)] + print "ERROR: %s" % err + except Exception: + print "Command failed, please check the log for more info." + raise + else: + common.print_actions(cmd, actions) + else: + common.print_commands(COMMANDS) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 3bd7acc..ee8d5a5 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,8 @@ def read_file(file_name): "Programming Language :: Python" ], entry_points={ - "console_scripts": ["reddwarf-cli = reddwarfclient.cli:main"] + "console_scripts": ["reddwarf-cli = reddwarfclient.cli:main", + "reddwarf-mgmt-cli = reddwarfclient.mcli:main", + ] } ) From f1bafd3d3c4d8bb96511d52221070c9de8f21a9b Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Tue, 15 May 2012 08:27:26 -0500 Subject: [PATCH 12/42] Added the ability to handle "basic auth" authentication. --- reddwarfclient/client.py | 60 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 693e268..d31f87a 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -25,6 +25,7 @@ from novaclient.client import HTTPClient from novaclient.v1_1.client import Client +from novaclient import exceptions as nova_exceptions from reddwarfclient import exceptions @@ -35,15 +36,60 @@ class ReddwarfHTTPClient(HTTPClient): """ def __init__(self, user, apikey, tenant, auth_url, service_name, - service_type=None, service_url=None, timeout=None): + service_url=None, + auth_strategy=None, **kwargs): super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, auth_url, - service_type=service_type, - timeout=timeout) + **kwargs) self.api_key = apikey self.tenant = tenant self.service = service_name self.management_url = service_url + if auth_strategy == "basic": + self.auth_strategy = self.basic_auth + else: + self.auth_strategy = super(ReddwarfHTTPClient, self).authenticate + + def authenticate(self): + self.auth_strategy() + + def _authenticate_without_tokens(self, url, body): + """Authenticate and extract the service catalog.""" + #TODO(tim.simpson): Copy pasta from Nova client's "_authenticate" but + # does not append "tokens" to the url. + + # Make sure we follow redirects when trying to reach Keystone + tmp_follow_all_redirects = self.follow_all_redirects + self.follow_all_redirects = True + + try: + resp, body = self.request(url, "POST", body=body) + finally: + self.follow_all_redirects = tmp_follow_all_redirects + + return resp, body + + def basic_auth(self): + """Authenticate against a v2.0 auth service.""" + auth_url = self.auth_url + body = {"credentials": {"username": self.user, + "key": self.password}} + resp, resp_body = self._authenticate_without_tokens(auth_url, body) + + try: + self.auth_token = resp_body['auth']['token']['id'] + except KeyError: + raise nova_exceptions.AuthorizationFailure() + catalog = resp_body['auth']['serviceCatalog'] + if 'cloudDatabases' not in catalog: + raise nova_exceptions.EndpointNotFound() + endpoints = catalog['cloudDatabases'] + for endpoint in endpoints: + if self.region_name is None or \ + endpoint['region'] == self.region_name: + self.management_url = endpoint['publicURL'] + return + raise nova_exceptions.EndpointNotFound() def _get_token(self, path, req_body): """Set the management url and auth token""" @@ -124,7 +170,8 @@ class Dbaas(Client): def __init__(self, username, api_key, tenant=None, auth_url=None, service_type='reddwarf', service_name='Reddwarf Service', - service_url=None): + service_url=None, insecure=False, auth_strategy=None, + region_name=None): from reddwarfclient.versions import Versions from reddwarfclient.databases import Databases from reddwarfclient.flavors import Flavors @@ -142,7 +189,10 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url, service_type=service_type, service_name=service_name, - service_url=service_url) + 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) From 68d036debc14d1accf2ce7875b2c1146f0640379 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Thu, 17 May 2012 12:52:20 -0500 Subject: [PATCH 13/42] Fixed error where self was passed unnecessarily to super class __init__, messing up other arguments. --- reddwarfclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index d31f87a..ec21b01 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -185,7 +185,7 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, from reddwarfclient.config import Configs from reddwarfclient.diagnostics import Interrogator - super(Dbaas, self).__init__(self, username, api_key, tenant, auth_url) + super(Dbaas, self).__init__(username, api_key, tenant, auth_url) self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url, service_type=service_type, service_name=service_name, From bbda631e8abc4a7ec3e7c99480676dbe810842b4 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Fri, 18 May 2012 14:58:54 -0500 Subject: [PATCH 14/42] Saving the last response. --- reddwarfclient/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index ec21b01..0d3a510 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -133,6 +133,9 @@ def request(self, *args, **kwargs): resp, body = super(HTTPClient, 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: From 389bdf81aa6d18c015de5495640b33404d8e8a47 Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Tue, 22 May 2012 16:35:47 -0500 Subject: [PATCH 15/42] Adds pagination limit and marker to instances, databases, and users indices. --- reddwarfclient/cli.py | 26 ++++++++++++++++------ reddwarfclient/client.py | 2 +- reddwarfclient/common.py | 43 +++++++++++++++++++++++++++++++++++++ reddwarfclient/databases.py | 23 +++++++++++++++----- reddwarfclient/instances.py | 25 +++++++++++++++------ reddwarfclient/users.py | 22 ++++++++++++++----- 6 files changed, 118 insertions(+), 23 deletions(-) diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index c908cb8..03fa845 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -81,12 +81,18 @@ def get(self, id): except: print sys.exc_info()[1] - def list(self): + def list(self, limit=None, marker=None): """List all instances for account""" dbaas = common.get_client() + if limit: + limit = int(limit, 10) try: - for instance in dbaas.instances.list(): + instances = dbaas.instances.list(limit, marker) + for instance in instances: _pretty_print(instance._info) + if instances.links: + for link in instances.links: + _pretty_print(link) except: print sys.exc_info()[1] @@ -160,12 +166,16 @@ def delete(self, id, dbname): except: print sys.exc_info()[1] - def list(self, id): + def list(self, id, limit=None, marker=None): """List the databases""" dbaas = common.get_client() try: - for database in dbaas.databases.list(id): + databases = dbaas.databases.list(id, limit, marker) + for database in databases: _pretty_print(database._info) + if databases.links: + for link in databases.links: + _pretty_print(link) except: print sys.exc_info()[1] @@ -196,12 +206,16 @@ def delete(self, id, user): except: print sys.exc_info()[1] - def list(self, id): + def list(self, id, limit=None, marker=None): """List all the users for an instance""" dbaas = common.get_client() try: - for user in dbaas.users.list(id): + users = dbaas.users.list(id, limit, marker) + for user in users: _pretty_print(user._info) + if users.next: + for link in users.next: + _pretty_print(link) except: print sys.exc_info()[1] diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 0d3a510..252be0d 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -37,7 +37,7 @@ class ReddwarfHTTPClient(HTTPClient): def __init__(self, user, apikey, tenant, auth_url, service_name, service_url=None, - auth_strategy=None, **kwargs): + auth_strategy=None, **kwargs): super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, auth_url, **kwargs) diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index 48bfdb7..2d8a7fb 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -74,6 +74,18 @@ def print_commands(commands): 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 + + class APIToken(object): """A token object containing the user, apikey and token which is pickleable.""" @@ -111,3 +123,34 @@ def login(self, user, apikey, tenant="dbaas", print apitoken._token except: print sys.exc_info()[1] + + +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 index 2c721f3..48934e1 100644 --- a/reddwarfclient/databases.py +++ b/reddwarfclient/databases.py @@ -1,6 +1,9 @@ from novaclient 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): @@ -34,22 +37,32 @@ def delete(self, instance_id, dbname): resp, body = self.api.client.delete(url) check_for_exceptions(resp, body) - def _list(self, url, response_key): - resp, body = self.api.client.get(url) + 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.") - return [self.resource_class(self, res) for res in body[response_key]] + 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): + 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") + "databases", limit, marker) # def get(self, instance, database): # """ diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index e918d48..866e072 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -16,8 +16,11 @@ from novaclient 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' @@ -68,19 +71,29 @@ def create(self, name, flavor_id, volume, databases=None, users=None): return self._create("/instances", body, "instance") - def _list(self, url, response_key): - resp, body = self.api.client.get(url) + 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.") - return [self.resource_class(self, res) for res in body[response_key]] - - def list(self): + 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/detail", "instances") + return self._list("/instances/detail", "instances", limit, marker) def index(self): """ diff --git a/reddwarfclient/users.py b/reddwarfclient/users.py index f3a49a6..5f21ada 100644 --- a/reddwarfclient/users.py +++ b/reddwarfclient/users.py @@ -15,7 +15,10 @@ from novaclient 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 User(base.Resource): @@ -47,19 +50,28 @@ def delete(self, instance_id, user): resp, body = self.api.client.delete(url) check_for_exceptions(resp, body) - def _list(self, url, response_key): - resp, body = self.api.client.get(url) + 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.") - return [self.resource_class(self, res) for res in body[response_key]] + 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): + 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") + "users", limit, marker) From 0b21c5d6b7df63aa138bea9fe9b3591f546fd4bc Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Mon, 4 Jun 2012 10:39:48 -0500 Subject: [PATCH 16/42] Small bugfix, now iterates over users.links like dbs and instances. --- reddwarfclient/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index 03fa845..941dbed 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -213,8 +213,8 @@ def list(self, id, limit=None, marker=None): users = dbaas.users.list(id, limit, marker) for user in users: _pretty_print(user._info) - if users.next: - for link in users.next: + if users.links: + for link in users.links: _pretty_print(link) except: print sys.exc_info()[1] From b5bc8b7c55c7a108f2f524a93c2b3c0af38984d4 Mon Sep 17 00:00:00 2001 From: Nirmal Ranganathan Date: Wed, 13 Jun 2012 11:47:47 -0500 Subject: [PATCH 17/42] Au revoir instances/detail. Changing the instances list to just use index. --- reddwarfclient/instances.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index 866e072..2682224 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -93,23 +93,7 @@ def list(self, limit=None, marker=None): :rtype: list of :class:`Instance`. """ - return self._list("/instances/detail", "instances", limit, marker) - - def index(self): - """ - Get a list of all instances. - - :rtype: list of :class:`Instance`. - """ - return self._list("/instances", "instances") - - def details(self): - """ - Get details of all instances. - - :rtype: list of :class:`Instance`. - """ - return self._list("/instances/detail", "instances") + return self._list("/instances", "instances", limit, marker) def get(self, instance): """ From 9aaf84cc9910389e666a5577f5f8d155d2c1636b Mon Sep 17 00:00:00 2001 From: Nirmal Ranganathan Date: Wed, 13 Jun 2012 15:32:11 -0500 Subject: [PATCH 18/42] Removing flavor detail/index and just sticking with flavor list which is the default --- reddwarfclient/flavors.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/reddwarfclient/flavors.py b/reddwarfclient/flavors.py index a205fcc..6bd280b 100644 --- a/reddwarfclient/flavors.py +++ b/reddwarfclient/flavors.py @@ -48,26 +48,10 @@ def list(self): """ Get a list of all flavors. - :rtype: list of :class:`Flavor`. - """ - return self.detail() - - def index(self): - """ - Get a list of all flavors. - :rtype: list of :class:`Flavor`. """ return self._list("/flavors", "flavors") - def detail(self): - """ - Get the details of all flavors. - - :rtype: list of :class:`Flavor`. - """ - return self._list("/flavors/detail", "flavors") - def get(self, flavor): """ Get a specific flavor. From 290d515c7672e4e3bab49e4b52a5d2ffaaddb85c Mon Sep 17 00:00:00 2001 From: Nirmal Ranganathan Date: Sun, 17 Jun 2012 22:53:55 -0500 Subject: [PATCH 19/42] Adding rax auth --- reddwarfclient/client.py | 28 ++++++++++++++++++++++++++++ reddwarfclient/common.py | 26 ++++++++++++++++++-------- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 252be0d..b950284 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -47,6 +47,8 @@ def __init__(self, user, apikey, tenant, auth_url, service_name, self.management_url = service_url if auth_strategy == "basic": self.auth_strategy = self.basic_auth + elif auth_strategy == "rax": + self.auth_strategy = self._rax_auth else: self.auth_strategy = super(ReddwarfHTTPClient, self).authenticate @@ -91,6 +93,32 @@ def basic_auth(self): return raise nova_exceptions.EndpointNotFound() + def _rax_auth(self): + """Authenticate against the Rackspace auth service.""" + body = {'auth': { + 'RAX-KSKEY:apiKeyCredentials': { + 'username': self.user, + 'apiKey': self.password, + 'tenantName': self.projectid}}} + + resp, resp_body = self._authenticate_without_tokens(self.auth_url, body) + + try: + self.auth_token = resp_body['access']['token']['id'] + except KeyError: + raise nova_exceptions.AuthorizationFailure() + if not self.management_url: + catalogs = resp_body['access']['serviceCatalog'] + for catalog in catalogs: + if catalog['name'] == "cloudDatabases": + endpoints = catalog['endpoints'] + for endpoint in endpoints: + if self.region_name is None or \ + endpoint['region'] == self.region_name: + self.management_url = endpoint['publicURL'] + return + raise nova_exceptions.EndpointNotFound() + def _get_token(self, path, req_body): """Set the management url and auth token""" token_url = urlparse.urljoin(self.auth_url, path) diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index 2d8a7fb..70b9f90 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -28,9 +28,12 @@ def get_client(): try: with open(APITOKEN, 'rb') as token: apitoken = pickle.load(token) - dbaas = Dbaas(apitoken._user, apitoken._apikey, apitoken._tenant, - apitoken._auth_url, apitoken._service_name, - apitoken._service_url) + dbaas = Dbaas(apitoken._user, apitoken._apikey, + tenant=apitoken._tenant, auth_url=apitoken._auth_url, + auth_strategy=apitoken._auth_strategy, + service_name=apitoken._service_name, + service_url=apitoken._service_url, + insecure=apitoken._insecure) dbaas.client.auth_token = apitoken._token return dbaas except IOError: @@ -90,15 +93,18 @@ class APIToken(object): """A token object containing the user, apikey and token which is pickleable.""" - def __init__(self, user, apikey, tenant, token, auth_url, service_name, - service_url): + def __init__(self, user, apikey, tenant, token, auth_url, auth_strategy, + service_name, service_url, region_name, insecure): self._user = user self._apikey = apikey self._tenant = tenant self._token = token self._auth_url = auth_url + self._auth_strategy = auth_strategy self._service_name = service_name self._service_url = service_url + self._region_name = region_name + self._insecure = insecure class Auth(object): @@ -109,14 +115,18 @@ def __init__(self): def login(self, user, apikey, tenant="dbaas", auth_url="http://localhost:5000/v1.1", - service_name="reddwarf", service_url=None): + auth_strategy="basic", service_name="reddwarf", + region_name="default", service_url=None, insecure=True): """Login to retrieve an auth token to use for other api calls""" try: dbaas = Dbaas(user, apikey, tenant, auth_url=auth_url, - service_name=service_name, service_url=service_url) + auth_strategy=auth_strategy, + service_name=service_name, region_name=None, + service_url=service_url, insecure=insecure) dbaas.authenticate() apitoken = APIToken(user, apikey, tenant, dbaas.client.auth_token, - auth_url, service_name, service_url) + auth_url, auth_strategy, service_name, + service_url, region_name, insecure) with open(APITOKEN, 'wb') as token: pickle.dump(apitoken, token, protocol=2) From 44c952347aa15bbc2722a0bd881d5ad3f34d4221 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Mon, 18 Jun 2012 15:01:24 -0500 Subject: [PATCH 20/42] Changing the default auth_strategy from "basic" to None. --- reddwarfclient/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index 70b9f90..e5261e2 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -115,7 +115,7 @@ def __init__(self): def login(self, user, apikey, tenant="dbaas", auth_url="http://localhost:5000/v1.1", - auth_strategy="basic", service_name="reddwarf", + auth_strategy=None, service_name="reddwarf", region_name="default", service_url=None, insecure=True): """Login to retrieve an auth token to use for other api calls""" try: From 6fd2d54d554596f6a70b37ef76fa0085cb27f91e Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Thu, 21 Jun 2012 15:05:13 -0500 Subject: [PATCH 21/42] Import nova exceptions as our own, so they can be used by importing from our client directly. --- reddwarfclient/exceptions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/reddwarfclient/exceptions.py b/reddwarfclient/exceptions.py index 8f0d8f1..5a9a0c7 100644 --- a/reddwarfclient/exceptions.py +++ b/reddwarfclient/exceptions.py @@ -13,6 +13,20 @@ # under the License. from novaclient import exceptions +from novaclient.exceptions import UnsupportedVersion +from novaclient.exceptions import CommandError +from novaclient.exceptions import AuthorizationFailure +from novaclient.exceptions import NoUniqueMatch +from novaclient.exceptions import NoTokenLookupException +from novaclient.exceptions import EndpointNotFound +from novaclient.exceptions import AmbiguousEndpoints +from novaclient.exceptions import ClientException +from novaclient.exceptions import BadRequest +from novaclient.exceptions import Unauthorized +from novaclient.exceptions import Forbidden +from novaclient.exceptions import NotFound +from novaclient.exceptions import OverLimit +from novaclient.exceptions import HTTPNotImplemented class UnprocessableEntity(exceptions.ClientException): From 5521b2de4fd7e51a07015f8a22a65f374665fd37 Mon Sep 17 00:00:00 2001 From: Nirmal Ranganathan Date: Thu, 7 Jun 2012 22:27:24 -0500 Subject: [PATCH 22/42] Adding back accounts to the mgmt cli --- reddwarfclient/mcli.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index fe1ecbd..92b6d35 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -62,6 +62,21 @@ def history(self, id): print sys.exc_info()[1] +class AccountCommands(object): + """Commands to list account info""" + + def __init__(self): + pass + + def get(self, acct): + """List details for the account provided""" + dbaas = common.get_client() + try: + _pretty_print(dbaas.accounts.show(acct)._info) + except: + print sys.exc_info()[1] + + def config_options(): global oparser oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", @@ -70,6 +85,7 @@ def config_options(): COMMANDS = {'root': RootCommands, + 'account': AccountCommands, } From 79ecd5c077c858dc6d5eb8e2b6413aaf5e066331 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Fri, 15 Jun 2012 10:13:31 -0500 Subject: [PATCH 23/42] Adding hosts to management CLI. --- reddwarfclient/mcli.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 92b6d35..6e1ebc3 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -46,6 +46,30 @@ def _pretty_print(info): print json.dumps(info, sort_keys=True, indent=4) +class HostCommands(object): + """Commands to list info on hosts""" + + def __init__(self): + pass + + def get(self, name): + """List details for the specified host""" + dbaas = common.get_client() + try: + _pretty_print(dbaas.hosts.get(name)._info) + except: + print sys.exc_info()[1] + + def list(self): + """List all compute hosts""" + dbaas = common.get_client() + try: + for host in dbaas.hosts.index(): + _pretty_print(host._info) + except: + print sys.exc_info()[1] + + class RootCommands(object): """List details about the root info for an instance.""" @@ -84,8 +108,9 @@ def config_options(): Default: http://localhost:5000/v1.1") -COMMANDS = {'root': RootCommands, - 'account': AccountCommands, +COMMANDS = {'account': AccountCommands, + 'host': HostCommands, + 'root': RootCommands, } From 773590df6afa4dea5b85f8fa9138f4d7697f617e Mon Sep 17 00:00:00 2001 From: Sudarshan Acharya Date: Mon, 18 Jun 2012 12:26:34 -0500 Subject: [PATCH 24/42] Mgmt Instances. --- reddwarfclient/diagnostics.py | 6 ++--- reddwarfclient/management.py | 31 ++++++++++++++++---------- reddwarfclient/mcli.py | 41 +++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/reddwarfclient/diagnostics.py b/reddwarfclient/diagnostics.py index 934ebcf..1fae43d 100644 --- a/reddwarfclient/diagnostics.py +++ b/reddwarfclient/diagnostics.py @@ -30,10 +30,10 @@ class Interrogator(base.ManagerWithFind): Manager class for Interrogator resource """ resource_class = Diagnostics - url = "/mgmt/instances/%s/diagnostics" - def get(self, instance_id): + def get(self, instance): """ Get the diagnostics of the guest on the instance. """ - return self._get(self.url % base.getid(instance_id), "diagnostics") + return self._get("/mgmt/instances/%s/diagnostics" % base.getid(instance), + "diagnostics") diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py index 4f31b4b..54a0165 100644 --- a/reddwarfclient/management.py +++ b/reddwarfclient/management.py @@ -14,8 +14,11 @@ # under the License. from novaclient 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 @@ -31,11 +34,21 @@ class Management(base.ManagerWithFind): """ resource_class = Instance - def _list(self, url, response_key): - resp, body = self.api.client.get(url) + 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.") - return self.resource_class(self, body[response_key]) + 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): """ @@ -44,10 +57,10 @@ def show(self, instance): :rtype: :class:`Instance`. """ - return self._list("/mgmt/instances/%s" % base.getid(instance), - 'instance') + return self._get("/mgmt/instances/%s" % base.getid(instance), + 'instance') - def index(self, deleted=None): + def index(self, deleted=None, limit=None, marker=None): """ Show an overview of all local instances. Optionally, filter by deleted status. @@ -62,11 +75,7 @@ def index(self, deleted=None): form = "?deleted=false" url = "/mgmt/instances%s" % form - 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, instance) - for instance in body['instances']] + return self._list(url, "instances", limit, marker) def root_enabled_history(self, instance): """ diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 6e1ebc3..58c4075 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -101,6 +101,46 @@ def get(self, acct): print sys.exc_info()[1] +class InstanceCommands(object): + """List details about an instance.""" + + def __init__(self): + pass + + def get(self, id): + """List details for the instance.""" + dbaas = common.get_client() + try: + result = dbaas.management.show(id) + _pretty_print(result._info) + except: + print sys.exc_info()[1] + + def list(self, deleted=None, limit=None, marker=None): + """List all instances for account""" + dbaas = common.get_client() + if limit: + limit = int(limit, 10) + try: + instances = dbaas.management.index(deleted, limit, marker) + for instance in instances: + _pretty_print(instance._info) + if instances.links: + for link in instances.links: + _pretty_print(link) + except: + print sys.exc_info()[1] + + def diagnostic(self, id): + """List diagnostic details about an instance.""" + dbaas = common.get_client() + try: + result = dbaas.diagnostics.get(id) + _pretty_print(result._info) + except: + print sys.exc_info()[1] + + def config_options(): global oparser oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", @@ -110,6 +150,7 @@ def config_options(): COMMANDS = {'account': AccountCommands, 'host': HostCommands, + 'instance': InstanceCommands, 'root': RootCommands, } From b15346162255c8f74485051a32df9164dcecf252 Mon Sep 17 00:00:00 2001 From: Sudarshan Acharya Date: Thu, 21 Jun 2012 14:06:11 -0500 Subject: [PATCH 25/42] Mgmt Storage device details --- reddwarfclient/mcli.py | 17 +++++++++++++++++ reddwarfclient/storage.py | 6 ++++++ 2 files changed, 23 insertions(+) diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 58c4075..32a8cc7 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -141,6 +141,22 @@ def diagnostic(self, id): print sys.exc_info()[1] +class StorageCommands(object): + """Commands to list devices info""" + + def __init__(self): + pass + + def list(self): + """List details for the storage device""" + dbaas = common.get_client() + try: + for storage in dbaas.storage.index(): + _pretty_print(storage._info) + except: + print sys.exc_info()[1] + + def config_options(): global oparser oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", @@ -152,6 +168,7 @@ def config_options(): 'host': HostCommands, 'instance': InstanceCommands, 'root': RootCommands, + 'storage': StorageCommands, } diff --git a/reddwarfclient/storage.py b/reddwarfclient/storage.py index 5edeebb..9a317f5 100644 --- a/reddwarfclient/storage.py +++ b/reddwarfclient/storage.py @@ -30,6 +30,12 @@ class StorageInfo(base.ManagerWithFind): """ 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. From 199ded7eb54a539b3036d8a5a7056c55b24ff9f0 Mon Sep 17 00:00:00 2001 From: Nirmal Ranganathan Date: Tue, 3 Jul 2012 10:21:43 -0500 Subject: [PATCH 26/42] Moving away from novaclient and adding all the missing pieces into reddwarfclient. - Added parameters for the authentication instead of the arguments. - Cleaned out the HttpClient and Authentication pieces. --- reddwarfclient/__init__.py | 1 - reddwarfclient/accounts.py | 2 +- reddwarfclient/auth.py | 178 ++++++++++++++++++++ reddwarfclient/base.py | 299 ++++++++++++++++++++++++++++++++-- reddwarfclient/cli.py | 41 +++-- reddwarfclient/client.py | 273 ++++++++++++++++--------------- reddwarfclient/common.py | 36 ++-- reddwarfclient/config.py | 73 --------- reddwarfclient/databases.py | 2 +- reddwarfclient/diagnostics.py | 2 +- reddwarfclient/exceptions.py | 143 +++++++++++++--- reddwarfclient/flavors.py | 2 +- reddwarfclient/hosts.py | 2 +- reddwarfclient/instances.py | 2 +- reddwarfclient/management.py | 2 +- reddwarfclient/mcli.py | 2 - reddwarfclient/root.py | 2 +- reddwarfclient/storage.py | 2 +- reddwarfclient/users.py | 2 +- reddwarfclient/utils.py | 68 ++++++++ reddwarfclient/versions.py | 2 +- setup.py | 2 +- 22 files changed, 863 insertions(+), 275 deletions(-) create mode 100644 reddwarfclient/auth.py delete mode 100644 reddwarfclient/config.py create mode 100644 reddwarfclient/utils.py diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py index 2914c33..40bd3f8 100644 --- a/reddwarfclient/__init__.py +++ b/reddwarfclient/__init__.py @@ -15,7 +15,6 @@ from reddwarfclient.accounts import Accounts -from reddwarfclient.config import Configs from reddwarfclient.databases import Databases from reddwarfclient.flavors import Flavors from reddwarfclient.instances import Instances diff --git a/reddwarfclient/accounts.py b/reddwarfclient/accounts.py index 8c5db21..183330e 100644 --- a/reddwarfclient/accounts.py +++ b/reddwarfclient/accounts.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base class Account(base.Resource): diff --git a/reddwarfclient/auth.py b/reddwarfclient/auth.py new file mode 100644 index 0000000..35ccf1d --- /dev/null +++ b/reddwarfclient/auth.py @@ -0,0 +1,178 @@ +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from reddwarfclient import exceptions + + +class Authenticator(object): + """ + Helper class to perform Keystone or other miscellaneous authentication. + """ + + def __init__(self, client, type, url, username, password, tenant, + region=None, service_type=None, service_name=None, + service_url=None): + self.client = client + self.type = type + self.url = url + self.username = username + self.password = password + self.tenant = tenant + self.region = region + self.service_type = service_type + self.service_name = service_name + self.service_url = service_url + + def _authenticate(self, url, body): + """Authenticate and extract the service catalog.""" + # Make sure we follow redirects when trying to reach Keystone + tmp_follow_all_redirects = self.client.follow_all_redirects + self.client.follow_all_redirects = True + + try: + resp, body = self.client._time_request(url, "POST", body=body) + finally: + self.client.follow_all_redirects = tmp_follow_all_redirects + + if resp.status == 200: # content must always present + try: + return ServiceCatalog(body, region=self.region, + service_type=self.service_type, + service_name=self.service_name, + service_url=self.service_url) + except exceptions.AmbiguousEndpoints: + print "Found more than one valid endpoint. Use a more "\ + "restrictive filter" + raise + except KeyError: + raise exceptions.AuthorizationFailure() + except exceptions.EndpointNotFound: + print "Could not find any suitable endpoint. Correct region?" + raise + + elif resp.status == 305: + return resp['location'] + else: + raise exceptions.from_response(resp, body) + + def authenticate(self): + if self.type == "keystone": + return self._v2_auth(self.url) + elif self.type == "rax": + return self._rax_auth(self.url) + + def _v2_auth(self, url): + """Authenticate against a v2.0 auth service.""" + body = {"auth": { + "passwordCredentials": { + "username": self.username, + "password": self.password} + } + } + + if self.tenant: + body['auth']['tenantName'] = self.tenant + + return self._authenticate(url, body) + + def _rax_auth(self, url): + """Authenticate against the Rackspace auth service.""" + body = {'auth': { + 'RAX-KSKEY:apiKeyCredentials': { + 'username': self.username, + 'apiKey': self.password, + 'tenantName': self.tenant} + } + } + + return self._authenticate(self.url, body) + + +class ServiceCatalog(object): + """Helper methods for dealing with a Keystone Service Catalog.""" + + def __init__(self, resource_dict, region=None, service_type=None, + service_name=None, service_url=None): + self.catalog = resource_dict + self.region = region + self.service_type = service_type + self.service_name = service_name + self.service_url = service_url + self.management_url = None + self.public_url = None + self._load() + + def _load(self): + if not self.service_url: + self.public_url = self._url_for(attr='region', + filter_value=self.region, + endpoint_type="publicURL") + self.management_url = self._url_for(attr='region', + filter_value=self.region, + endpoint_type="adminURL") + else: + self.public_url = self.service_url + self.management_url = self.service_url + + def get_token(self): + return self.catalog['access']['token']['id'] + + def get_management_url(self): + return self.management_url + + def get_public_url(self): + return self.public_url + + def _url_for(self, attr=None, filter_value=None, + endpoint_type='publicURL'): + """ + Fetch the public URL from the Reddwarf service for a particular + endpoint attribute. If none given, return the first. + """ + matching_endpoints = [] + if 'endpoints' in self.catalog: + # We have a bastardized service catalog. Treat it special. :/ + for endpoint in self.catalog['endpoints']: + if not filter_value or endpoint[attr] == filter_value: + matching_endpoints.append(endpoint) + if not matching_endpoints: + raise exceptions.EndpointNotFound() + + # We don't always get a service catalog back ... + if not 'serviceCatalog' in self.catalog['access']: + raise exceptions.EndpointNotFound() + + # Full catalog ... + catalog = self.catalog['access']['serviceCatalog'] + + for service in catalog: + if service.get("type") != self.service_type: + continue + + if (self.service_name and self.service_type == 'reddwarf' and + service.get('name') != self.service_name): + continue + + endpoints = service['endpoints'] + for endpoint in endpoints: + if not filter_value or endpoint.get(attr) == filter_value: + endpoint["serviceName"] = service.get("name") + matching_endpoints.append(endpoint) + + if not matching_endpoints: + raise exceptions.EndpointNotFound() + elif len(matching_endpoints) > 1: + raise exceptions.AmbiguousEndpoints(endpoints=matching_endpoints) + else: + return matching_endpoints[0].get(endpoint_type, None) diff --git a/reddwarfclient/base.py b/reddwarfclient/base.py index db627a1..6660ff4 100644 --- a/reddwarfclient/base.py +++ b/reddwarfclient/base.py @@ -1,14 +1,293 @@ -def isid(obj): +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Base utilities to build API operation managers and objects on top of. +""" + +import contextlib +import hashlib +import os +from reddwarfclient import exceptions +from reddwarfclient import utils + + +# Python 2.4 compat +try: + all +except NameError: + def all(iterable): + return True not in (not x for x in iterable) + + +def getid(obj): """ - Returns true if the given object can be converted to an ID, - false otherwise. + Abstracts the common pattern of allowing both an object or an object's ID + as a parameter when dealing with relationships. """ - if hasattr(obj, "id"): - return True - else: + try: + 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: - int(obj) - except ValueError: - return False + 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 True + 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 index 941dbed..6cbc694 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -32,16 +32,11 @@ if os.path.exists(os.path.join(possible_topdir, 'reddwarfclient', '__init__.py')): sys.path.insert(0, possible_topdir) -if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): - sys.path.insert(0, possible_topdir) from reddwarfclient import common -oparser = None - - def _pretty_print(info): print json.dumps(info, sort_keys=True, indent=4) @@ -261,11 +256,29 @@ def list(self, url): print sys.exc_info()[1] -def config_options(): - global oparser - oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1", +def config_options(oparser): + oparser.add_option("--auth_url", default="http://localhost:5000/v2.0", help="Auth API endpoint URL with port and version. \ - Default: http://localhost:5000/v1.1") + Default: http://localhost:5000/v2.0") + oparser.add_option("--username", help="Login username") + oparser.add_option("--apikey", help="Api key") + oparser.add_option("--tenant_id", + help="Tenant Id associated with the account") + oparser.add_option("--auth_type", default="keystone", + help="Auth type to support different auth environments, \ + Supported values are 'keystone', 'rax'.") + oparser.add_option("--service_type", default="reddwarf", + help="Service type is a name associated for the catalog") + oparser.add_option("--service_name", default="Reddwarf", + help="Service name as provided in the service catalog") + oparser.add_option("--service_url", default="", + help="Service endpoint to use if the catalog doesn't \ + have one") + oparser.add_option("--region", default="RegionOne", + help="Region the service is located in") + oparser.add_option("-i", "--insecure", action="store_true", + dest="insecure", default=False, + help="Run in insecure mode for https endpoints.") COMMANDS = {'auth': common.Auth, @@ -280,10 +293,9 @@ def config_options(): def main(): # Parse arguments - global oparser oparser = optparse.OptionParser("%prog [options] ", version='1.0') - config_options() + config_options(oparser) (options, args) = oparser.parse_args() if not args: @@ -307,7 +319,12 @@ def main(): fn = actions.get(action) try: - fn(*args) + # TODO(rnirmal): Fix when we have proper argument parsing for + # the rest of the commands. + if fn.__name__ == "login": + fn(*args, options=options) + else: + fn(*args) sys.exit(0) except TypeError as err: print "Possible wrong number of arguments supplied." diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index b950284..7219722 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -13,6 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +import httplib2 +import logging +import os import time import urlparse @@ -21,137 +24,88 @@ except ImportError: import simplejson as json +# Python 2.5 compat fix +if not hasattr(urlparse, 'parse_qsl'): + import cgi + urlparse.parse_qsl = cgi.parse_qsl -from novaclient.client import HTTPClient -from novaclient.v1_1.client import Client - -from novaclient import exceptions as nova_exceptions +from reddwarfclient import auth from reddwarfclient import exceptions -class ReddwarfHTTPClient(HTTPClient): - """ - Class for overriding the HTTP authenticate call and making it specific to - reddwarf - """ - - def __init__(self, user, apikey, tenant, auth_url, service_name, - service_url=None, - auth_strategy=None, **kwargs): - super(ReddwarfHTTPClient, self).__init__(user, apikey, tenant, - auth_url, - **kwargs) - self.api_key = apikey - self.tenant = tenant - self.service = service_name - self.management_url = service_url - if auth_strategy == "basic": - self.auth_strategy = self.basic_auth - elif auth_strategy == "rax": - self.auth_strategy = self._rax_auth - else: - self.auth_strategy = super(ReddwarfHTTPClient, self).authenticate - - def authenticate(self): - self.auth_strategy() +_logger = logging.getLogger(__name__) +if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']: + ch = logging.StreamHandler() + _logger.setLevel(logging.DEBUG) + _logger.addHandler(ch) - def _authenticate_without_tokens(self, url, body): - """Authenticate and extract the service catalog.""" - #TODO(tim.simpson): Copy pasta from Nova client's "_authenticate" but - # does not append "tokens" to the url. - # Make sure we follow redirects when trying to reach Keystone - tmp_follow_all_redirects = self.follow_all_redirects - self.follow_all_redirects = True +class ReddwarfHTTPClient(httplib2.Http): - try: - resp, body = self.request(url, "POST", body=body) - finally: - self.follow_all_redirects = tmp_follow_all_redirects + USER_AGENT = 'python-reddwarfclient' - return resp, body + 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): - def basic_auth(self): - """Authenticate against a v2.0 auth service.""" - auth_url = self.auth_url - body = {"credentials": {"username": self.user, - "key": self.password}} - resp, resp_body = self._authenticate_without_tokens(auth_url, body) + super(ReddwarfHTTPClient, self).__init__(timeout=timeout) - try: - self.auth_token = resp_body['auth']['token']['id'] - except KeyError: - raise nova_exceptions.AuthorizationFailure() - catalog = resp_body['auth']['serviceCatalog'] - if 'cloudDatabases' not in catalog: - raise nova_exceptions.EndpointNotFound() - endpoints = catalog['cloudDatabases'] - for endpoint in endpoints: - if self.region_name is None or \ - endpoint['region'] == self.region_name: - self.management_url = endpoint['publicURL'] - return - raise nova_exceptions.EndpointNotFound() - - def _rax_auth(self): - """Authenticate against the Rackspace auth service.""" - body = {'auth': { - 'RAX-KSKEY:apiKeyCredentials': { - 'username': self.user, - 'apiKey': self.password, - 'tenantName': self.projectid}}} - - resp, resp_body = self._authenticate_without_tokens(self.auth_url, body) - - try: - self.auth_token = resp_body['access']['token']['id'] - except KeyError: - raise nova_exceptions.AuthorizationFailure() - if not self.management_url: - catalogs = resp_body['access']['serviceCatalog'] - for catalog in catalogs: - if catalog['name'] == "cloudDatabases": - endpoints = catalog['endpoints'] - for endpoint in endpoints: - if self.region_name is None or \ - endpoint['region'] == self.region_name: - self.management_url = endpoint['publicURL'] - return - raise nova_exceptions.EndpointNotFound() - - def _get_token(self, path, req_body): - """Set the management url and auth token""" - token_url = urlparse.urljoin(self.auth_url, path) - resp, body = self.request(token_url, "POST", body=req_body) - if 'access' in body: - if not self.management_url: - # Assume the new Keystone lite: - catalog = body['access']['serviceCatalog'] - for service in catalog: - if service['name'] == self.service: - self.management_url = service['adminURL'] - self.auth_token = body['access']['token']['id'] - else: - # Assume pre-Keystone Light: - try: - if not self.management_url: - keys = ['auth', - 'serviceCatalog', - self.service, - 0, - 'publicURL'] - url = body - for key in keys: - url = url[key] - self.management_url = url - self.auth_token = body['auth']['token']['id'] - except KeyError: - raise NotImplementedError("Service: %s is not available" - % self.service) + self.username = user + self.password = password + self.tenant = tenant + self.auth_url = auth_url.rstrip('/') + self.region_name = region_name + self.endpoint_type = endpoint_type + self.service_url = service_url + self.service_type = service_type + self.service_name = service_name + self.timings = timings + + 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 + self.authenticator = auth.Authenticator(self, auth_strategy, + self.auth_url, self.username, + self.password, self.tenant, + region=region_name, + service_type=service_type, + service_name=service_name, + service_url=service_url) + + def get_timings(self): + return self.times + + def http_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 request(self, *args, **kwargs): - #TODO(tim.simpson): Copy and pasted from novaclient, since we raise - # extra exception subclasses not raised there. kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT kwargs['headers']['Accept'] = 'application/json' @@ -159,11 +113,10 @@ def request(self, *args, **kwargs): kwargs['headers']['Content-Type'] = 'application/json' kwargs['body'] = json.dumps(kwargs['body']) - resp, body = super(HTTPClient, self).request(*args, **kwargs) + resp, body = super(ReddwarfHTTPClient, self).request(*args, **kwargs) # Save this in case anyone wants it. self.last_response = (resp, body) - self.http_log(args, kwargs, resp, body) if body: @@ -179,8 +132,60 @@ def request(self, *args, **kwargs): return resp, body + def _time_request(self, url, method, **kwargs): + start_time = time.time() + resp, body = self.request(url, method, **kwargs) + self.times.append(("%s %s" % (method, url), + start_time, time.time())) + return resp, body + + def _cs_request(self, url, method, **kwargs): + if not self.auth_token or not self.service_url: + self.authenticate() + + # Perform the request once. If we get a 401 back then it + # might be because the auth token expired, so try to + # re-authenticate and try again. If it still fails, bail. + try: + kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token + if self.tenant: + kwargs['headers']['X-Auth-Project-Id'] = self.tenant + + resp, body = self._time_request(self.service_url + url, method, + **kwargs) + return resp, body + except exceptions.Unauthorized, ex: + try: + self.authenticate() + resp, body = self._time_request(self.service_url + url, + method, **kwargs) + return resp, body + except exceptions.Unauthorized: + raise ex + + def get(self, url, **kwargs): + return self._cs_request(url, 'GET', **kwargs) + + def post(self, url, **kwargs): + return self._cs_request(url, 'POST', **kwargs) + + def put(self, url, **kwargs): + return self._cs_request(url, 'PUT', **kwargs) + + def delete(self, url, **kwargs): + return self._cs_request(url, 'DELETE', **kwargs) -class Dbaas(Client): + def authenticate(self): + catalog = self.authenticator.authenticate() + self.auth_token = catalog.get_token() + if not self.service_url: + if self.endpoint_type == "publicURL": + self.service_url = catalog.get_public_url() + elif self.endpoint_type == "adminURL": + self.service_url = catalog.get_management_url() + + +class Dbaas(object): """ Top-level object to access the Rackspace Database as a Service API. @@ -200,8 +205,8 @@ class Dbaas(Client): """ def __init__(self, username, api_key, tenant=None, auth_url=None, - service_type='reddwarf', service_name='Reddwarf Service', - service_url=None, insecure=False, auth_strategy=None, + service_type='reddwarf', service_name='Reddwarf', + service_url=None, insecure=False, auth_strategy='keystone', region_name=None): from reddwarfclient.versions import Versions from reddwarfclient.databases import Databases @@ -213,10 +218,8 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, from reddwarfclient.storage import StorageInfo from reddwarfclient.management import Management from reddwarfclient.accounts import Accounts - from reddwarfclient.config import Configs from reddwarfclient.diagnostics import Interrogator - super(Dbaas, self).__init__(username, api_key, tenant, auth_url) self.client = ReddwarfHTTPClient(username, api_key, tenant, auth_url, service_type=service_type, service_name=service_name, @@ -234,5 +237,21 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, self.storage = StorageInfo(self) self.management = Management(self) self.accounts = Accounts(self) - self.configs = Configs(self) self.diagnostics = Interrogator(self) + + def set_management_url(self, url): + self.client.management_url = url + + def get_timings(self): + return self.client.get_timings() + + def authenticate(self): + """ + Authenticate against the server. + + This is called to perform an authentication to retrieve a token. + + Returns on success; raises :exc:`exceptions.Unauthorized` if the + credentials are wrong. + """ + self.client.authenticate() diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index e5261e2..c3449c6 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -17,7 +17,7 @@ import sys from reddwarfclient.client import Dbaas -import exceptions +from reddwarfclient import exceptions APITOKEN = os.path.expanduser("~/.apitoken") @@ -31,9 +31,11 @@ def get_client(): dbaas = Dbaas(apitoken._user, apitoken._apikey, tenant=apitoken._tenant, auth_url=apitoken._auth_url, auth_strategy=apitoken._auth_strategy, + service_type=apitoken._service_type, service_name=apitoken._service_name, service_url=apitoken._service_url, - insecure=apitoken._insecure) + insecure=apitoken._insecure, + region_name=apitoken._region_name) dbaas.client.auth_token = apitoken._token return dbaas except IOError: @@ -94,13 +96,15 @@ class APIToken(object): is pickleable.""" def __init__(self, user, apikey, tenant, token, auth_url, auth_strategy, - service_name, service_url, region_name, insecure): + service_type, service_name, service_url, region_name, + insecure): self._user = user self._apikey = apikey self._tenant = tenant self._token = token self._auth_url = auth_url self._auth_strategy = auth_strategy + self._service_type = service_type self._service_name = service_name self._service_url = service_url self._region_name = region_name @@ -113,20 +117,24 @@ class Auth(object): def __init__(self): pass - def login(self, user, apikey, tenant="dbaas", - auth_url="http://localhost:5000/v1.1", - auth_strategy=None, service_name="reddwarf", - region_name="default", service_url=None, insecure=True): + def login(self, options=None): """Login to retrieve an auth token to use for other api calls""" try: - dbaas = Dbaas(user, apikey, tenant, auth_url=auth_url, - auth_strategy=auth_strategy, - service_name=service_name, region_name=None, - service_url=service_url, insecure=insecure) + dbaas = Dbaas(options.username, options.apikey, options.tenant_id, + auth_url=options.auth_url, + auth_strategy=options.auth_type, + service_type=options.service_type, + service_name=options.service_name, + region_name=options.region, + service_url=options.service_url, + insecure=options.insecure) dbaas.authenticate() - apitoken = APIToken(user, apikey, tenant, dbaas.client.auth_token, - auth_url, auth_strategy, service_name, - service_url, region_name, insecure) + apitoken = APIToken(options.username, options.apikey, + options.tenant_id, dbaas.client.auth_token, + options.auth_url, options.auth_type, + options.service_type, options.service_name, + options.service_url, options.region, + options.insecure) with open(APITOKEN, 'wb') as token: pickle.dump(apitoken, token, protocol=2) diff --git a/reddwarfclient/config.py b/reddwarfclient/config.py deleted file mode 100644 index e342932..0000000 --- a/reddwarfclient/config.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient import base - - -class Config(base.Resource): - """ - A configuration entry - """ - def __repr__(self): - return "" % self.key - - -class Configs(base.ManagerWithFind): - """ - Manage :class:`Configs` resources. - """ - resource_class = Config - - def create(self, configs): - """ - Create the configuration entries - """ - body = {"configs": configs} - url = "/mgmt/configs" - resp, body = self.api.client.post(url, body=body) - - def delete(self, config): - """ - Delete an existing configuration - """ - url = "/mgmt/configs/%s" % config - self._delete(url) - - def list(self): - """ - Get a list of all configuration entries - """ - resp, body = self.api.client.get("/mgmt/configs") - if not body: - raise Exception("Call to /mgmt/configs did not return a body.") - return [self.resource_class(self, res) for res in body['configs']] - - def get(self, config): - """ - Get the specified configuration entry - """ - url = "/mgmt/configs/%s" % config - resp, body = self.api.client.get(url) - if not body: - raise Exception("Call to %s did not return a body." % url) - return self.resource_class(self, body['config']) - - def update(self, config): - """ - Update the configuration entries - """ - body = {"config": config} - url = "/mgmt/configs/%s" % config['key'] - resp, body = self.api.client.put(url, body=body) diff --git a/reddwarfclient/databases.py b/reddwarfclient/databases.py index 48934e1..d7f31e1 100644 --- a/reddwarfclient/databases.py +++ b/reddwarfclient/databases.py @@ -1,4 +1,4 @@ -from novaclient import base +from reddwarfclient import base from reddwarfclient.common import check_for_exceptions from reddwarfclient.common import limit_url from reddwarfclient.common import Paginated diff --git a/reddwarfclient/diagnostics.py b/reddwarfclient/diagnostics.py index 1fae43d..3a81ab8 100644 --- a/reddwarfclient/diagnostics.py +++ b/reddwarfclient/diagnostics.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base import exceptions diff --git a/reddwarfclient/exceptions.py b/reddwarfclient/exceptions.py index 5a9a0c7..33c25e8 100644 --- a/reddwarfclient/exceptions.py +++ b/reddwarfclient/exceptions.py @@ -12,24 +12,113 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import exceptions -from novaclient.exceptions import UnsupportedVersion -from novaclient.exceptions import CommandError -from novaclient.exceptions import AuthorizationFailure -from novaclient.exceptions import NoUniqueMatch -from novaclient.exceptions import NoTokenLookupException -from novaclient.exceptions import EndpointNotFound -from novaclient.exceptions import AmbiguousEndpoints -from novaclient.exceptions import ClientException -from novaclient.exceptions import BadRequest -from novaclient.exceptions import Unauthorized -from novaclient.exceptions import Forbidden -from novaclient.exceptions import NotFound -from novaclient.exceptions import OverLimit -from novaclient.exceptions import HTTPNotImplemented - - -class UnprocessableEntity(exceptions.ClientException): +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 AmbiguousEndpoints(Exception): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + self.endpoints = endpoints + + def __str__(self): + return "AmbiguousEndpoints: %s" % repr(self.endpoints) + + +class ClientException(Exception): + """ + The base exception class for all exceptions this library raises. + """ + def __init__(self, code, message=None, details=None, request_id=None): + self.code = code + self.message = message or self.__class__.message + self.details = details + self.request_id = request_id + + def __str__(self): + formatted_string = "%s (HTTP %s)" % (self.message, self.code) + if self.request_id: + formatted_string += " (Request-ID: %s)" % self.request_id + + return formatted_string + + +class BadRequest(ClientException): + """ + HTTP 400 - Bad request: you sent some malformed data. + """ + http_status = 400 + message = "Bad request" + + +class Unauthorized(ClientException): + """ + HTTP 401 - Unauthorized: bad credentials. + """ + http_status = 401 + message = "Unauthorized" + + +class Forbidden(ClientException): + """ + HTTP 403 - Forbidden: your credentials don't give you access to this + resource. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(ClientException): + """ + HTTP 404 - Not found + """ + http_status = 404 + message = "Not found" + + +class OverLimit(ClientException): + """ + HTTP 413 - Over limit: you're over the API limits for this time period. + """ + http_status = 413 + message = "Over limit" + + +# NotImplemented is a python keyword. +class HTTPNotImplemented(ClientException): + """ + HTTP 501 - Not Implemented: the server does not support this operation. + """ + http_status = 501 + message = "Not Implemented" + + +class UnprocessableEntity(ClientException): """ HTTP 422 - Unprocessable Entity: The request cannot be processed. """ @@ -37,7 +126,16 @@ class UnprocessableEntity(exceptions.ClientException): message = "Unprocessable Entity" -_code_map = dict((c.http_status, c) for c in [UnprocessableEntity]) +# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() +# so we can do this: +# _code_map = dict((c.http_status, c) +# for c in ClientException.__subclasses__()) +# +# Instead, we have to hardcode it: +_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, + Forbidden, NotFound, OverLimit, + HTTPNotImplemented, + UnprocessableEntity]) def from_response(response, body): @@ -51,10 +149,7 @@ def from_response(response, body): if resp.status != 200: raise exception_from_response(resp, body) """ - cls = _code_map.get(response.status, None) - if not cls: - cls = exceptions._code_map.get(response.status, - exceptions.ClientException) + cls = _code_map.get(response.status, ClientException) if body: message = "n/a" details = "n/a" @@ -64,4 +159,4 @@ def from_response(response, body): details = error.get('details', None) return cls(code=response.status, message=message, details=details) else: - return cls(code=response.status) + return cls(code=response.status, request_id=request_id) diff --git a/reddwarfclient/flavors.py b/reddwarfclient/flavors.py index 6bd280b..ba01a5f 100644 --- a/reddwarfclient/flavors.py +++ b/reddwarfclient/flavors.py @@ -14,7 +14,7 @@ # under the License. -from novaclient import base +from reddwarfclient import base import exceptions diff --git a/reddwarfclient/hosts.py b/reddwarfclient/hosts.py index 5f047c3..96bc621 100644 --- a/reddwarfclient/hosts.py +++ b/reddwarfclient/hosts.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base class Host(base.Resource): diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index 2682224..6004551 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base import exceptions import urlparse diff --git a/reddwarfclient/management.py b/reddwarfclient/management.py index 54a0165..5a695f7 100644 --- a/reddwarfclient/management.py +++ b/reddwarfclient/management.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base import urlparse from reddwarfclient.common import check_for_exceptions diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 32a8cc7..6f4dd48 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -32,8 +32,6 @@ if os.path.exists(os.path.join(possible_topdir, 'reddwarfclient', '__init__.py')): sys.path.insert(0, possible_topdir) -if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): - sys.path.insert(0, possible_topdir) from reddwarfclient import common diff --git a/reddwarfclient/root.py b/reddwarfclient/root.py index a71b200..33b0da7 100644 --- a/reddwarfclient/root.py +++ b/reddwarfclient/root.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base from reddwarfclient import users from reddwarfclient.common import check_for_exceptions diff --git a/reddwarfclient/storage.py b/reddwarfclient/storage.py index 9a317f5..653096e 100644 --- a/reddwarfclient/storage.py +++ b/reddwarfclient/storage.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base class Device(base.Resource): diff --git a/reddwarfclient/users.py b/reddwarfclient/users.py index 5f21ada..4ee6d33 100644 --- a/reddwarfclient/users.py +++ b/reddwarfclient/users.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base from reddwarfclient.common import check_for_exceptions from reddwarfclient.common import limit_url from reddwarfclient.common import Paginated diff --git a/reddwarfclient/utils.py b/reddwarfclient/utils.py new file mode 100644 index 0000000..3deb806 --- /dev/null +++ b/reddwarfclient/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/reddwarfclient/versions.py b/reddwarfclient/versions.py index b666e6a..f7b52c4 100644 --- a/reddwarfclient/versions.py +++ b/reddwarfclient/versions.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base +from reddwarfclient import base class Version(base.Resource): diff --git a/setup.py b/setup.py index ee8d5a5..93389a3 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import sys -requirements = ["python-novaclient"] +requirements = [] def read_file(file_name): From 5fa0c6f799b77d8e4d6b85c72cd32eaa73867c5b Mon Sep 17 00:00:00 2001 From: Paul Marshall Date: Thu, 5 Jul 2012 14:52:44 -0500 Subject: [PATCH 27/42] adding mysql stop and instance reboot commands for management client --- reddwarfclient/mcli.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 6f4dd48..8f40dcf 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -138,6 +138,22 @@ def diagnostic(self, id): except: print sys.exc_info()[1] + def stop(self, id): + """Stop MySQL on the given instance.""" + dbaas = common.get_client() + try: + result = dbaas.management.stop(id) + except: + print sys.exc_info()[1] + + def reboot(self, id): + """Reboot the instance.""" + dbaas = common.get_client() + try: + result = dbaas.management.reboot(id) + except: + print sys.exec_info()[1] + class StorageCommands(object): """Commands to list devices info""" From d202079a8b07b7d6d6ea66792808f4efbed1da6e Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Tue, 10 Jul 2012 11:08:17 -0500 Subject: [PATCH 28/42] Added a "pretty_print" option which makes captured output look even better. * Shows a possibly usable curl statement which includes the request body. * Also prints the request and response body as well formated, easy to read JSON. * Specify RDC_PP while running tests to see the http communication in this format in any logged test failures. --- reddwarfclient/client.py | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 7219722..d860be9 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -34,6 +34,9 @@ _logger = logging.getLogger(__name__) +RDC_PP = os.environ.get("RDC_PP", "False") == "True" + + if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']: ch = logging.StreamHandler() _logger.setLevel(logging.DEBUG) @@ -86,6 +89,12 @@ 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 @@ -105,6 +114,42 @@ def http_log(self, args, kwargs, resp, body): _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 From 20f1611fa7b57cdc503aa96f587ee98b5db58ec6 Mon Sep 17 00:00:00 2001 From: Paul Marshall Date: Wed, 11 Jul 2012 10:24:57 -0500 Subject: [PATCH 29/42] list all accounts with non-deleted instances --- reddwarfclient/accounts.py | 11 +++++++++++ reddwarfclient/mcli.py | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/reddwarfclient/accounts.py b/reddwarfclient/accounts.py index 183330e..43e9135 100644 --- a/reddwarfclient/accounts.py +++ b/reddwarfclient/accounts.py @@ -14,6 +14,7 @@ # under the License. from reddwarfclient import base +from reddwarfclient.common import check_for_exceptions class Account(base.Resource): @@ -37,6 +38,16 @@ def _list(self, url, response_key): 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. diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 8f40dcf..394d143 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -90,6 +90,14 @@ class AccountCommands(object): def __init__(self): pass + def list(self): + """List all accounts with non-deleted instances""" + dbaas = common.get_client() + try: + _pretty_print(dbaas.accounts.index()._info) + except: + print sys.exc_info()[1] + def get(self, acct): """List details for the account provided""" dbaas = common.get_client() From 3d7037e231bbf70999370f42214ba2183d956be0 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Thu, 12 Jul 2012 13:47:59 -0500 Subject: [PATCH 30/42] Changed client to allow custom Authenticator classes. * This allows a user to specify a custom strategy. The motivation is to locally run fake mode without having to fake keystone. * Updated the setup.py with the project's requirements (swiped from Nova Client's). --- reddwarfclient/auth.py | 41 +++++++++++++++++++++++++++++++++++----- reddwarfclient/client.py | 25 +++++++++++++++++------- setup.py | 6 +++++- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/reddwarfclient/auth.py b/reddwarfclient/auth.py index 35ccf1d..57e0bcd 100644 --- a/reddwarfclient/auth.py +++ b/reddwarfclient/auth.py @@ -15,9 +15,26 @@ 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 + 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. + """ def __init__(self, client, type, url, username, password, tenant, @@ -67,10 +84,13 @@ def _authenticate(self, url, body): raise exceptions.from_response(resp, body) def authenticate(self): - if self.type == "keystone": - return self._v2_auth(self.url) - elif self.type == "rax": - return self._rax_auth(self.url) + raise NotImplementedError("Missing authenticate method.") + + +class KeyStoneV2Authenticator(Authenticator): + + def authenticate(self): + return self._v2_auth(self.url) def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" @@ -86,6 +106,12 @@ def _v2_auth(self, url): return self._authenticate(url, body) + +class RaxAuthenticator(Authenticator): + + def authenticate(self): + return self._rax_auth(self.url) + def _rax_auth(self, url): """Authenticate against the Rackspace auth service.""" body = {'auth': { @@ -100,7 +126,12 @@ def _rax_auth(self, url): class ServiceCatalog(object): - """Helper methods for dealing with a Keystone Service Catalog.""" + """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): diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 7219722..14d57f7 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -74,13 +74,16 @@ def __init__(self, user, password, tenant, auth_url, service_name, # httplib2 overrides self.force_exception_to_status_code = True self.disable_ssl_certificate_validation = insecure - self.authenticator = auth.Authenticator(self, auth_strategy, - self.auth_url, self.username, - self.password, self.tenant, - region=region_name, - service_type=service_type, - service_name=service_name, - service_url=service_url) + + 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 @@ -176,6 +179,14 @@ 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() self.auth_token = catalog.get_token() if not self.service_url: diff --git a/setup.py b/setup.py index 93389a3..1b0fff1 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,11 @@ import sys -requirements = [] +requirements = ["httplib2", "prettytable"] +if sys.version_info < (2, 6): + requirements.append("simplejson") +if sys.version_info < (2, 7): + requirements.append("argparse") def read_file(file_name): From 6a02267f9e22464bb342313e1babfb3620bc0472 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Mon, 16 Jul 2012 15:16:27 -0500 Subject: [PATCH 31/42] Adding tox to Python-RDC to make documenting the client very easy. * Added some Sphinx docs. --- .gitignore | 3 + README.rst | 14 +-- docs/conf.py | 28 ++++++ docs/source/conf.py | 28 ++++++ docs/source/index.rst | 31 ++++++ docs/source/pydocs.rst | 16 ++++ docs/source/usage.rst | 209 +++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- tox.ini | 13 +++ 9 files changed, 337 insertions(+), 7 deletions(-) create mode 100644 docs/conf.py create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/pydocs.rst create mode 100644 docs/source/usage.rst create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index e1f6c47..463eb71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.pyc +.tox/* +build/* +html/* python_reddwarfclient.egg* diff --git a/README.rst b/README.rst index 9feeab5..929143b 100644 --- a/README.rst +++ b/README.rst @@ -5,15 +5,17 @@ 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. -.. contents:: Contents: - :local: - Command-line API ---------------- -TODO: Add docs +To use the command line API, first log in using your user name, api key, +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 -Python API ----------- +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. TODO: Add docs diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0ad4db2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import sys, os + +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage'] + +templates_path = ['_templates'] + +source_suffix = '.rst' + +master_doc = 'index' + +project = u'python-reddwarfclient' +copyright = u'2012, OpenStack' + +version = '1.0' +release = '1.0' +exclude_trees = [] + +pygments_style = 'sphinx' + +html_theme = 'default' +html_static_path = ['_static'] +htmlhelp_basename = 'python-reddwarfclientdoc' +latex_documents = [ + ('index', 'python-reddwarfclient.tex', u'python-reddwarfclient Documentation', + u'OpenStack', 'manual'), +] + diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..0ad4db2 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import sys, os + +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage'] + +templates_path = ['_templates'] + +source_suffix = '.rst' + +master_doc = 'index' + +project = u'python-reddwarfclient' +copyright = u'2012, OpenStack' + +version = '1.0' +release = '1.0' +exclude_trees = [] + +pygments_style = 'sphinx' + +html_theme = 'default' +html_static_path = ['_static'] +htmlhelp_basename = 'python-reddwarfclientdoc' +latex_documents = [ + ('index', 'python-reddwarfclient.tex', u'python-reddwarfclient Documentation', + u'OpenStack', 'manual'), +] + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..454f8ab --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,31 @@ +.. + 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. + +.. include:: ../../README.rst + :start-line: 0 + :end-line: 22 + + +.. contents:: Contents + :local: + +.. include:: ./usage.rst + +.. include:: ./pydocs.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/pydocs.rst b/docs/source/pydocs.rst new file mode 100644 index 0000000..eddda86 --- /dev/null +++ b/docs/source/pydocs.rst @@ -0,0 +1,16 @@ +PyDocs +================= + +reddwarfclient +-------------- + + +.. automodule:: reddwarfclient + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: reddwarfclient.client.Dbaas + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 0000000..caaa12a --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,209 @@ +Using the Client Programmatically +================================= + + +.. testsetup:: + + # Creates some vars we don't show in the docs. + AUTH_URL="http://localhost:8779/v1.0/auth" + + from reddwarfclient import Dbaas + from reddwarfclient import auth + class FakeAuth(auth.Authenticator): + + 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) + + from reddwarfclient import Dbaas + OLD_INIT = Dbaas.__init__ + def new_init(*args, **kwargs): + kwargs['auth_strategy'] = FakeAuth + OLD_INIT(*args, **kwargs) + + # Monkey patch init so it'll work with fake auth. + Dbaas.__init__ = new_init + + + client = Dbaas("jsmith", "abcdef", tenant="12345", + auth_url=AUTH_URL) + client.authenticate() + + # Delete all instances. + instances = [1] + while len(instances) > 0: + instances = client.instances.list() + for instance in instances: + try: + instance.delete() + except: + pass + + flavor_id = "1" + for i in range(30): + name = "Instance #%d" % i + client.instances.create(name, flavor_id, None) + + + +Authentication +-------------- + +Authenticating is necessary to use every feature of the client (except to +discover available versions). + +To create the client, create an instance of the Dbaas (Database as a Service) +class. The auth url, auth user, key, and tenant ID must be specified in the +call to the constructor. + +.. testcode:: + + from reddwarfclient import Dbaas + global AUTH_URL + + client = Dbaas("jsmith", "abcdef", tenant="12345", + auth_url=AUTH_URL) + client.authenticate() + +The default authentication strategy assumes a Keystone complaint auth system. +For Rackspace auth, use the keyword argument "auth_strategy='rax'". + + +Versions +-------- + +You can discover the available versions by querying the versions property as +follows: + + +.. testcode:: + + versions = client.versions.index("http://localhost:8779") + + +The "index" method returns a list of Version objects which have the ID as well +as a list of links, each with a URL to use to reach that particular version. + +.. testcode:: + + for version in versions: + print(version.id) + for link in version.links: + if link['rel'] == 'self': + print(" %s" % link['href']) + +.. testoutput:: + + v1.0 + http://localhost:8779/v1.0 + + +Instances +--------- + +The following example creates a 512 MB instance with a 1 GB volume: + +.. testcode:: + + client.authenticate() + flavor_id = "1" + volume = {'size':1} + databases = [{"name": "my_db", + "character_set": "latin2", # This and the next field are + "collate": "latin2_general_ci"}] # optional. + users = [{"name": "jsmith", "password": "12345", + "databases": [{"name": "my_db"}] + }] + instance = client.instances.create("My Instance", flavor_id, volume, + databases, users) + +To retrieve the instance, use the "get" method of "instances": + +.. testcode:: + + updated_instance = client.instances.get(instance.id) + print(updated_instance.name) + print(" Status=%s Flavor=%s" % + (updated_instance.status, updated_instance.flavor['id'])) + +.. testoutput:: + + My Instance + Status=BUILD Flavor=1 + +You can delete an instance by calling "delete" on the instance object itself, +or by using the delete method on "instances." + +.. testcode:: + + # Wait for the instance to be ready before we delete it. + import time + from reddwarfclient.exceptions import NotFound + + while instance.status == "BUILD": + instance.get() + time.sleep(1) + print("Ready in an %s state." % instance.status) + instance.delete() + # Delete and wait for the instance to go away. + while True: + try: + instance = client.instances.get(instance.id) + assert instance.status == "SHUTDOWN" + except NotFound: + break + +.. testoutput:: + + Ready in an ACTIVE state. + + +Listing instances and Pagination +-------------------------------- + +To list all instances, use the list method of "instances": + +.. testcode:: + + instances = client.instances.list() + + +Lists paginate after twenty items, meaning you'll only get twenty items back +even if there are more. To see the next set of items, send a marker. The marker +is a key value (in the case of instances, the ID) which is the non-inclusive +starting point for all returned items. + +The lists returned by the client always include a "next" property. This +can be used as the "marker" argument to get the next section of the list +back from the server. If no more items are available, then the next property +is None. + +.. testcode:: + + # There are currently 30 instances. + + instances = client.instances.list() + print(len(instances)) + print(instances.next is None) + + instances2 = client.instances.list(marker=instances.next) + print(len(instances2)) + print(instances2.next is None) + +.. testoutput:: + + 20 + False + 10 + True + diff --git a/setup.py b/setup.py index 1b0fff1..47752c8 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ def read_file(file_name): version="2012.3", author="Rackspace", description="Rich client bindings for Reddwarf REST API.", - long_description=read_file("README.rst"), + long_description="""Rich client bindings for Reddwarf REST API.""", license="Apache License, Version 2.0", url="https://github.com/openstack/python-reddwarfclient", packages=["reddwarfclient"], diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..fa600d3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +# Python Reddwarf Client + +[tox] +envlist = py26, docs + +[testenv:docs] +deps = + coverage + httplib2 + sphinx +commands = + sphinx-build -b doctest {toxinidir}/docs/source {envtmpdir}/html + sphinx-build -b html {toxinidir}/docs/source {envtmpdir}/html From fc5d8bd49056226e763c9fa4e11489ffa968a2d1 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Mon, 23 Jul 2012 17:28:04 -0500 Subject: [PATCH 32/42] Made the client send and receive XML. * Added a FakeAuth class, since it was so useful for hitting fake mode. * Added a morph_request and morph_response function to the ReddwarfHTTPClient class, to make it possible to use different content types (XML). * Added an XML module with a version of the client class that reads XML. * Added a "mgmt" attribute to the Dbaas class, so that management classes can be hit in a way that closer approximates their URLs. * Added a "resize_flavor" method, which is a clone of the "resize_instance" method, since that name makes a lot more sense. --- reddwarfclient/auth.py | 21 ++++ reddwarfclient/client.py | 44 +++++--- reddwarfclient/instances.py | 2 + reddwarfclient/xml.py | 205 ++++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 reddwarfclient/xml.py diff --git a/reddwarfclient/auth.py b/reddwarfclient/auth.py index 57e0bcd..51c901d 100644 --- a/reddwarfclient/auth.py +++ b/reddwarfclient/auth.py @@ -24,6 +24,9 @@ def get_authenticator_cls(cls_or_name): return KeyStoneV2Authenticator elif cls_or_name == "rax": return RaxAuthenticator + elif cls_or_name == "fake": + return FakeAuth + raise ValueError("Could not determine authenticator class from the given " "value %r." % cls_or_name) @@ -125,6 +128,24 @@ def _rax_auth(self, url): 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. diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 5b898b3..440e2b9 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -156,10 +156,7 @@ def pretty_log(self, args, kwargs, resp, body): def request(self, *args, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT - kwargs['headers']['Accept'] = 'application/json' - if 'body' in kwargs: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['body'] = json.dumps(kwargs['body']) + self.morph_request(kwargs) resp, body = super(ReddwarfHTTPClient, self).request(*args, **kwargs) @@ -168,10 +165,7 @@ def request(self, *args, **kwargs): self.http_log(args, kwargs, resp, body) if body: - try: - body = json.loads(body) - except ValueError: - pass + body = self.morph_response_body(body) else: body = None @@ -180,6 +174,15 @@ def request(self, *args, **kwargs): return 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): + return json.loads(body_string) + def _time_request(self, url, method, **kwargs): start_time = time.time() resp, body = self.request(url, method, **kwargs) @@ -263,7 +266,7 @@ class Dbaas(object): def __init__(self, username, api_key, tenant=None, auth_url=None, service_type='reddwarf', service_name='Reddwarf', service_url=None, insecure=False, auth_strategy='keystone', - region_name=None): + region_name=None, client_cls=ReddwarfHTTPClient): from reddwarfclient.versions import Versions from reddwarfclient.databases import Databases from reddwarfclient.flavors import Flavors @@ -276,13 +279,13 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, from reddwarfclient.accounts import Accounts from reddwarfclient.diagnostics import Interrogator - self.client = ReddwarfHTTPClient(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.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) @@ -295,6 +298,15 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, self.accounts = Accounts(self) self.diagnostics = Interrogator(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 diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index 6004551..7a5b520 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -147,6 +147,8 @@ def restart(self, instance_id): self._action(instance_id, body) +Instances.resize_flavor = Instances.resize_instance + class InstanceStatus(object): ACTIVE = "ACTIVE" diff --git a/reddwarfclient/xml.py b/reddwarfclient/xml.py new file mode 100644 index 0000000..ae1c226 --- /dev/null +++ b/reddwarfclient/xml.py @@ -0,0 +1,205 @@ +from lxml import etree +import json +from numbers import Number + +from reddwarfclient.client import ReddwarfHTTPClient + + +XML_NS = { None: "http://docs.openstack.org/database/api/v1.0" } + +# This dictionary contains XML paths of things that should become list items. +LISTIFY = { + "accounts":[[]], + "databases":[[]], + "flavors": [[]], + "instances": [[]], + "links" : [["flavor", "instance", "instances"], + ["instance", "instances"]], + "hosts": [[]], + "devices": [[]], + "users": [[]], + "versions": [[]], +} + +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? :'( + return bool(element.text), None + elif 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) + 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)) + else: + create_subelement(element, key, 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 = {} + root_element = etree.XML(body_string) + 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 + return result From 248f87906c27276b3eedd42b64809bbae2daa8e9 Mon Sep 17 00:00:00 2001 From: Vipul Sabhaya Date: Fri, 27 Jul 2012 11:00:00 -0700 Subject: [PATCH 33/42] Support for root user reset-password action --- reddwarfclient/cli.py | 11 ++++++++++- reddwarfclient/instances.py | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index 6cbc694..481d96b 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -121,7 +121,16 @@ def restart(self, id): except: print sys.exc_info()[1] - + def reset_password(self, id): + """Reset the root user Password""" + dbaas = common.get_client() + try: + result = dbaas.instances.reset_password(id) + if result: + _pretty_print(result) + except: + print sys.exc_info()[1] + class FlavorsCommands(object): """Commands for listing Flavors""" diff --git a/reddwarfclient/instances.py b/reddwarfclient/instances.py index 6004551..a0b12d1 100644 --- a/reddwarfclient/instances.py +++ b/reddwarfclient/instances.py @@ -122,6 +122,7 @@ def _action(self, instance_id, body): url = "/instances/%s/action" % instance_id resp, body = self.api.client.post(url, body=body) check_for_exceptions(resp, body) + return body def resize_volume(self, instance_id, volume_size): """ @@ -146,7 +147,15 @@ def restart(self, instance_id): 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) + class InstanceStatus(object): ACTIVE = "ACTIVE" From 0fad705f91396a7b3b3cd6a8153c3db0cd9a7961 Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Thu, 26 Jul 2012 14:35:40 -0500 Subject: [PATCH 34/42] Built common CommandsBase so we can share double-dashed options from the parser between command classes. --- reddwarfclient/cli.py | 287 +++++++++++++++------------------------ reddwarfclient/common.py | 123 ++++++++++++++--- 2 files changed, 209 insertions(+), 201 deletions(-) diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index 6cbc694..eb3e7f7 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -18,7 +18,6 @@ Reddwarf Command line tool """ -import json import optparse import os import sys @@ -37,223 +36,162 @@ from reddwarfclient import common -def _pretty_print(info): - print json.dumps(info, sort_keys=True, indent=4) - - -class InstanceCommands(object): +class InstanceCommands(common.CommandsBase): """Commands to perform various instances operations and actions""" - def __init__(self): - pass + params = [ + 'flavor', + 'id', + 'limit', + 'marker', + 'name', + 'size', + ] - def create(self, name, volume_size, - flavorRef="http://localhost:8775/v1.0/flavors/1"): + def create(self): """Create a new instance""" - dbaas = common.get_client() - volume = {"size": volume_size} - try: - result = dbaas.instances.create(name, flavorRef, volume) - _pretty_print(result._info) - except: - print sys.exc_info()[1] - - def delete(self, id): + self._require('name', 'volume_size') + # flavorRef is not required. + flavorRef = self.flavor or "http://localhost:8775/v1.0/flavors/1" + volume = {"size": self.size} + self._pretty_print(self.dbaas.instances.create, self.name, + flavorRef, volume) + + def delete(self): """Delete the specified instance""" - dbaas = common.get_client() - try: - result = dbaas.instances.delete(id) - if result: - print result - except: - print sys.exc_info()[1] + self._require('id') + print self.dbaas.instances.delete(self.id) - def get(self, id): + def get(self): """Get details for the specified instance""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.instances.get(id)._info) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.instances.get, self.id) - def list(self, limit=None, marker=None): + def list(self): """List all instances for account""" - dbaas = common.get_client() + # limit and marker are not required. + limit = self.limit or None if limit: limit = int(limit, 10) - try: - instances = dbaas.instances.list(limit, marker) - for instance in instances: - _pretty_print(instance._info) - if instances.links: - for link in instances.links: - _pretty_print(link) - except: - print sys.exc_info()[1] + self._pretty_paged(self.dbaas.instances.list) - def resize_volume(self, id, size): + def resize_volume(self): """Resize an instance volume""" - dbaas = common.get_client() - try: - result = dbaas.instances.resize_volume(id, size) - if result: - print result - except: - print sys.exc_info()[1] + self._require('id', 'size') + self._pretty_print(self.dbaas.instances.resize_volume, self.id, + self.size) - def resize_instance(self, id, flavor_id): + def resize_instance(self): """Resize an instance flavor""" - dbaas = common.get_client() - try: - result = dbaas.instances.resize_instance(id, flavor_id) - if result: - print result - except: - print sys.exc_info()[1] + self._require('id', 'flavor') + self._pretty_print(self.dbaas.instances.resize_instance, self.id, + self.flavor_id) - def restart(self, id): + def restart(self): """Restart the database""" - dbaas = common.get_client() - try: - result = dbaas.instances.restart(id) - if result: - print result - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.instances.restart, self.id) -class FlavorsCommands(object): +class FlavorsCommands(common.CommandsBase): """Commands for listing Flavors""" - def __init__(self): - pass + params = [] def list(self): """List the available flavors""" - dbaas = common.get_client() - try: - for flavor in dbaas.flavors.list(): - _pretty_print(flavor._info) - except: - print sys.exc_info()[1] + self._pretty_print(self.dbaas.flavors.list) -class DatabaseCommands(object): +class DatabaseCommands(common.CommandsBase): """Database CRUD operations on an instance""" - def __init__(self): - pass + params = [ + 'name', + 'id', + 'limit', + 'marker', + ] - def create(self, id, dbname): + def create(self): """Create a database""" - dbaas = common.get_client() - try: - databases = [{'name': dbname}] - dbaas.databases.create(id, databases) - except: - print sys.exc_info()[1] + self._require('id', 'name') + databases = [{'name': self.name}] + print self.dbaas.databases.create(self.id, databases) - def delete(self, id, dbname): + def delete(self): """Delete a database""" - dbaas = common.get_client() - try: - dbaas.databases.delete(id, dbname) - except: - print sys.exc_info()[1] + self._require('id', 'name') + print self.dbaas.databases.delete(self.id, self.name) - def list(self, id, limit=None, marker=None): + def list(self): """List the databases""" - dbaas = common.get_client() - try: - databases = dbaas.databases.list(id, limit, marker) - for database in databases: - _pretty_print(database._info) - if databases.links: - for link in databases.links: - _pretty_print(link) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_paged(self.dbaas.databases.list, self.id) -class UserCommands(object): +class UserCommands(common.CommandsBase): """User CRUD operations on an instance""" - - def __init__(self): - pass - - def create(self, id, username, password, dbname, *args): + params = [ + 'id', + 'databases', + 'name', + 'password', + ] + + def create(self): """Create a user in instance, with access to one or more databases""" - dbaas = common.get_client() - try: - databases = [{'name': dbname}] - [databases.append({"name": db}) for db in args] - users = [{'name': username, 'password': password, - 'databases': databases}] - dbaas.users.create(id, users) - except: - print sys.exc_info()[1] - - def delete(self, id, user): + self._require('id', 'name', 'password', 'databases') + self._make_list('databases') + databases = [{'name': dbname} for dbname in self.databases] + users = [{'name': self.username, 'password': self.password, + 'databases': databases}] + self.dbaas.users.create(self.id, users) + + def delete(self): """Delete the specified user""" - dbaas = common.get_client() - try: - dbaas.users.delete(id, user) - except: - print sys.exc_info()[1] + self._require('id', 'name') + self.users.delete(self.id, self.name) - def list(self, id, limit=None, marker=None): + def list(self): """List all the users for an instance""" - dbaas = common.get_client() - try: - users = dbaas.users.list(id, limit, marker) - for user in users: - _pretty_print(user._info) - if users.links: - for link in users.links: - _pretty_print(link) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_paged(self.dbaas.users.list, self.id) -class RootCommands(object): +class RootCommands(common.CommandsBase): """Root user related operations on an instance""" - def __init__(self): - pass + params = [ + 'id', + ] - def create(self, id): + def create(self): """Enable the instance's root user.""" - dbaas = common.get_client() + self._require('id') try: - user, password = dbaas.root.create(id) + 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, id): + def enabled(self): """Check the instance for root access""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.root.is_root_enabled(id)) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.root.is_root_enabled, self.id) -class VersionCommands(object): +class VersionCommands(common.CommandsBase): """List available versions""" - def __init__(self): - pass + params = [ + 'url', + ] - def list(self, url): + def list(self): """List all the supported versions""" - dbaas = common.get_client() - try: - versions = dbaas.versions.index(url) - for version in versions: - _pretty_print(version._info) - except: - print sys.exc_info()[1] + self._require('url') + self._pretty_print(self.dbaas.versions.index, self.url) def config_options(oparser): @@ -290,12 +228,14 @@ def config_options(oparser): 'version': VersionCommands, } - def main(): # Parse arguments - oparser = optparse.OptionParser("%prog [options] ", - version='1.0') + oparser = optparse.OptionParser(usage="%prog [options] ", + version='1.0', + conflict_handler='resolve') config_options(oparser) + for k, v in COMMANDS.items(): + v._prepare_parser(oparser) (options, args) = oparser.parse_args() if not args: @@ -305,7 +245,7 @@ def main(): cmd = args.pop(0) if cmd in COMMANDS: fn = COMMANDS.get(cmd) - command_object = fn() + command_object = fn(oparser) # Get a list of supported actions for the command actions = common.methods_of(command_object) @@ -316,25 +256,10 @@ def main(): # Check for a valid action and perform that action action = args.pop(0) if action in actions: - fn = actions.get(action) - try: - # TODO(rnirmal): Fix when we have proper argument parsing for - # the rest of the commands. - if fn.__name__ == "login": - fn(*args, options=options) - else: - fn(*args) - sys.exit(0) - except TypeError as err: - print "Possible wrong number of arguments supplied." - print "%s %s: %s" % (cmd, action, fn.__doc__) - print "\t\t", [fn.func_code.co_varnames[i] for i in - range(fn.func_code.co_argcount)] - print "ERROR: %s" % err - except Exception: - print "Command failed, please check the log for more info." - raise + getattr(command_object, action)() + except Exception as ex: + print ex else: common.print_actions(cmd, actions) else: diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index c3449c6..9a766f2 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import os import pickle import sys @@ -111,30 +112,112 @@ def __init__(self, user, apikey, tenant, token, auth_url, auth_strategy, self._insecure = insecure -class Auth(object): - """Authenticate with your username and api key""" +class ArgumentRequired(Exception): + def __init__(self, param): + self.param = param + + def __str__(self): + return 'Argument "--%s" required.' % self.param + + +class CommandsBase(object): + params = [] + + @classmethod + def _prepare_parser(cls, parser): + for param in cls.params: + parser.add_option("--%s" % param) + + def __init__(self, parser): + self.dbaas = get_client() + self._parse_options(parser) + + 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 any([param not in self.params, + 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 __init__(self): - pass + def _pretty_print(self, func, *args, **kwargs): + try: + result = func(*args, **kwargs) + print json.dumps(result._info, sort_keys=True, indent=4) + except: + print sys.exc_info()[1] - def login(self, options=None): + 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) + for item in result: + print json.dumps(item._info, sort_keys=True, indent=4) + if result.links: + for link in result.links: + print json.dumps(link, sort_keys=True, indent=4) + except: + print sys.exc_info()[1] + + +class Auth(CommandsBase): + """Authenticate with your username and api key""" + params = [ + 'apikey', + 'auth_strategy', + 'auth_type', + 'auth_url', + 'insecure', + 'options', + 'region', + 'service_name', + 'service_type', + 'service_url', + 'tenant_id', + 'username', + ] + + def __init__(self, parser): + self.parser = parser + self.dbaas = None + self._parse_options(parser) + + def login(self): """Login to retrieve an auth token to use for other api calls""" + self._require('username', 'apikey', 'tenant_id') try: - dbaas = Dbaas(options.username, options.apikey, options.tenant_id, - auth_url=options.auth_url, - auth_strategy=options.auth_type, - service_type=options.service_type, - service_name=options.service_name, - region_name=options.region, - service_url=options.service_url, - insecure=options.insecure) - dbaas.authenticate() - apitoken = APIToken(options.username, options.apikey, - options.tenant_id, dbaas.client.auth_token, - options.auth_url, options.auth_type, - options.service_type, options.service_name, - options.service_url, options.region, - options.insecure) + self.dbaas = 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) + self.dbaas.authenticate() + apitoken = APIToken(self.username, self.apikey, + self.tenant_id, self.dbaas.client.auth_token, + self.auth_url, self.auth_type, + self.service_type, self.service_name, + self.service_url, self.region, + self.insecure) with open(APITOKEN, 'wb') as token: pickle.dump(apitoken, token, protocol=2) From e9318ec03cdeda7a5bd0a8d7a08f8adf3f2d5909 Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Thu, 9 Aug 2012 14:15:08 -0500 Subject: [PATCH 35/42] Fixed volume_size for size --- reddwarfclient/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index eb3e7f7..37971f2 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -50,7 +50,7 @@ class InstanceCommands(common.CommandsBase): def create(self): """Create a new instance""" - self._require('name', 'volume_size') + self._require('name', 'size') # flavorRef is not required. flavorRef = self.flavor or "http://localhost:8775/v1.0/flavors/1" volume = {"size": self.size} From 611fd13894f4208bdc4be8838a1e2b32396c24c5 Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Tue, 14 Aug 2012 09:58:06 -0500 Subject: [PATCH 36/42] Fixes reference to flavor_id that should be flavor instead --- reddwarfclient/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index 37971f2..4a86e8b 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -85,7 +85,7 @@ def resize_instance(self): """Resize an instance flavor""" self._require('id', 'flavor') self._pretty_print(self.dbaas.instances.resize_instance, self.id, - self.flavor_id) + self.flavor) def restart(self): """Restart the database""" From 88f9530151b5252487b260cec8e4cdb61f07acec Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Thu, 9 Aug 2012 11:04:28 -0500 Subject: [PATCH 37/42] Added a ton of CLI options, plus fixed a CI bug. * Renamed the auth_type "basic" to the more apt "auth1.1". * Made it possible to pass an "token" and "service_url" argument alone to the client. It wouldn't work with just this before. * The client now saves all arguments you give it to the pickled file, including the auth strategy, and preserves the token and service_url (which it didn't before) which makes exotic auth types such as "fake" easier to work with. * Not raising an error for a lack of an auth_url until auth occurs (which is usually right after creation of the client anyway for most auth types). * Moved oparser code into CliOption class. This is where the options live plus is the name of that pickled file that gets stored on login. * Added a "debug" option which avoids swallowing stack traces if something goes wrong with the CLI. Should make client work much easier. * Added a "verbose" option which changes the output to instead show the simulated CURL statement plus the request and response headers and bodies, which is useful because I... * Added an "xml" option which does all the communication in XML. * Fixed a bug which was affecting the CI tests where the client would fail if the response body could not be parsed. * Added all of Ed's work to update the mgmt CLI module with his newer named parameters. --- reddwarfclient/auth.py | 51 ++++++- reddwarfclient/cli.py | 68 ++++----- reddwarfclient/client.py | 51 +++++-- reddwarfclient/common.py | 278 +++++++++++++++++++++++++---------- reddwarfclient/exceptions.py | 15 ++ reddwarfclient/mcli.py | 182 ++++++++++------------- reddwarfclient/xml.py | 6 +- 7 files changed, 411 insertions(+), 240 deletions(-) diff --git a/reddwarfclient/auth.py b/reddwarfclient/auth.py index 51c901d..76ef176 100644 --- a/reddwarfclient/auth.py +++ b/reddwarfclient/auth.py @@ -24,6 +24,8 @@ def get_authenticator_cls(cls_or_name): 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 @@ -40,6 +42,8 @@ class Authenticator(object): """ + URL_REQUIRED=True + def __init__(self, client, type, url, username, password, tenant, region=None, service_type=None, service_name=None, service_url=None): @@ -54,7 +58,7 @@ def __init__(self, client, type, url, username, password, tenant, self.service_name = service_name self.service_url = service_url - def _authenticate(self, url, body): + 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 @@ -70,7 +74,8 @@ def _authenticate(self, url, body): return ServiceCatalog(body, region=self.region, service_type=self.service_type, service_name=self.service_name, - service_url=self.service_url) + service_url=self.service_url, + root_key=root_key) except exceptions.AmbiguousEndpoints: print "Found more than one valid endpoint. Use a more "\ "restrictive filter" @@ -93,6 +98,8 @@ def authenticate(self): 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): @@ -110,9 +117,40 @@ def _v2_auth(self, url): 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): @@ -155,7 +193,7 @@ class ServiceCatalog(object): """ def __init__(self, resource_dict, region=None, service_type=None, - service_name=None, service_url=None): + service_name=None, service_url=None, root_key='access'): self.catalog = resource_dict self.region = region self.service_type = service_type @@ -163,6 +201,7 @@ def __init__(self, resource_dict, region=None, service_type=None, self.service_url = service_url self.management_url = None self.public_url = None + self.root_key = root_key self._load() def _load(self): @@ -178,7 +217,7 @@ def _load(self): self.management_url = self.service_url def get_token(self): - return self.catalog['access']['token']['id'] + return self.catalog[self.root_key]['token']['id'] def get_management_url(self): return self.management_url @@ -202,11 +241,11 @@ def _url_for(self, attr=None, filter_value=None, raise exceptions.EndpointNotFound() # We don't always get a service catalog back ... - if not 'serviceCatalog' in self.catalog['access']: + if not 'serviceCatalog' in self.catalog[self.root_key]: raise exceptions.EndpointNotFound() # Full catalog ... - catalog = self.catalog['access']['serviceCatalog'] + catalog = self.catalog[self.root_key]['serviceCatalog'] for service in catalog: if service.get("type") != self.service_type: diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index 37971f2..9b4489d 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -18,6 +18,7 @@ Reddwarf Command line tool """ +#TODO(tim.simpson): optparse is deprecated. Replace with argparse. import optparse import os import sys @@ -36,7 +37,7 @@ from reddwarfclient import common -class InstanceCommands(common.CommandsBase): +class InstanceCommands(common.AuthedCommandsBase): """Commands to perform various instances operations and actions""" params = [ @@ -93,17 +94,17 @@ def restart(self): self._pretty_print(self.dbaas.instances.restart, self.id) -class FlavorsCommands(common.CommandsBase): +class FlavorsCommands(common.AuthedCommandsBase): """Commands for listing Flavors""" params = [] def list(self): """List the available flavors""" - self._pretty_print(self.dbaas.flavors.list) + self._pretty_list(self.dbaas.flavors.list) -class DatabaseCommands(common.CommandsBase): +class DatabaseCommands(common.AuthedCommandsBase): """Database CRUD operations on an instance""" params = [ @@ -130,7 +131,7 @@ def list(self): self._pretty_paged(self.dbaas.databases.list, self.id) -class UserCommands(common.CommandsBase): +class UserCommands(common.AuthedCommandsBase): """User CRUD operations on an instance""" params = [ 'id', @@ -159,7 +160,7 @@ def list(self): self._pretty_paged(self.dbaas.users.list, self.id) -class RootCommands(common.CommandsBase): +class RootCommands(common.AuthedCommandsBase): """Root user related operations on an instance""" params = [ @@ -181,7 +182,7 @@ def enabled(self): self._pretty_print(self.dbaas.root.is_root_enabled, self.id) -class VersionCommands(common.CommandsBase): +class VersionCommands(common.AuthedCommandsBase): """List available versions""" params = [ @@ -194,31 +195,6 @@ def list(self): self._pretty_print(self.dbaas.versions.index, self.url) -def config_options(oparser): - oparser.add_option("--auth_url", default="http://localhost:5000/v2.0", - help="Auth API endpoint URL with port and version. \ - Default: http://localhost:5000/v2.0") - oparser.add_option("--username", help="Login username") - oparser.add_option("--apikey", help="Api key") - oparser.add_option("--tenant_id", - help="Tenant Id associated with the account") - oparser.add_option("--auth_type", default="keystone", - help="Auth type to support different auth environments, \ - Supported values are 'keystone', 'rax'.") - oparser.add_option("--service_type", default="reddwarf", - help="Service type is a name associated for the catalog") - oparser.add_option("--service_name", default="Reddwarf", - help="Service name as provided in the service catalog") - oparser.add_option("--service_url", default="", - help="Service endpoint to use if the catalog doesn't \ - have one") - oparser.add_option("--region", default="RegionOne", - help="Region the service is located in") - oparser.add_option("-i", "--insecure", action="store_true", - dest="insecure", default=False, - help="Run in insecure mode for https endpoints.") - - COMMANDS = {'auth': common.Auth, 'instance': InstanceCommands, 'flavor': FlavorsCommands, @@ -230,10 +206,7 @@ def config_options(oparser): def main(): # Parse arguments - oparser = optparse.OptionParser(usage="%prog [options] ", - version='1.0', - conflict_handler='resolve') - config_options(oparser) + oparser = common.CliOptions.create_optparser() for k, v in COMMANDS.items(): v._prepare_parser(oparser) (options, args) = oparser.parse_args() @@ -241,11 +214,21 @@ def main(): 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 = fn(oparser) + 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) @@ -256,10 +239,15 @@ def main(): # Check for a valid action and perform that action action = args.pop(0) if action in actions: - try: + if not options.debug: + try: + getattr(command_object, action)() + except Exception as ex: + if options.debug: + raise + print ex + else: getattr(command_object, action)() - except Exception as ex: - print ex else: common.print_actions(cmd, actions) else: diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 440e2b9..75dd457 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -18,6 +18,7 @@ import os import time import urlparse +import sys try: import json @@ -37,12 +38,17 @@ RDC_PP = os.environ.get("RDC_PP", "False") == "True" -if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']: - ch = logging.StreamHandler() +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' @@ -60,7 +66,10 @@ def __init__(self, user, password, tenant, auth_url, service_name, self.username = user self.password = password self.tenant = tenant - self.auth_url = auth_url.rstrip('/') + 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 @@ -165,7 +174,13 @@ def request(self, *args, **kwargs): self.http_log(args, kwargs, resp, body) if body: - body = self.morph_response_body(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 @@ -174,6 +189,10 @@ def request(self, *args, **kwargs): return resp, body + def raise_error_from_status(self, resp, body): + if resp.status in (400, 401, 403, 404, 408, 409, 413, 500, 501): + raise exceptions.from_response(resp, body) + def morph_request(self, kwargs): kwargs['headers']['Accept'] = 'application/json' kwargs['headers']['Content-Type'] = 'application/json' @@ -181,7 +200,10 @@ def morph_request(self, kwargs): kwargs['body'] = json.dumps(kwargs['body']) def morph_response_body(self, body_string): - return json.loads(body_string) + try: + return json.loads(body_string) + except ValueError: + raise exceptions.ResponseFormatError() def _time_request(self, url, method, **kwargs): start_time = time.time() @@ -236,12 +258,23 @@ def authenticate(self): """ catalog = self.authenticator.authenticate() - self.auth_token = catalog.get_token() - if not self.service_url: + if self.service_url: + possible_service_url = None + else: if self.endpoint_type == "publicURL": - self.service_url = catalog.get_public_url() + possible_service_url = catalog.get_public_url() elif self.endpoint_type == "adminURL": - self.service_url = catalog.get_management_url() + 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): diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index 9a766f2..c06a7e7 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -12,42 +12,18 @@ # 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.client import Dbaas +from reddwarfclient import client +from reddwarfclient.xml import ReddwarfXmlClient from reddwarfclient import exceptions -APITOKEN = os.path.expanduser("~/.apitoken") - - -def get_client(): - """Load an existing apitoken if available""" - try: - with open(APITOKEN, 'rb') as token: - apitoken = pickle.load(token) - dbaas = Dbaas(apitoken._user, apitoken._apikey, - tenant=apitoken._tenant, auth_url=apitoken._auth_url, - auth_strategy=apitoken._auth_strategy, - service_type=apitoken._service_type, - service_name=apitoken._service_name, - service_url=apitoken._service_url, - insecure=apitoken._insecure, - region_name=apitoken._region_name) - dbaas.client.auth_token = apitoken._token - return dbaas - except IOError: - print "ERROR: You need to login first and get an auth token\n" - sys.exit(1) - except: - print "ERROR: There was an error using your existing auth token, " \ - "please login again.\n" - sys.exit(1) - - 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)""" @@ -92,24 +68,110 @@ def limit_url(url, limit=None, marker=None): return url + query -class APIToken(object): +class CliOptions(object): """A token object containing the user, apikey and token which is pickleable.""" - def __init__(self, user, apikey, tenant, token, auth_url, auth_strategy, - service_type, service_name, service_url, region_name, - insecure): - self._user = user - self._apikey = apikey - self._tenant = tenant - self._token = token - self._auth_url = auth_url - self._auth_strategy = auth_strategy - self._service_type = service_type - self._service_name = service_name - self._service_url = service_url - self._region_name = region_name - self._insecure = insecure + APITOKEN = os.path.expanduser("~/.apitoken") + + DEFAULT_VALUES = { + 'username':None, + 'apikey':None, + 'tenant_id':None, + 'auth_url':None, + 'auth_type':'keystone', + 'service_type':'reddwarf', + '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): + oparser = optparse.OptionParser( + usage="%prog [options] ", + version='1.0', conflict_handler='resolve') + file = cls.load_from_file() + 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): @@ -123,15 +185,49 @@ def __str__(self): 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 __init__(self, parser): - self.dbaas = get_client() - self._parse_options(parser) - def _parse_options(self, parser): opts, args = parser.parse_args() for param in opts.__dict__: @@ -140,9 +236,8 @@ def _parse_options(self, parser): def _require(self, *params): for param in params: - if any([param not in self.params, - not hasattr(self, param)]): - raise ArgumentRequired(param) + if not hasattr(self, param): + raise ArgumentRequired(param) if not getattr(self, param): raise ArgumentRequired(param) @@ -156,11 +251,23 @@ def _make_list(self, *params): setattr(self, param, raw) def _pretty_print(self, func, *args, **kwargs): - try: + 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) print json.dumps(result._info, sort_keys=True, indent=4) - except: - print sys.exc_info()[1] + 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 + for item in result: + print self._dumps(item._info) def _pretty_paged(self, func, *args, **kwargs): try: @@ -168,12 +275,16 @@ def _pretty_paged(self, func, *args, **kwargs): 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. for item in result: - print json.dumps(item._info, sort_keys=True, indent=4) + print self._dumps(item._info) if result.links: for link in result.links: - print json.dumps(link, sort_keys=True, indent=4) + print self._dumps((link)) except: + if self.debug: + raise print sys.exc_info()[1] @@ -195,37 +306,52 @@ class Auth(CommandsBase): ] def __init__(self, parser): - self.parser = parser + super(Auth, self).__init__(parser) self.dbaas = None - self._parse_options(parser) def login(self): """Login to retrieve an auth token to use for other api calls""" - self._require('username', 'apikey', 'tenant_id') + self._require('username', 'apikey', 'tenant_id', 'auth_url') try: - self.dbaas = 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) + self.dbaas = self.get_client() self.dbaas.authenticate() - apitoken = APIToken(self.username, self.apikey, - self.tenant_id, self.dbaas.client.auth_token, - self.auth_url, self.auth_type, - self.service_type, self.service_name, - self.service_url, self.region, - self.insecure) - - with open(APITOKEN, 'wb') as token: - pickle.dump(apitoken, token, protocol=2) - print apitoken._token + self.token = self.dbaas.client.auth_token + self.service_url = self.dbaas.client.service_url + CliOptions.save_from_instance_fields(self) + print(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. """ diff --git a/reddwarfclient/exceptions.py b/reddwarfclient/exceptions.py index 33c25e8..eb305a3 100644 --- a/reddwarfclient/exceptions.py +++ b/reddwarfclient/exceptions.py @@ -41,6 +41,20 @@ class EndpointNotFound(Exception): 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): @@ -159,4 +173,5 @@ def from_response(response, body): 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/mcli.py b/reddwarfclient/mcli.py index 394d143..2cacfa5 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -44,143 +44,112 @@ def _pretty_print(info): print json.dumps(info, sort_keys=True, indent=4) -class HostCommands(object): +class HostCommands(common.AuthedCommandsBase): """Commands to list info on hosts""" - def __init__(self): - pass + params = [ + 'name', + ] - def get(self, 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""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.hosts.get(name)._info) - except: - print sys.exc_info()[1] + self._require('name') + self._pretty_print(self.dbaas.hosts.get, self.name) def list(self): """List all compute hosts""" - dbaas = common.get_client() - try: - for host in dbaas.hosts.index(): - _pretty_print(host._info) - except: - print sys.exc_info()[1] + self._pretty_list(self.dbaas.hosts.index) -class RootCommands(object): +class RootCommands(common.AuthedCommandsBase): """List details about the root info for an instance.""" - def __init__(self): - pass + params = [ + 'id', + ] - def history(self, id): + def history(self): """List root history for the instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.root_enabled_history(id) - _pretty_print(result._info) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.root_enabled_history, self.id) -class AccountCommands(object): +class AccountCommands(common.AuthedCommandsBase): """Commands to list account info""" - def __init__(self): - pass + params = [ + 'id', + ] def list(self): """List all accounts with non-deleted instances""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.accounts.index()._info) - except: - print sys.exc_info()[1] + self._pretty_print(self.dbaas.accounts.index) - def get(self, acct): + def get(self): """List details for the account provided""" - dbaas = common.get_client() - try: - _pretty_print(dbaas.accounts.show(acct)._info) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.accounts.show, self.id) -class InstanceCommands(object): +class InstanceCommands(common.AuthedCommandsBase): """List details about an instance.""" - def __init__(self): - pass + params = [ + 'deleted', + 'id', + 'limit', + 'marker', + ] - def get(self, id): + def get(self): """List details for the instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.show(id) - _pretty_print(result._info) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.show, self.id) - def list(self, deleted=None, limit=None, marker=None): + def list(self): """List all instances for account""" - dbaas = common.get_client() - if limit: - limit = int(limit, 10) - try: - instances = dbaas.management.index(deleted, limit, marker) - for instance in instances: - _pretty_print(instance._info) - if instances.links: - for link in instances.links: - _pretty_print(link) - except: - print sys.exc_info()[1] - - def diagnostic(self, id): + 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 diagnostic(self): """List diagnostic details about an instance.""" + self._require('id') dbaas = common.get_client() - try: - result = dbaas.diagnostics.get(id) - _pretty_print(result._info) - except: - print sys.exc_info()[1] + self._pretty_print(self.dbaas.diagnostics.get, self.id) - def stop(self, id): + def stop(self): """Stop MySQL on the given instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.stop(id) - except: - print sys.exc_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.stop, self.id) def reboot(self, id): """Reboot the instance.""" - dbaas = common.get_client() - try: - result = dbaas.management.reboot(id) - except: - print sys.exec_info()[1] + self._require('id') + self._pretty_print(self.dbaas.management.reboot, self.id) -class StorageCommands(object): +class StorageCommands(common.AuthedCommandsBase): """Commands to list devices info""" - def __init__(self): - pass + params = [] def list(self): """List details for the storage device""" dbaas = common.get_client() - try: - for storage in dbaas.storage.index(): - _pretty_print(storage._info) - except: - print sys.exc_info()[1] + self._pretty_list(self.dbaas.storage.index) -def config_options(): - global oparser +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") @@ -196,10 +165,9 @@ def config_options(): def main(): # Parse arguments - global oparser - oparser = optparse.OptionParser("%prog [options] ", - version='1.0') - config_options() + oparser = common.CliOptions.create_optparser() + for k, v in COMMANDS.items(): + v._prepare_parser(oparser) (options, args) = oparser.parse_args() if not args: @@ -209,7 +177,13 @@ def main(): cmd = args.pop(0) if cmd in COMMANDS: fn = COMMANDS.get(cmd) - command_object = fn() + 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) @@ -220,20 +194,12 @@ def main(): # Check for a valid action and perform that action action = args.pop(0) if action in actions: - fn = actions.get(action) - try: - fn(*args) - sys.exit(0) - except TypeError as err: - print "Possible wrong number of arguments supplied." - print "%s %s: %s" % (cmd, action, fn.__doc__) - print "\t\t", [fn.func_code.co_varnames[i] for i in - range(fn.func_code.co_argcount)] - print "ERROR: %s" % err - except Exception: - print "Command failed, please check the log for more info." - raise + getattr(command_object, action)() + except Exception as ex: + if options.debug: + raise + print ex else: common.print_actions(cmd, actions) else: diff --git a/reddwarfclient/xml.py b/reddwarfclient/xml.py index ae1c226..d0b7f9b 100644 --- a/reddwarfclient/xml.py +++ b/reddwarfclient/xml.py @@ -2,6 +2,7 @@ import json from numbers import Number +from reddwarfclient import exceptions from reddwarfclient.client import ReddwarfHTTPClient @@ -196,7 +197,10 @@ 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 = {} - root_element = etree.XML(body_string) + 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 } From 3e2106b858dccb509a99e828fee903a9f2368a90 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Tue, 14 Aug 2012 13:09:38 -0500 Subject: [PATCH 38/42] Update setup.py Adding lxml to the requirements in setup.py. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 47752c8..037af38 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import sys -requirements = ["httplib2", "prettytable"] +requirements = ["httplib2", "lxml", "prettytable"] if sys.version_info < (2, 6): requirements.append("simplejson") if sys.version_info < (2, 7): From 62e603a3b757f7c405ce065401ca8a4920670601 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Tue, 14 Aug 2012 15:29:38 -0500 Subject: [PATCH 39/42] Minor fixes and tweaks. * insecure no longer needs a string value added to it. * Print "ok" in situations where the client would otherwise give no output. * Fixed a bug in non-verbose output. --- reddwarfclient/cli.py | 2 +- reddwarfclient/common.py | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/reddwarfclient/cli.py b/reddwarfclient/cli.py index ceed9e9..ea83d7b 100644 --- a/reddwarfclient/cli.py +++ b/reddwarfclient/cli.py @@ -97,7 +97,7 @@ 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""" diff --git a/reddwarfclient/common.py b/reddwarfclient/common.py index c06a7e7..2e4ff80 100644 --- a/reddwarfclient/common.py +++ b/reddwarfclient/common.py @@ -198,7 +198,6 @@ def get_client(self): 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, @@ -256,7 +255,10 @@ def _pretty_print(self, func, *args, **kwargs): return # Skip this, since the verbose stuff will show up anyway. def wrapped_func(): result = func(*args, **kwargs) - print json.dumps(result._info, sort_keys=True, indent=4) + if result: + print(json.dumps(result._info, sort_keys=True, indent=4)) + else: + print("OK") self._safe_exec(wrapped_func) def _dumps(self, item): @@ -266,8 +268,11 @@ def _pretty_list(self, func, *args, **kwargs): result = self._safe_exec(func, *args, **kwargs) if self.verbose: return - for item in result: - print self._dumps(item._info) + 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: @@ -277,11 +282,15 @@ def _pretty_paged(self, func, *args, **kwargs): result = func(*args, limit=limit, marker=self.marker, **kwargs) if self.verbose: return # Verbose already shows the output, so skip this. - for item in result: - print self._dumps(item._info) - if result.links: - for link in result.links: - print self._dumps((link)) + 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 @@ -295,7 +304,6 @@ class Auth(CommandsBase): 'auth_strategy', 'auth_type', 'auth_url', - 'insecure', 'options', 'region', 'service_name', From 662214abf6576d6bd42bce9f3d8b5002f27696e4 Mon Sep 17 00:00:00 2001 From: Paul Marshall Date: Wed, 15 Aug 2012 10:58:59 -0500 Subject: [PATCH 40/42] adding hardware info call to management client --- reddwarfclient/__init__.py | 3 ++- reddwarfclient/client.py | 6 ++++-- reddwarfclient/diagnostics.py | 21 ++++++++++++++++++++- reddwarfclient/mcli.py | 6 +++++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/reddwarfclient/__init__.py b/reddwarfclient/__init__.py index 40bd3f8..0383828 100644 --- a/reddwarfclient/__init__.py +++ b/reddwarfclient/__init__.py @@ -25,6 +25,7 @@ from reddwarfclient.storage import StorageInfo from reddwarfclient.users import Users from reddwarfclient.versions import Versions -from reddwarfclient.diagnostics import Interrogator +from reddwarfclient.diagnostics import DiagnosticsInterrogator +from reddwarfclient.diagnostics import HwInfoInterrogator from reddwarfclient.client import Dbaas from reddwarfclient.client import ReddwarfHTTPClient diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 75dd457..7dbd01b 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -310,7 +310,8 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, from reddwarfclient.storage import StorageInfo from reddwarfclient.management import Management from reddwarfclient.accounts import Accounts - from reddwarfclient.diagnostics import Interrogator + from reddwarfclient.diagnostics import DiagnosticsInterrogator + from reddwarfclient.diagnostics import HwInfoInterrogator self.client = client_cls(username, api_key, tenant, auth_url, service_type=service_type, @@ -329,7 +330,8 @@ def __init__(self, username, api_key, tenant=None, auth_url=None, self.storage = StorageInfo(self) self.management = Management(self) self.accounts = Accounts(self) - self.diagnostics = Interrogator(self) + self.diagnostics = DiagnosticsInterrogator(self) + self.hwinfo = HwInfoInterrogator(self) class Mgmt(object): def __init__(self, dbaas): diff --git a/reddwarfclient/diagnostics.py b/reddwarfclient/diagnostics.py index 3a81ab8..64904b7 100644 --- a/reddwarfclient/diagnostics.py +++ b/reddwarfclient/diagnostics.py @@ -25,7 +25,7 @@ def __repr__(self): return "" % self.version -class Interrogator(base.ManagerWithFind): +class DiagnosticsInterrogator(base.ManagerWithFind): """ Manager class for Interrogator resource """ @@ -37,3 +37,22 @@ def get(self, 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/mcli.py b/reddwarfclient/mcli.py index 2cacfa5..fb76c93 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -121,10 +121,14 @@ def list(self): 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') - dbaas = common.get_client() self._pretty_print(self.dbaas.diagnostics.get, self.id) def stop(self): From 4fd5a896decc07fc06d7ac01d8c77e97878ce3ba Mon Sep 17 00:00:00 2001 From: Ed Cranford Date: Wed, 15 Aug 2012 16:42:37 -0500 Subject: [PATCH 41/42] Removes the id from reboot since params and require take care of that now. --- reddwarfclient/mcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reddwarfclient/mcli.py b/reddwarfclient/mcli.py index 2cacfa5..17c1b1e 100644 --- a/reddwarfclient/mcli.py +++ b/reddwarfclient/mcli.py @@ -132,7 +132,7 @@ def stop(self): self._require('id') self._pretty_print(self.dbaas.management.stop, self.id) - def reboot(self, id): + def reboot(self): """Reboot the instance.""" self._require('id') self._pretty_print(self.dbaas.management.reboot, self.id) From 9be007515ad7ffce7a0841a8250233657b538c4d Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Thu, 16 Aug 2012 13:49:05 -0500 Subject: [PATCH 42/42] Fixed re-authentication logic to actually use the fresher X-Auth-Token. --- reddwarfclient/client.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/reddwarfclient/client.py b/reddwarfclient/client.py index 75dd457..bca09c2 100644 --- a/reddwarfclient/client.py +++ b/reddwarfclient/client.py @@ -213,13 +213,7 @@ def _time_request(self, url, method, **kwargs): return resp, body def _cs_request(self, url, method, **kwargs): - if not self.auth_token or not self.service_url: - self.authenticate() - - # Perform the request once. If we get a 401 back then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: + def request(): kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token if self.tenant: kwargs['headers']['X-Auth-Project-Id'] = self.tenant @@ -227,14 +221,18 @@ def _cs_request(self, url, method, **kwargs): 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: - try: - self.authenticate() - resp, body = self._time_request(self.service_url + url, - method, **kwargs) - return resp, body - except exceptions.Unauthorized: - raise ex + self.authenticate() + return request() def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs)