summaryrefslogtreecommitdiff
path: root/cinderclient
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-05-21 16:32:35 -0400
committerMonty Taylor <mordred@inaugust.com>2012-05-21 16:32:35 -0400
commit471704df644eced17026c280b0aab9e549718e14 (patch)
treec2d8d0ec74fa45e0b61ca4b2153fb5b0e7bf490d /cinderclient
downloadpython-cinderclient-0.0.tar.gz
Initial split from python-novaclient.0.0
Diffstat (limited to 'cinderclient')
-rw-r--r--cinderclient/__init__.py0
-rw-r--r--cinderclient/base.py293
-rw-r--r--cinderclient/client.py330
-rw-r--r--cinderclient/exceptions.py146
-rw-r--r--cinderclient/extension.py39
-rw-r--r--cinderclient/service_catalog.py77
-rw-r--r--cinderclient/shell.py435
-rw-r--r--cinderclient/utils.py261
-rw-r--r--cinderclient/v1/__init__.py17
-rw-r--r--cinderclient/v1/client.py71
-rw-r--r--cinderclient/v1/contrib/__init__.py0
-rw-r--r--cinderclient/v1/shell.py241
-rw-r--r--cinderclient/v1/volume_snapshots.py88
-rw-r--r--cinderclient/v1/volume_types.py77
-rw-r--r--cinderclient/v1/volumes.py135
15 files changed, 2210 insertions, 0 deletions
diff --git a/cinderclient/__init__.py b/cinderclient/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cinderclient/__init__.py
diff --git a/cinderclient/base.py b/cinderclient/base.py
new file mode 100644
index 0000000..02d3549
--- /dev/null
+++ b/cinderclient/base.py
@@ -0,0 +1,293 @@
+# Copyright 2010 Jacob Kaplan-Moss
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Base utilities to build API operation managers and objects on top of.
+"""
+
+import contextlib
+import hashlib
+import os
+from cinderclient import exceptions
+from cinderclient import utils
+
+
+# Python 2.4 compat
+try:
+ all
+except NameError:
+ def all(iterable):
+ return True not in (not x for x in iterable)
+
+
+def getid(obj):
+ """
+ Abstracts the common pattern of allowing both an object or an object's ID
+ as a parameter when dealing with relationships.
+ """
+ try:
+ return obj.id
+ except AttributeError:
+ return obj
+
+
+class Manager(utils.HookableMixin):
+ """
+ Managers interact with a particular type of API (servers, flavors, images,
+ etc.) and provide CRUD operations for them.
+ """
+ resource_class = None
+
+ def __init__(self, api):
+ self.api = api
+
+ def _list(self, url, response_key, obj_class=None, body=None):
+ resp = None
+ if body:
+ resp, body = self.api.client.post(url, body=body)
+ else:
+ resp, body = self.api.client.get(url)
+
+ if obj_class is None:
+ obj_class = self.resource_class
+
+ data = body[response_key]
+ # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
+ # unlike other services which just return the list...
+ if isinstance(data, dict):
+ try:
+ data = data['values']
+ except KeyError:
+ pass
+
+ with self.completion_cache('human_id', obj_class, mode="w"):
+ with self.completion_cache('uuid', obj_class, mode="w"):
+ return [obj_class(self, res, loaded=True)
+ for res in data if res]
+
+ @contextlib.contextmanager
+ def completion_cache(self, cache_type, obj_class, mode):
+ """
+ The completion cache store items that can be used for bash
+ autocompletion, like UUIDs or human-friendly IDs.
+
+ A resource listing will clear and repopulate the cache.
+
+ A resource create will append to the cache.
+
+ Delete is not handled because listings are assumed to be performed
+ often enough to keep the cache reasonably up-to-date.
+ """
+ base_dir = utils.env('CINDERCLIENT_UUID_CACHE_DIR',
+ default="~/.cinderclient")
+
+ # NOTE(sirp): Keep separate UUID caches for each username + endpoint
+ # pair
+ username = utils.env('OS_USERNAME', 'CINDER_USERNAME')
+ url = utils.env('OS_URL', 'CINDER_URL')
+ uniqifier = hashlib.md5(username + url).hexdigest()
+
+ cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
+
+ try:
+ os.makedirs(cache_dir, 0755)
+ except OSError:
+ # NOTE(kiall): This is typicaly either permission denied while
+ # attempting to create the directory, or the directory
+ # already exists. Either way, don't fail.
+ pass
+
+ resource = obj_class.__name__.lower()
+ filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
+ path = os.path.join(cache_dir, filename)
+
+ cache_attr = "_%s_cache" % cache_type
+
+ try:
+ setattr(self, cache_attr, open(path, mode))
+ except IOError:
+ # NOTE(kiall): This is typicaly a permission denied while
+ # attempting to write the cache file.
+ pass
+
+ try:
+ yield
+ finally:
+ cache = getattr(self, cache_attr, None)
+ if cache:
+ cache.close()
+ delattr(self, cache_attr)
+
+ def write_to_completion_cache(self, cache_type, val):
+ cache = getattr(self, "_%s_cache" % cache_type, None)
+ if cache:
+ cache.write("%s\n" % val)
+
+ def _get(self, url, response_key=None):
+ resp, body = self.api.client.get(url)
+ if response_key:
+ return self.resource_class(self, body[response_key], loaded=True)
+ else:
+ return self.resource_class(self, body, loaded=True)
+
+ def _create(self, url, body, response_key, return_raw=False, **kwargs):
+ self.run_hooks('modify_body_for_create', body, **kwargs)
+ resp, body = self.api.client.post(url, body=body)
+ if return_raw:
+ return body[response_key]
+
+ with self.completion_cache('human_id', self.resource_class, mode="a"):
+ with self.completion_cache('uuid', self.resource_class, mode="a"):
+ return self.resource_class(self, body[response_key])
+
+ def _delete(self, url):
+ resp, body = self.api.client.delete(url)
+
+ def _update(self, url, body, **kwargs):
+ self.run_hooks('modify_body_for_update', body, **kwargs)
+ resp, body = self.api.client.put(url, body=body)
+ return body
+
+
+class ManagerWithFind(Manager):
+ """
+ Like a `Manager`, but with additional `find()`/`findall()` methods.
+ """
+ def find(self, **kwargs):
+ """
+ Find a single item with attributes matching ``**kwargs``.
+
+ This isn't very efficient: it loads the entire list then filters on
+ the Python side.
+ """
+ matches = self.findall(**kwargs)
+ num_matches = len(matches)
+ if num_matches == 0:
+ msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+ raise exceptions.NotFound(404, msg)
+ elif num_matches > 1:
+ raise exceptions.NoUniqueMatch
+ else:
+ return matches[0]
+
+ def findall(self, **kwargs):
+ """
+ Find all items with attributes matching ``**kwargs``.
+
+ This isn't very efficient: it loads the entire list then filters on
+ the Python side.
+ """
+ found = []
+ searches = kwargs.items()
+
+ for obj in self.list():
+ try:
+ if all(getattr(obj, attr) == value
+ for (attr, value) in searches):
+ found.append(obj)
+ except AttributeError:
+ continue
+
+ return found
+
+ def list(self):
+ raise NotImplementedError
+
+
+class Resource(object):
+ """
+ A resource represents a particular instance of an object (server, flavor,
+ etc). This is pretty much just a bag for attributes.
+
+ :param manager: Manager object
+ :param info: dictionary representing resource attributes
+ :param loaded: prevent lazy-loading if set to True
+ """
+ HUMAN_ID = False
+
+ def __init__(self, manager, info, loaded=False):
+ self.manager = manager
+ self._info = info
+ self._add_details(info)
+ self._loaded = loaded
+
+ # NOTE(sirp): ensure `id` is already present because if it isn't we'll
+ # enter an infinite loop of __getattr__ -> get -> __init__ ->
+ # __getattr__ -> ...
+ if 'id' in self.__dict__ and len(str(self.id)) == 36:
+ self.manager.write_to_completion_cache('uuid', self.id)
+
+ human_id = self.human_id
+ if human_id:
+ self.manager.write_to_completion_cache('human_id', human_id)
+
+ @property
+ def human_id(self):
+ """Subclasses may override this provide a pretty ID which can be used
+ for bash completion.
+ """
+ if 'name' in self.__dict__ and self.HUMAN_ID:
+ return utils.slugify(self.name)
+ return None
+
+ def _add_details(self, info):
+ for (k, v) in info.iteritems():
+ try:
+ setattr(self, k, v)
+ except AttributeError:
+ # In this case we already defined the attribute on the class
+ pass
+
+ def __getattr__(self, k):
+ if k not in self.__dict__:
+ #NOTE(bcwaldon): disallow lazy-loading if already loaded once
+ if not self.is_loaded():
+ self.get()
+ return self.__getattr__(k)
+
+ raise AttributeError(k)
+ else:
+ return self.__dict__[k]
+
+ def __repr__(self):
+ reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
+ k != 'manager')
+ info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
+ return "<%s %s>" % (self.__class__.__name__, info)
+
+ def get(self):
+ # set_loaded() first ... so if we have to bail, we know we tried.
+ self.set_loaded(True)
+ if not hasattr(self.manager, 'get'):
+ return
+
+ new = self.manager.get(self.id)
+ if new:
+ self._add_details(new._info)
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return False
+ if hasattr(self, 'id') and hasattr(other, 'id'):
+ return self.id == other.id
+ return self._info == other._info
+
+ def is_loaded(self):
+ return self._loaded
+
+ def set_loaded(self, val):
+ self._loaded = val
diff --git a/cinderclient/client.py b/cinderclient/client.py
new file mode 100644
index 0000000..278e922
--- /dev/null
+++ b/cinderclient/client.py
@@ -0,0 +1,330 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Piston Cloud Computing, Inc.
+
+# All Rights Reserved.
+"""
+OpenStack Client interface. Handles the REST calls and responses.
+"""
+
+import httplib2
+import logging
+import os
+import urlparse
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+# Python 2.5 compat fix
+if not hasattr(urlparse, 'parse_qsl'):
+ import cgi
+ urlparse.parse_qsl = cgi.parse_qsl
+
+from cinderclient import exceptions
+from cinderclient import service_catalog
+from cinderclient import utils
+
+
+_logger = logging.getLogger(__name__)
+if 'CINDERCLIENT_DEBUG' in os.environ and os.environ['CINDERCLIENT_DEBUG']:
+ ch = logging.StreamHandler()
+ _logger.setLevel(logging.DEBUG)
+ _logger.addHandler(ch)
+
+
+class HTTPClient(httplib2.Http):
+
+ USER_AGENT = 'python-cinderclient'
+
+ def __init__(self, user, password, projectid, auth_url, insecure=False,
+ timeout=None, proxy_tenant_id=None,
+ proxy_token=None, region_name=None,
+ endpoint_type='publicURL', service_type=None,
+ service_name=None, volume_service_name=None):
+ super(HTTPClient, self).__init__(timeout=timeout)
+ self.user = user
+ self.password = password
+ self.projectid = projectid
+ self.auth_url = auth_url.rstrip('/')
+ self.version = 'v1'
+ self.region_name = region_name
+ self.endpoint_type = endpoint_type
+ self.service_type = service_type
+ self.service_name = service_name
+ self.volume_service_name = volume_service_name
+
+ self.management_url = None
+ 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
+
+ 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):
+ 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 >= 400:
+ raise exceptions.from_response(resp, body)
+
+ return resp, body
+
+ def _cs_request(self, url, method, **kwargs):
+ if not self.management_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.projectid:
+ kwargs['headers']['X-Auth-Project-Id'] = self.projectid
+
+ resp, body = self.request(self.management_url + url, method,
+ **kwargs)
+ return resp, body
+ except exceptions.Unauthorized, ex:
+ try:
+ self.authenticate()
+ resp, body = self.request(self.management_url + url, method,
+ **kwargs)
+ return resp, body
+ except exceptions.Unauthorized:
+ raise ex
+
+ def get(self, url, **kwargs):
+ return self._cs_request(url, 'GET', **kwargs)
+
+ def post(self, url, **kwargs):
+ return self._cs_request(url, 'POST', **kwargs)
+
+ def put(self, url, **kwargs):
+ return self._cs_request(url, 'PUT', **kwargs)
+
+ def delete(self, url, **kwargs):
+ return self._cs_request(url, 'DELETE', **kwargs)
+
+ def _extract_service_catalog(self, url, resp, body, extract_token=True):
+ """See what the auth service told us and process the response.
+ We may get redirected to another site, fail or actually get
+ back a service catalog with a token and our endpoints."""
+
+ if resp.status == 200: # content must always present
+ try:
+ self.auth_url = url
+ self.service_catalog = \
+ service_catalog.ServiceCatalog(body)
+
+ if extract_token:
+ self.auth_token = self.service_catalog.get_token()
+
+ management_url = self.service_catalog.url_for(
+ attr='region',
+ filter_value=self.region_name,
+ endpoint_type=self.endpoint_type,
+ service_type=self.service_type,
+ service_name=self.service_name,
+ volume_service_name=self.volume_service_name,)
+ self.management_url = management_url.rstrip('/')
+ return None
+ except exceptions.AmbiguousEndpoints:
+ print "Found more than one valid endpoint. Use a more " \
+ "restrictive filter"
+ raise
+ except KeyError:
+ raise exceptions.AuthorizationFailure()
+ except exceptions.EndpointNotFound:
+ print "Could not find any suitable endpoint. Correct region?"
+ raise
+
+ elif resp.status == 305:
+ return resp['location']
+ else:
+ raise exceptions.from_response(resp, body)
+
+ def _fetch_endpoints_from_auth(self, url):
+ """We have a token, but don't know the final endpoint for
+ the region. We have to go back to the auth service and
+ ask again. This request requires an admin-level token
+ to work. The proxy token supplied could be from a low-level enduser.
+
+ We can't get this from the keystone service endpoint, we have to use
+ the admin endpoint.
+
+ This will overwrite our admin token with the user token.
+ """
+
+ # GET ...:5001/v2.0/tokens/#####/endpoints
+ url = '/'.join([url, 'tokens', '%s?belongsTo=%s'
+ % (self.proxy_token, self.proxy_tenant_id)])
+ _logger.debug("Using Endpoint URL: %s" % url)
+ resp, body = self.request(url, "GET",
+ headers={'X-Auth_Token': self.auth_token})
+ return self._extract_service_catalog(url, resp, body,
+ extract_token=False)
+
+ def authenticate(self):
+ magic_tuple = urlparse.urlsplit(self.auth_url)
+ scheme, netloc, path, query, frag = magic_tuple
+ port = magic_tuple.port
+ if port is None:
+ port = 80
+ path_parts = path.split('/')
+ for part in path_parts:
+ if len(part) > 0 and part[0] == 'v':
+ self.version = part
+ break
+
+ # TODO(sandy): Assume admin endpoint is 35357 for now.
+ # Ideally this is going to have to be provided by the service catalog.
+ new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,))
+ admin_url = urlparse.urlunsplit(
+ (scheme, new_netloc, path, query, frag))
+
+ auth_url = self.auth_url
+ if self.version == "v2.0":
+ while auth_url:
+ if "CINDER_RAX_AUTH" in os.environ:
+ auth_url = self._rax_auth(auth_url)
+ else:
+ auth_url = self._v2_auth(auth_url)
+
+ # Are we acting on behalf of another user via an
+ # existing token? If so, our actual endpoints may
+ # be different than that of the admin token.
+ if self.proxy_token:
+ self._fetch_endpoints_from_auth(admin_url)
+ # Since keystone no longer returns the user token
+ # with the endpoints any more, we need to replace
+ # our service account token with the user token.
+ self.auth_token = self.proxy_token
+ else:
+ try:
+ while auth_url:
+ auth_url = self._v1_auth(auth_url)
+ # In some configurations cinder makes redirection to
+ # v2.0 keystone endpoint. Also, new location does not contain
+ # real endpoint, only hostname and port.
+ except exceptions.AuthorizationFailure:
+ if auth_url.find('v2.0') < 0:
+ auth_url = auth_url + '/v2.0'
+ self._v2_auth(auth_url)
+
+ def _v1_auth(self, url):
+ if self.proxy_token:
+ raise exceptions.NoTokenLookupException()
+
+ headers = {'X-Auth-User': self.user,
+ 'X-Auth-Key': self.password}
+ if self.projectid:
+ headers['X-Auth-Project-Id'] = self.projectid
+
+ resp, body = self.request(url, 'GET', headers=headers)
+ if resp.status in (200, 204): # in some cases we get No Content
+ try:
+ mgmt_header = 'x-server-management-url'
+ self.management_url = resp[mgmt_header].rstrip('/')
+ self.auth_token = resp['x-auth-token']
+ self.auth_url = url
+ except KeyError:
+ raise exceptions.AuthorizationFailure()
+ elif resp.status == 305:
+ return resp['location']
+ else:
+ raise exceptions.from_response(resp, body)
+
+ def _v2_auth(self, url):
+ """Authenticate against a v2.0 auth service."""
+ body = {"auth": {
+ "passwordCredentials": {"username": self.user,
+ "password": self.password}}}
+
+ if self.projectid:
+ body['auth']['tenantName'] = self.projectid
+
+ self._authenticate(url, body)
+
+ def _rax_auth(self, url):
+ """Authenticate against the Rackspace auth service."""
+ body = {"auth": {
+ "RAX-KSKEY:apiKeyCredentials": {
+ "username": self.user,
+ "apiKey": self.password,
+ "tenantName": self.projectid}}}
+
+ self._authenticate(url, body)
+
+ def _authenticate(self, url, body):
+ """Authenticate and extract the service catalog."""
+ token_url = url + "/tokens"
+
+ # Make sure we follow redirects when trying to reach Keystone
+ tmp_follow_all_redirects = self.follow_all_redirects
+ self.follow_all_redirects = True
+
+ try:
+ resp, body = self.request(token_url, "POST", body=body)
+ finally:
+ self.follow_all_redirects = tmp_follow_all_redirects
+
+ return self._extract_service_catalog(url, resp, body)
+
+
+def get_client_class(version):
+ version_map = {
+ '1': 'cinderclient.v1.client.Client',
+ }
+ try:
+ client_path = version_map[str(version)]
+ except (KeyError, ValueError):
+ msg = "Invalid client version '%s'. must be one of: %s" % (
+ (version, ', '.join(version_map.keys())))
+ raise exceptions.UnsupportedVersion(msg)
+
+ return utils.import_class(client_path)
+
+
+def Client(version, *args, **kwargs):
+ client_class = get_client_class(version)
+ return client_class(*args, **kwargs)
diff --git a/cinderclient/exceptions.py b/cinderclient/exceptions.py
new file mode 100644
index 0000000..91bf30e
--- /dev/null
+++ b/cinderclient/exceptions.py
@@ -0,0 +1,146 @@
+# Copyright 2010 Jacob Kaplan-Moss
+"""
+Exception definitions.
+"""
+
+
+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"
+
+
+# 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])
+
+
+def from_response(response, body):
+ """
+ Return an instance of an ClientException or subclass
+ based on an httplib2 response.
+
+ Usage::
+
+ resp, body = http.request(...)
+ if resp.status != 200:
+ raise exception_from_response(resp, body)
+ """
+ cls = _code_map.get(response.status, ClientException)
+ request_id = response.get('x-compute-request-id')
+ 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,
+ request_id=request_id)
+ else:
+ return cls(code=response.status, request_id=request_id)
diff --git a/cinderclient/extension.py b/cinderclient/extension.py
new file mode 100644
index 0000000..ced67f0
--- /dev/null
+++ b/cinderclient/extension.py
@@ -0,0 +1,39 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from cinderclient import base
+from cinderclient import utils
+
+
+class Extension(utils.HookableMixin):
+ """Extension descriptor."""
+
+ SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
+
+ def __init__(self, name, module):
+ self.name = name
+ self.module = module
+ self._parse_extension_module()
+
+ def _parse_extension_module(self):
+ self.manager_class = None
+ for attr_name, attr_value in self.module.__dict__.items():
+ if attr_name in self.SUPPORTED_HOOKS:
+ self.add_hook(attr_name, attr_value)
+ elif utils.safe_issubclass(attr_value, base.Manager):
+ self.manager_class = attr_value
+
+ def __repr__(self):
+ return "<Extension '%s'>" % self.name
diff --git a/cinderclient/service_catalog.py b/cinderclient/service_catalog.py
new file mode 100644
index 0000000..a2c8b37
--- /dev/null
+++ b/cinderclient/service_catalog.py
@@ -0,0 +1,77 @@
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011, Piston Cloud Computing, Inc.
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import cinderclient.exceptions
+
+
+class ServiceCatalog(object):
+ """Helper methods for dealing with a Keystone Service Catalog."""
+
+ def __init__(self, resource_dict):
+ self.catalog = resource_dict
+
+ def get_token(self):
+ return self.catalog['access']['token']['id']
+
+ def url_for(self, attr=None, filter_value=None,
+ service_type=None, endpoint_type='publicURL',
+ service_name=None, volume_service_name=None):
+ """Fetch the public URL from the Compute service for
+ a particular endpoint attribute. If none given, return
+ the first. See tests for sample service catalog."""
+ matching_endpoints = []
+ if 'endpoints' in self.catalog:
+ # We have a bastardized service catalog. Treat it special. :/
+ for endpoint in self.catalog['endpoints']:
+ if not filter_value or endpoint[attr] == filter_value:
+ matching_endpoints.append(endpoint)
+ if not matching_endpoints:
+ raise cinderclient.exceptions.EndpointNotFound()
+
+ # We don't always get a service catalog back ...
+ if not 'serviceCatalog' in self.catalog['access']:
+ return None
+
+ # Full catalog ...
+ catalog = self.catalog['access']['serviceCatalog']
+
+ for service in catalog:
+ if service.get("type") != service_type:
+ continue
+
+ if (service_name and service_type == 'compute' and
+ service.get('name') != service_name):
+ continue
+
+ if (volume_service_name and service_type == 'volume' and
+ service.get('name') != volume_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 cinderclient.exceptions.EndpointNotFound()
+ elif len(matching_endpoints) > 1:
+ raise cinderclient.exceptions.AmbiguousEndpoints(
+ endpoints=matching_endpoints)
+ else:
+ return matching_endpoints[0][endpoint_type]
diff --git a/cinderclient/shell.py b/cinderclient/shell.py
new file mode 100644
index 0000000..25d5536
--- /dev/null
+++ b/cinderclient/shell.py
@@ -0,0 +1,435 @@
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Command-line interface to the OpenStack Volume API.
+"""
+
+import argparse
+import glob
+import httplib2
+import imp
+import itertools
+import os
+import pkgutil
+import sys
+import logging
+
+from cinderclient import client
+from cinderclient import exceptions as exc
+import cinderclient.extension
+from cinderclient import utils
+from cinderclient.v1 import shell as shell_v1
+
+DEFAULT_OS_VOLUME_API_VERSION = "1"
+DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
+DEFAULT_CINDER_SERVICE_TYPE = 'compute'
+
+logger = logging.getLogger(__name__)
+
+
+class CinderClientArgumentParser(argparse.ArgumentParser):
+
+ def __init__(self, *args, **kwargs):
+ super(CinderClientArgumentParser, self).__init__(*args, **kwargs)
+
+ def error(self, message):
+ """error(message: string)
+
+ Prints a usage message incorporating the message to stderr and
+ exits.
+ """
+ self.print_usage(sys.stderr)
+ #FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
+ choose_from = ' (choose from'
+ progparts = self.prog.partition(' ')
+ self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
+ " for more information.\n" %
+ {'errmsg': message.split(choose_from)[0],
+ 'mainp': progparts[0],
+ 'subp': progparts[2]})
+
+
+class OpenStackCinderShell(object):
+
+ def get_base_parser(self):
+ parser = CinderClientArgumentParser(
+ prog='cinder',
+ description=__doc__.strip(),
+ epilog='See "cinder help COMMAND" '\
+ 'for help on a specific command.',
+ add_help=False,
+ formatter_class=OpenStackHelpFormatter,
+ )
+
+ # Global arguments
+ parser.add_argument('-h', '--help',
+ action='store_true',
+ help=argparse.SUPPRESS,
+ )
+
+ parser.add_argument('--debug',
+ default=False,
+ action='store_true',
+ help="Print debugging output")
+
+ parser.add_argument('--os_username',
+ default=utils.env('OS_USERNAME', 'CINDER_USERNAME'),
+ help='Defaults to env[OS_USERNAME].')
+
+ parser.add_argument('--os_password',
+ default=utils.env('OS_PASSWORD', 'CINDER_PASSWORD'),
+ help='Defaults to env[OS_PASSWORD].')
+
+ parser.add_argument('--os_tenant_name',
+ default=utils.env('OS_TENANT_NAME', 'CINDER_PROJECT_ID'),
+ help='Defaults to env[OS_TENANT_NAME].')
+
+ parser.add_argument('--os_auth_url',
+ default=utils.env('OS_AUTH_URL', 'CINDER_URL'),
+ help='Defaults to env[OS_AUTH_URL].')
+
+ parser.add_argument('--os_region_name',
+ default=utils.env('OS_REGION_NAME', 'CINDER_REGION_NAME'),
+ help='Defaults to env[OS_REGION_NAME].')
+
+ parser.add_argument('--service_type',
+ help='Defaults to compute for most actions')
+
+ parser.add_argument('--service_name',
+ default=utils.env('CINDER_SERVICE_NAME'),
+ help='Defaults to env[CINDER_SERVICE_NAME]')
+
+ parser.add_argument('--volume_service_name',
+ default=utils.env('CINDER_VOLUME_SERVICE_NAME'),
+ help='Defaults to env[CINDER_VOLUME_SERVICE_NAME]')
+
+ parser.add_argument('--endpoint_type',
+ default=utils.env('CINDER_ENDPOINT_TYPE',
+ default=DEFAULT_CINDER_ENDPOINT_TYPE),
+ help='Defaults to env[CINDER_ENDPOINT_TYPE] or '
+ + DEFAULT_CINDER_ENDPOINT_TYPE + '.')
+
+ parser.add_argument('--os_volume_api_version',
+ default=utils.env('OS_VOLUME_API_VERSION',
+ default=DEFAULT_OS_VOLUME_API_VERSION),
+ help='Accepts 1, defaults to env[OS_VOLUME_API_VERSION].')
+
+ parser.add_argument('--insecure',
+ default=utils.env('CINDERCLIENT_INSECURE', default=False),
+ action='store_true',
+ help=argparse.SUPPRESS)
+
+ # FIXME(dtroyer): The args below are here for diablo compatibility,
+ # remove them in folsum cycle
+
+ # alias for --os_username, left in for backwards compatibility
+ parser.add_argument('--username',
+ help='Deprecated')
+
+ # alias for --os_region_name, left in for backwards compatibility
+ parser.add_argument('--region_name',
+ help='Deprecated')
+
+ # alias for --os_password, left in for backwards compatibility
+ parser.add_argument('--apikey', '--password', dest='apikey',
+ default=utils.env('CINDER_API_KEY'),
+ help='Deprecated')
+
+ # alias for --os_tenant_name, left in for backward compatibility
+ parser.add_argument('--projectid', '--tenant_name', dest='projectid',
+ default=utils.env('CINDER_PROJECT_ID'),
+ help='Deprecated')
+
+ # alias for --os_auth_url, left in for backward compatibility
+ parser.add_argument('--url', '--auth_url', dest='url',
+ default=utils.env('CINDER_URL'),
+ help='Deprecated')
+
+ return parser
+
+ def get_subcommand_parser(self, version):
+ parser = self.get_base_parser()
+
+ self.subcommands = {}
+ subparsers = parser.add_subparsers(metavar='<subcommand>')
+
+ try:
+ actions_module = {
+ '1.1': shell_v1,
+ '2': shell_v1,
+ }[version]
+ except KeyError:
+ actions_module = shell_v1
+
+ self._find_actions(subparsers, actions_module)
+ self._find_actions(subparsers, self)
+
+ for extension in self.extensions:
+ self._find_actions(subparsers, extension.module)
+
+ self._add_bash_completion_subparser(subparsers)
+
+ return parser
+
+ def _discover_extensions(self, version):
+ extensions = []
+ for name, module in itertools.chain(
+ self._discover_via_python_path(version),
+ self._discover_via_contrib_path(version)):
+
+ extension = cinderclient.extension.Extension(name, module)
+ extensions.append(extension)
+
+ return extensions
+
+ def _discover_via_python_path(self, version):
+ for (module_loader, name, ispkg) in pkgutil.iter_modules():
+ if name.endswith('python_cinderclient_ext'):
+ if not hasattr(module_loader, 'load_module'):
+ # Python 2.6 compat: actually get an ImpImporter obj
+ module_loader = module_loader.find_module(name)
+
+ module = module_loader.load_module(name)
+ yield name, module
+
+ def _discover_via_contrib_path(self, version):
+ module_path = os.path.dirname(os.path.abspath(__file__))
+ version_str = "v%s" % version.replace('.', '_')
+ ext_path = os.path.join(module_path, version_str, 'contrib')
+ ext_glob = os.path.join(ext_path, "*.py")
+
+ for ext_path in glob.iglob(ext_glob):
+ name = os.path.basename(ext_path)[:-3]
+
+ if name == "__init__":
+ continue
+
+ module = imp.load_source(name, ext_path)
+ yield name, module
+
+ def _add_bash_completion_subparser(self, subparsers):
+ subparser = subparsers.add_parser('bash_completion',
+ add_help=False,
+ formatter_class=OpenStackHelpFormatter
+ )
+ self.subcommands['bash_completion'] = subparser
+ subparser.set_defaults(func=self.do_bash_completion)
+
+ def _find_actions(self, subparsers, actions_module):
+ for attr in (a for a in dir(actions_module) if a.startswith('do_')):
+ # I prefer to be hypen-separated instead of underscores.
+ command = attr[3:].replace('_', '-')
+ callback = getattr(actions_module, attr)
+ desc = callback.__doc__ or ''
+ help = desc.strip().split('\n')[0]
+ arguments = getattr(callback, 'arguments', [])
+
+ subparser = subparsers.add_parser(command,
+ help=help,
+ description=desc,
+ add_help=False,
+ formatter_class=OpenStackHelpFormatter
+ )
+ subparser.add_argument('-h', '--help',
+ action='help',
+ help=argparse.SUPPRESS,
+ )
+ self.subcommands[command] = subparser
+ for (args, kwargs) in arguments:
+ subparser.add_argument(*args, **kwargs)
+ subparser.set_defaults(func=callback)
+
+ def setup_debugging(self, debug):
+ if not debug:
+ return
+
+ streamhandler = logging.StreamHandler()
+ streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
+ streamhandler.setFormatter(logging.Formatter(streamformat))
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(streamhandler)
+
+ httplib2.debuglevel = 1
+
+ def main(self, argv):
+ # Parse args once to find version
+ parser = self.get_base_parser()
+ (options, args) = parser.parse_known_args(argv)
+ self.setup_debugging(options.debug)
+
+ # build available subcommands based on version
+ self.extensions = self._discover_extensions(
+ options.os_volume_api_version)
+ self._run_extension_hooks('__pre_parse_args__')
+
+ subcommand_parser = self.get_subcommand_parser(
+ options.os_volume_api_version)
+ self.parser = subcommand_parser
+
+ if options.help and len(args) == 0:
+ subcommand_parser.print_help()
+ return 0
+
+ args = subcommand_parser.parse_args(argv)
+ self._run_extension_hooks('__post_parse_args__', args)
+
+ # Short-circuit and deal with help right away.
+ if args.func == self.do_help:
+ self.do_help(args)
+ return 0
+ elif args.func == self.do_bash_completion:
+ self.do_bash_completion(args)
+ return 0
+
+ (os_username, os_password, os_tenant_name, os_auth_url,
+ os_region_name, endpoint_type, insecure,
+ service_type, service_name, volume_service_name,
+ username, apikey, projectid, url, region_name) = (
+ args.os_username, args.os_password,
+ args.os_tenant_name, args.os_auth_url,
+ args.os_region_name, args.endpoint_type,
+ args.insecure, args.service_type, args.service_name,
+ args.volume_service_name, args.username,
+ args.apikey, args.projectid,
+ args.url, args.region_name)
+
+ if not endpoint_type:
+ endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE
+
+ if not service_type:
+ service_type = DEFAULT_CINDER_SERVICE_TYPE
+ service_type = utils.get_service_type(args.func) or service_type
+
+ #FIXME(usrleon): Here should be restrict for project id same as
+ # for os_username or os_password but for compatibility it is not.
+
+ if not utils.isunauthenticated(args.func):
+ if not os_username:
+ if not username:
+ raise exc.CommandError("You must provide a username "
+ "via either --os_username or env[OS_USERNAME]")
+ else:
+ os_username = username
+
+ if not os_password:
+ if not apikey:
+ raise exc.CommandError("You must provide a password "
+ "via either --os_password or via "
+ "env[OS_PASSWORD]")
+ else:
+ os_password = apikey
+
+ if not os_tenant_name:
+ if not projectid:
+ raise exc.CommandError("You must provide a tenant name "
+ "via either --os_tenant_name or "
+ "env[OS_TENANT_NAME]")
+ else:
+ os_tenant_name = projectid
+
+ if not os_auth_url:
+ if not url:
+ raise exc.CommandError("You must provide an auth url "
+ "via either --os_auth_url or env[OS_AUTH_URL]")
+ else:
+ os_auth_url = url
+
+ if not os_region_name and region_name:
+ os_region_name = region_name
+
+ if not os_tenant_name:
+ raise exc.CommandError("You must provide a tenant name "
+ "via either --os_tenant_name or env[OS_TENANT_NAME]")
+
+ if not os_auth_url:
+ raise exc.CommandError("You must provide an auth url "
+ "via either --os_auth_url or env[OS_AUTH_URL]")
+
+ self.cs = client.Client(options.os_volume_api_version, os_username,
+ os_password, os_tenant_name, os_auth_url, insecure,
+ region_name=os_region_name, endpoint_type=endpoint_type,
+ extensions=self.extensions, service_type=service_type,
+ service_name=service_name,
+ volume_service_name=volume_service_name)
+
+ try:
+ if not utils.isunauthenticated(args.func):
+ self.cs.authenticate()
+ except exc.Unauthorized:
+ raise exc.CommandError("Invalid OpenStack Nova credentials.")
+ except exc.AuthorizationFailure:
+ raise exc.CommandError("Unable to authorize user")
+
+ args.func(self.cs, args)
+
+ def _run_extension_hooks(self, hook_type, *args, **kwargs):
+ """Run hooks for all registered extensions."""
+ for extension in self.extensions:
+ extension.run_hooks(hook_type, *args, **kwargs)
+
+ def do_bash_completion(self, args):
+ """
+ Prints all of the commands and options to stdout so that the
+ cinder.bash_completion script doesn't have to hard code them.
+ """
+ commands = set()
+ options = set()
+ for sc_str, sc in self.subcommands.items():
+ commands.add(sc_str)
+ for option in sc._optionals._option_string_actions.keys():
+ options.add(option)
+
+ commands.remove('bash-completion')
+ commands.remove('bash_completion')
+ print ' '.join(commands | options)
+
+ @utils.arg('command', metavar='<subcommand>', nargs='?',
+ help='Display help for <subcommand>')
+ def do_help(self, args):
+ """
+ Display help about this program or one of its subcommands.
+ """
+ if args.command:
+ if args.command in self.subcommands:
+ self.subcommands[args.command].print_help()
+ else:
+ raise exc.CommandError("'%s' is not a valid subcommand" %
+ args.command)
+ else:
+ self.parser.print_help()
+
+
+# I'm picky about my shell help.
+class OpenStackHelpFormatter(argparse.HelpFormatter):
+ def start_section(self, heading):
+ # Title-case the headings
+ heading = '%s%s' % (heading[0].upper(), heading[1:])
+ super(OpenStackHelpFormatter, self).start_section(heading)
+
+
+def main():
+ try:
+ OpenStackCinderShell().main(sys.argv[1:])
+
+ except Exception, e:
+ logger.debug(e, exc_info=1)
+ print >> sys.stderr, "ERROR: %s" % str(e)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/cinderclient/utils.py b/cinderclient/utils.py
new file mode 100644
index 0000000..52f4da9
--- /dev/null
+++ b/cinderclient/utils.py
@@ -0,0 +1,261 @@
+import os
+import re
+import sys
+import uuid
+
+import prettytable
+
+from cinderclient import exceptions
+
+
+def arg(*args, **kwargs):
+ """Decorator for CLI args."""
+ def _decorator(func):
+ add_arg(func, *args, **kwargs)
+ return func
+ return _decorator
+
+
+def env(*vars, **kwargs):
+ """
+ returns the first environment variable set
+ if none are non-empty, defaults to '' or keyword arg default
+ """
+ for v in vars:
+ value = os.environ.get(v, None)
+ if value:
+ return value
+ return kwargs.get('default', '')
+
+
+def add_arg(f, *args, **kwargs):
+ """Bind CLI arguments to a shell.py `do_foo` function."""
+
+ if not hasattr(f, 'arguments'):
+ f.arguments = []
+
+ # NOTE(sirp): avoid dups that can occur when the module is shared across
+ # tests.
+ if (args, kwargs) not in f.arguments:
+ # Because of the sematics of decorator composition if we just append
+ # to the options list positional options will appear to be backwards.
+ f.arguments.insert(0, (args, kwargs))
+
+
+def add_resource_manager_extra_kwargs_hook(f, hook):
+ """Adds hook to bind CLI arguments to ResourceManager calls.
+
+ The `do_foo` calls in shell.py will receive CLI args and then in turn pass
+ them through to the ResourceManager. Before passing through the args, the
+ hooks registered here will be called, giving us a chance to add extra
+ kwargs (taken from the command-line) to what's passed to the
+ ResourceManager.
+ """
+ if not hasattr(f, 'resource_manager_kwargs_hooks'):
+ f.resource_manager_kwargs_hooks = []
+
+ names = [h.__name__ for h in f.resource_manager_kwargs_hooks]
+ if hook.__name__ not in names:
+ f.resource_manager_kwargs_hooks.append(hook)
+
+
+def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
+ """Return extra_kwargs by calling resource manager kwargs hooks."""
+ hooks = getattr(f, "resource_manager_kwargs_hooks", [])
+ extra_kwargs = {}
+ for hook in hooks:
+ hook_name = hook.__name__
+ hook_kwargs = hook(args)
+
+ conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
+ if conflicting_keys and not allow_conflicts:
+ raise Exception("Hook '%(hook_name)s' is attempting to redefine"
+ " attributes '%(conflicting_keys)s'" % locals())
+
+ extra_kwargs.update(hook_kwargs)
+
+ return extra_kwargs
+
+
+def unauthenticated(f):
+ """
+ Adds 'unauthenticated' attribute to decorated function.
+ Usage:
+ @unauthenticated
+ def mymethod(f):
+ ...
+ """
+ f.unauthenticated = True
+ return f
+
+
+def isunauthenticated(f):
+ """
+ Checks to see if the function is marked as not requiring authentication
+ with the @unauthenticated decorator. Returns True if decorator is
+ set to True, False otherwise.
+ """
+ return getattr(f, 'unauthenticated', False)
+
+
+def service_type(stype):
+ """
+ Adds 'service_type' attribute to decorated function.
+ Usage:
+ @service_type('volume')
+ def mymethod(f):
+ ...
+ """
+ def inner(f):
+ f.service_type = stype
+ return f
+ return inner
+
+
+def get_service_type(f):
+ """
+ Retrieves service type from function
+ """
+ return getattr(f, 'service_type', None)
+
+
+def pretty_choice_list(l):
+ return ', '.join("'%s'" % i for i in l)
+
+
+def print_list(objs, fields, formatters={}):
+ mixed_case_fields = ['serverId']
+ pt = prettytable.PrettyTable([f for f in fields], caching=False)
+ pt.aligns = ['l' for f in fields]
+
+ for o in objs:
+ row = []
+ for field in fields:
+ if field in formatters:
+ row.append(formatters[field](o))
+ else:
+ if field in mixed_case_fields:
+ field_name = field.replace(' ', '_')
+ else:
+ field_name = field.lower().replace(' ', '_')
+ data = getattr(o, field_name, '')
+ row.append(data)
+ pt.add_row(row)
+
+ print pt.get_string(sortby=fields[0])
+
+
+def print_dict(d, property="Property"):
+ pt = prettytable.PrettyTable([property, 'Value'], caching=False)
+ pt.aligns = ['l', 'l']
+ [pt.add_row(list(r)) for r in d.iteritems()]
+ print pt.get_string(sortby=property)
+
+
+def find_resource(manager, name_or_id):
+ """Helper for the _find_* methods."""
+ # first try to get entity as integer id
+ try:
+ if isinstance(name_or_id, int) or name_or_id.isdigit():
+ return manager.get(int(name_or_id))
+ except exceptions.NotFound:
+ pass
+
+ # now try to get entity as uuid
+ try:
+ uuid.UUID(str(name_or_id))
+ return manager.get(name_or_id)
+ except (ValueError, exceptions.NotFound):
+ pass
+
+ try:
+ try:
+ return manager.find(human_id=name_or_id)
+ except exceptions.NotFound:
+ pass
+
+ # finally try to find entity by name
+ try:
+ return manager.find(name=name_or_id)
+ except exceptions.NotFound:
+ try:
+ # Volumes does not have name, but display_name
+ return manager.find(display_name=name_or_id)
+ except exceptions.NotFound:
+ msg = "No %s with a name or ID of '%s' exists." % \
+ (manager.resource_class.__name__.lower(), name_or_id)
+ raise exceptions.CommandError(msg)
+ except exceptions.NoUniqueMatch:
+ msg = ("Multiple %s matches found for '%s', use an ID to be more"
+ " specific." % (manager.resource_class.__name__.lower(),
+ name_or_id))
+ raise exceptions.CommandError(msg)
+
+
+def _format_servers_list_networks(server):
+ output = []
+ for (network, addresses) in server.networks.items():
+ if len(addresses) == 0:
+ continue
+ addresses_csv = ', '.join(addresses)
+ group = "%s=%s" % (network, addresses_csv)
+ output.append(group)
+
+ return '; '.join(output)
+
+
+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 safe_issubclass(*args):
+ """Like issubclass, but will just return False if not a class."""
+
+ try:
+ if issubclass(*args):
+ return True
+ except TypeError:
+ pass
+
+ return False
+
+
+def import_class(import_str):
+ """Returns a class from a string including module and class."""
+ mod_str, _sep, class_str = import_str.rpartition('.')
+ __import__(mod_str)
+ return getattr(sys.modules[mod_str], class_str)
+
+_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/cinderclient/v1/__init__.py b/cinderclient/v1/__init__.py
new file mode 100644
index 0000000..cecfacd
--- /dev/null
+++ b/cinderclient/v1/__init__.py
@@ -0,0 +1,17 @@
+# 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 cinderclient.v1.client import Client
diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py
new file mode 100644
index 0000000..cbee8ba
--- /dev/null
+++ b/cinderclient/v1/client.py
@@ -0,0 +1,71 @@
+from cinderclient import client
+from cinderclient.v1 import volumes
+from cinderclient.v1 import volume_snapshots
+from cinderclient.v1 import volume_types
+
+
+class Client(object):
+ """
+ Top-level object to access the OpenStack Compute API.
+
+ Create an instance with your creds::
+
+ >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL)
+
+ Then call methods on its managers::
+
+ >>> client.servers.list()
+ ...
+ >>> client.flavors.list()
+ ...
+
+ """
+
+ # FIXME(jesse): project_id isn't required to authenticate
+ def __init__(self, username, api_key, project_id, auth_url,
+ insecure=False, timeout=None, proxy_tenant_id=None,
+ proxy_token=None, region_name=None,
+ endpoint_type='publicURL', extensions=None,
+ service_type='compute', service_name=None,
+ volume_service_name=None):
+ # FIXME(comstud): Rename the api_key argument above when we
+ # know it's not being used as keyword argument
+ password = api_key
+
+ # extensions
+ self.volumes = volumes.VolumeManager(self)
+ self.volume_snapshots = volume_snapshots.SnapshotManager(self)
+ self.volume_types = volume_types.VolumeTypeManager(self)
+
+ # Add in any extensions...
+ if extensions:
+ for extension in extensions:
+ if extension.manager_class:
+ setattr(self, extension.name,
+ extension.manager_class(self))
+
+ self.client = client.HTTPClient(username,
+ password,
+ project_id,
+ auth_url,
+ insecure=insecure,
+ timeout=timeout,
+ proxy_token=proxy_token,
+ proxy_tenant_id=proxy_tenant_id,
+ region_name=region_name,
+ endpoint_type=endpoint_type,
+ service_type=service_type,
+ service_name=service_name,
+ volume_service_name=volume_service_name)
+
+ def authenticate(self):
+ """
+ Authenticate against the server.
+
+ Normally this is called automatically when you first access the API,
+ but you can call this method to force authentication right now.
+
+ Returns on success; raises :exc:`exceptions.Unauthorized` if the
+ credentials are wrong.
+ """
+ self.client.authenticate()
diff --git a/cinderclient/v1/contrib/__init__.py b/cinderclient/v1/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cinderclient/v1/contrib/__init__.py
diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py
new file mode 100644
index 0000000..6b8b7bb
--- /dev/null
+++ b/cinderclient/v1/shell.py
@@ -0,0 +1,241 @@
+# Copyright 2010 Jacob Kaplan-Moss
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import sys
+import time
+
+from cinderclient import utils
+
+
+def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
+ poll_period=5, show_progress=True):
+ """Block while an action is being performed, periodically printing
+ progress.
+ """
+ def print_progress(progress):
+ if show_progress:
+ msg = ('\rInstance %(action)s... %(progress)s%% complete'
+ % dict(action=action, progress=progress))
+ else:
+ msg = '\rInstance %(action)s...' % dict(action=action)
+
+ sys.stdout.write(msg)
+ sys.stdout.flush()
+
+ print
+ while True:
+ obj = poll_fn(obj_id)
+ status = obj.status.lower()
+ progress = getattr(obj, 'progress', None) or 0
+ if status in final_ok_states:
+ print_progress(100)
+ print "\nFinished"
+ break
+ elif status == "error":
+ print "\nError %(action)s instance" % locals()
+ break
+ else:
+ print_progress(progress)
+ time.sleep(poll_period)
+
+
+def _find_volume(cs, volume):
+ """Get a volume by ID."""
+ return utils.find_resource(cs.volumes, volume)
+
+
+def _find_volume_snapshot(cs, snapshot):
+ """Get a volume snapshot by ID."""
+ return utils.find_resource(cs.volume_snapshots, snapshot)
+
+
+def _print_volume(cs, volume):
+ utils.print_dict(volume._info)
+
+
+def _print_volume_snapshot(cs, snapshot):
+ utils.print_dict(snapshot._info)
+
+
+def _translate_volume_keys(collection):
+ convert = [('displayName', 'display_name'), ('volumeType', 'volume_type')]
+ for item in collection:
+ keys = item.__dict__.keys()
+ for from_key, to_key in convert:
+ if from_key in keys and to_key not in keys:
+ setattr(item, to_key, item._info[from_key])
+
+
+def _translate_volume_snapshot_keys(collection):
+ convert = [('displayName', 'display_name'), ('volumeId', 'volume_id')]
+ for item in collection:
+ keys = item.__dict__.keys()
+ for from_key, to_key in convert:
+ if from_key in keys and to_key not in keys:
+ setattr(item, to_key, item._info[from_key])
+
+
+@utils.service_type('volume')
+def do_list(cs, args):
+ """List all the volumes."""
+ volumes = cs.volumes.list()
+ _translate_volume_keys(volumes)
+
+ # Create a list of servers to which the volume is attached
+ for vol in volumes:
+ servers = [s.get('server_id') for s in vol.attachments]
+ setattr(vol, 'attached_to', ','.join(map(str, servers)))
+ utils.print_list(volumes, ['ID', 'Status', 'Display Name',
+ 'Size', 'Volume Type', 'Attached to'])
+
+
+@utils.arg('volume', metavar='<volume>', help='ID of the volume.')
+@utils.service_type('volume')
+def do_show(cs, args):
+ """Show details about a volume."""
+ volume = _find_volume(cs, args.volume)
+ _print_volume(cs, volume)
+
+
+@utils.arg('size',
+ metavar='<size>',
+ type=int,
+ help='Size of volume in GB')
+@utils.arg('--snapshot_id',
+ metavar='<snapshot_id>',
+ help='Optional snapshot id to create the volume from. (Default=None)',
+ default=None)
+@utils.arg('--display_name', metavar='<display_name>',
+ help='Optional volume name. (Default=None)',
+ default=None)
+@utils.arg('--display_description', metavar='<display_description>',
+ help='Optional volume description. (Default=None)',
+ default=None)
+@utils.arg('--volume_type',
+ metavar='<volume_type>',
+ help='Optional volume type. (Default=None)',
+ default=None)
+@utils.service_type('volume')
+def do_create(cs, args):
+ """Add a new volume."""
+ cs.volumes.create(args.size,
+ args.snapshot_id,
+ args.display_name,
+ args.display_description,
+ args.volume_type)
+
+
+@utils.arg('volume', metavar='<volume>', help='ID of the volume to delete.')
+@utils.service_type('volume')
+def do_delete(cs, args):
+ """Remove a volume."""
+ volume = _find_volume(cs, args.volume)
+ volume.delete()
+
+
+@utils.service_type('volume')
+def do_snapshot_list(cs, args):
+ """List all the snapshots."""
+ snapshots = cs.volume_snapshots.list()
+ _translate_volume_snapshot_keys(snapshots)
+ utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name',
+ 'Size'])
+
+
+@utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.')
+@utils.service_type('volume')
+def do_snapshot_show(cs, args):
+ """Show details about a snapshot."""
+ snapshot = _find_volume_snapshot(cs, args.snapshot)
+ _print_volume_snapshot(cs, snapshot)
+
+
+@utils.arg('volume_id',
+ metavar='<volume_id>',
+ help='ID of the volume to snapshot')
+@utils.arg('--force',
+ metavar='<True|False>',
+ help='Optional flag to indicate whether to snapshot a volume even if its '
+ 'attached to an instance. (Default=False)',
+ default=False)
+@utils.arg('--display_name', metavar='<display_name>',
+ help='Optional snapshot name. (Default=None)',
+ default=None)
+@utils.arg('--display_description', metavar='<display_description>',
+ help='Optional snapshot description. (Default=None)',
+ default=None)
+@utils.service_type('volume')
+def do_snapshot_create(cs, args):
+ """Add a new snapshot."""
+ cs.volume_snapshots.create(args.volume_id,
+ args.force,
+ args.display_name,
+ args.display_description)
+
+
+@utils.arg('snapshot_id',
+ metavar='<snapshot_id>',
+ help='ID of the snapshot to delete.')
+@utils.service_type('volume')
+def do_snapshot_delete(cs, args):
+ """Remove a snapshot."""
+ snapshot = _find_volume_snapshot(cs, args.snapshot_id)
+ snapshot.delete()
+
+
+def _print_volume_type_list(vtypes):
+ utils.print_list(vtypes, ['ID', 'Name'])
+
+
+@utils.service_type('volume')
+def do_type_list(cs, args):
+ """Print a list of available 'volume types'."""
+ vtypes = cs.volume_types.list()
+ _print_volume_type_list(vtypes)
+
+
+@utils.arg('name',
+ metavar='<name>',
+ help="Name of the new flavor")
+@utils.service_type('volume')
+def do_type_create(cs, args):
+ """Create a new volume type."""
+ vtype = cs.volume_types.create(args.name)
+ _print_volume_type_list([vtype])
+
+
+@utils.arg('id',
+ metavar='<id>',
+ help="Unique ID of the volume type to delete")
+@utils.service_type('volume')
+def do_type_delete(cs, args):
+ """Delete a specific flavor"""
+ cs.volume_types.delete(args.id)
+
+
+def do_endpoints(cs, args):
+ """Discover endpoints that get returned from the authenticate services"""
+ catalog = cs.client.service_catalog.catalog
+ for e in catalog['access']['serviceCatalog']:
+ utils.print_dict(e['endpoints'][0], e['name'])
+
+
+def do_credentials(cs, args):
+ """Show user credentials returned from auth"""
+ catalog = cs.client.service_catalog.catalog
+ utils.print_dict(catalog['access']['user'], "User Credentials")
+ utils.print_dict(catalog['access']['token'], "Token")
diff --git a/cinderclient/v1/volume_snapshots.py b/cinderclient/v1/volume_snapshots.py
new file mode 100644
index 0000000..fa6c4b4
--- /dev/null
+++ b/cinderclient/v1/volume_snapshots.py
@@ -0,0 +1,88 @@
+# Copyright 2011 Denali Systems, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Volume snapshot interface (1.1 extension).
+"""
+
+from cinderclient import base
+
+
+class Snapshot(base.Resource):
+ """
+ A Snapshot is a point-in-time snapshot of an openstack volume.
+ """
+ def __repr__(self):
+ return "<Snapshot: %s>" % self.id
+
+ def delete(self):
+ """
+ Delete this snapshot.
+ """
+ self.manager.delete(self)
+
+
+class SnapshotManager(base.ManagerWithFind):
+ """
+ Manage :class:`Snapshot` resources.
+ """
+ resource_class = Snapshot
+
+ def create(self, volume_id, force=False,
+ display_name=None, display_description=None):
+
+ """
+ Create a snapshot of the given volume.
+
+ :param volume_id: The ID of the volume to snapshot.
+ :param force: If force is True, create a snapshot even if the volume is
+ attached to an instance. Default is False.
+ :param display_name: Name of the snapshot
+ :param display_description: Description of the snapshot
+ :rtype: :class:`Snapshot`
+ """
+ body = {'snapshot': {'volume_id': volume_id,
+ 'force': force,
+ 'display_name': display_name,
+ 'display_description': display_description}}
+ return self._create('/snapshots', body, 'snapshot')
+
+ def get(self, snapshot_id):
+ """
+ Get a snapshot.
+
+ :param snapshot_id: The ID of the snapshot to get.
+ :rtype: :class:`Snapshot`
+ """
+ return self._get("/snapshots/%s" % snapshot_id, "snapshot")
+
+ def list(self, detailed=True):
+ """
+ Get a list of all snapshots.
+
+ :rtype: list of :class:`Snapshot`
+ """
+ if detailed is True:
+ return self._list("/snapshots/detail", "snapshots")
+ else:
+ return self._list("/snapshots", "snapshots")
+
+ def delete(self, snapshot):
+ """
+ Delete a snapshot.
+
+ :param snapshot: The :class:`Snapshot` to delete.
+ """
+ self._delete("/snapshots/%s" % base.getid(snapshot))
diff --git a/cinderclient/v1/volume_types.py b/cinderclient/v1/volume_types.py
new file mode 100644
index 0000000..e6d644d
--- /dev/null
+++ b/cinderclient/v1/volume_types.py
@@ -0,0 +1,77 @@
+# Copyright (c) 2011 Rackspace US, Inc.
+#
+# 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.
+
+
+"""
+Volume Type interface.
+"""
+
+from cinderclient import base
+
+
+class VolumeType(base.Resource):
+ """
+ A Volume Type is the type of volume to be created
+ """
+ def __repr__(self):
+ return "<Volume Type: %s>" % self.name
+
+
+class VolumeTypeManager(base.ManagerWithFind):
+ """
+ Manage :class:`VolumeType` resources.
+ """
+ resource_class = VolumeType
+
+ def list(self):
+ """
+ Get a list of all volume types.
+
+ :rtype: list of :class:`VolumeType`.
+ """
+ return self._list("/types", "volume_types")
+
+ def get(self, volume_type):
+ """
+ Get a specific volume type.
+
+ :param volume_type: The ID of the :class:`VolumeType` to get.
+ :rtype: :class:`VolumeType`
+ """
+ return self._get("/types/%s" % base.getid(volume_type), "volume_type")
+
+ def delete(self, volume_type):
+ """
+ Delete a specific volume_type.
+
+ :param volume_type: The ID of the :class:`VolumeType` to get.
+ """
+ self._delete("/types/%s" % base.getid(volume_type))
+
+ def create(self, name):
+ """
+ Create a volume type.
+
+ :param name: Descriptive name of the volume type
+ :rtype: :class:`VolumeType`
+ """
+
+ body = {
+ "volume_type": {
+ "name": name,
+ }
+ }
+
+ return self._create("/types", body, "volume_type")
diff --git a/cinderclient/v1/volumes.py b/cinderclient/v1/volumes.py
new file mode 100644
index 0000000..d465724
--- /dev/null
+++ b/cinderclient/v1/volumes.py
@@ -0,0 +1,135 @@
+# Copyright 2011 Denali Systems, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Volume interface (1.1 extension).
+"""
+
+from cinderclient import base
+
+
+class Volume(base.Resource):
+ """
+ A volume is an extra block level storage to the OpenStack instances.
+ """
+ def __repr__(self):
+ return "<Volume: %s>" % self.id
+
+ def delete(self):
+ """
+ Delete this volume.
+ """
+ self.manager.delete(self)
+
+
+class VolumeManager(base.ManagerWithFind):
+ """
+ Manage :class:`Volume` resources.
+ """
+ resource_class = Volume
+
+ def create(self, size, snapshot_id=None,
+ display_name=None, display_description=None,
+ volume_type=None):
+ """
+ Create a volume.
+
+ :param size: Size of volume in GB
+ :param snapshot_id: ID of the snapshot
+ :param display_name: Name of the volume
+ :param display_description: Description of the volume
+ :param volume_type: Type of volume
+ :rtype: :class:`Volume`
+ """
+ body = {'volume': {'size': size,
+ 'snapshot_id': snapshot_id,
+ 'display_name': display_name,
+ 'display_description': display_description,
+ 'volume_type': volume_type}}
+ return self._create('/volumes', body, 'volume')
+
+ def get(self, volume_id):
+ """
+ Get a volume.
+
+ :param volume_id: The ID of the volume to delete.
+ :rtype: :class:`Volume`
+ """
+ return self._get("/volumes/%s" % volume_id, "volume")
+
+ def list(self, detailed=True):
+ """
+ Get a list of all volumes.
+
+ :rtype: list of :class:`Volume`
+ """
+ if detailed is True:
+ return self._list("/volumes/detail", "volumes")
+ else:
+ return self._list("/volumes", "volumes")
+
+ def delete(self, volume):
+ """
+ Delete a volume.
+
+ :param volume: The :class:`Volume` to delete.
+ """
+ self._delete("/volumes/%s" % base.getid(volume))
+
+ def create_server_volume(self, server_id, volume_id, device):
+ """
+ Attach a volume identified by the volume ID to the given server ID
+
+ :param server_id: The ID of the server
+ :param volume_id: The ID of the volume to attach.
+ :param device: The device name
+ :rtype: :class:`Volume`
+ """
+ body = {'volumeAttachment': {'volumeId': volume_id,
+ 'device': device}}
+ return self._create("/servers/%s/os-volume_attachments" % server_id,
+ body, "volumeAttachment")
+
+ def get_server_volume(self, server_id, attachment_id):
+ """
+ Get the volume identified by the attachment ID, that is attached to
+ the given server ID
+
+ :param server_id: The ID of the server
+ :param attachment_id: The ID of the attachment
+ :rtype: :class:`Volume`
+ """
+ return self._get("/servers/%s/os-volume_attachments/%s" % (server_id,
+ attachment_id,), "volumeAttachment")
+
+ def get_server_volumes(self, server_id):
+ """
+ Get a list of all the attached volumes for the given server ID
+
+ :param server_id: The ID of the server
+ :rtype: list of :class:`Volume`
+ """
+ return self._list("/servers/%s/os-volume_attachments" % server_id,
+ "volumeAttachments")
+
+ def delete_server_volume(self, server_id, attachment_id):
+ """
+ Detach a volume identified by the attachment ID from the given server
+
+ :param server_id: The ID of the server
+ :param attachment_id: The ID of the attachment
+ """
+ self._delete("/servers/%s/os-volume_attachments/%s" %
+ (server_id, attachment_id,))