commit 471704df644eced17026c280b0aab9e549718e14
Author: Jenkins <jenkins@review.openstack.org>
Date:   Mon May 21 16:32:35 2012 -0400

    Initial split from python-novaclient.

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..28d20be52
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+.coverage
+.venv
+*,cover
+cover
+*.pyc
+.idea
+*.swp
+*~
+build
+dist
+python_novaclient.egg-info
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 000000000..cb9446e7e
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,4 @@
+[gerrit]
+host=review.openstack.org
+port=29418
+project=openstack/python-cinderclient.git
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 000000000..f270bb6f5
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,15 @@
+Antony Messerli <amesserl@rackspace.com> root <root@debian.ohthree.com>
+<amesserl@rackspace.com> <root@debian.ohthree.com>
+<brian.waldon@rackspace.com> <bcwaldon@gmail.com>
+Chris Behrens <cbehrens+github@codestud.com> comstud <cbehrens+github@codestud.com>
+<cbehrens+github@codestud.com> <cbehrens@codestud.com>
+Johannes Erdfelt <johannes.erdfelt@rackspace.com> jerdfelt <johannes@erdfelt.com>
+<johannes.erdfelt@rackspace.com> <johannes@erdfelt.com>
+<josh@jk0.org> <jkearney@nova.(none)>
+<sandy@darksecretsoftware.com> <sandy.walsh@rackspace.com>
+<sandy@darksecretsoftware.com> <sandy@sandywalsh.com>
+Andy Smith <github@anarkystic.com> termie <github@anarkystic.com>
+<chmouel.boudjnah@rackspace.co.uk> <chmouel@chmouel.com>
+<matt.dietz@rackspace.com> <matthew.dietz@gmail.com>
+Nikolay Sokolov <nsokolov@griddynamics.com> Nokolay Sokolov <nsokolov@griddynamics.com>
+Nikolay Sokolov <nsokolov@griddynamics.com> Nokolay Sokolov <chemikadze@gmail.com>
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 000000000..ee399b9ea
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,60 @@
+Aaron Lee <aaron.lee@rackspace.com>
+Alex Meade <alex.meade@rackspace.com>
+Alvaro Lopez Garcia <aloga@ifca.unican.es>
+Andrey Brindeyev <abrindeyev@griddynamics.com>
+Andy Smith <github@anarkystic.com>
+Anthony Young <sleepsonthefloor@gmail.com>
+Antony Messerli <amesserl@rackspace.com>
+Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
+Brian Lamar <brian.lamar@rackspace.com>
+Brian Waldon <brian.waldon@rackspace.com>
+Chmouel Boudjnah <chmouel.boudjnah@rackspace.co.uk>
+Chris Behrens <cbehrens+github@codestud.com>
+Christian Berendt <berendt@b1-systems.de>
+Christopher MacGown <ignoti+github@gmail.com>
+Chuck Thier <cthier@gmail.com>
+Cole Robinson <crobinso@redhat.com>
+Dan Prince <dprince@redhat.com>
+Dan Wendlandt <dan@nicira.com>
+Dave Walker <Dave.Walker@canonical.com>
+Dean Troyer <dtroyer@gmail.com>
+Ed Leafe <ed@leafe.com>
+Edouard Thuleau <edouard1.thuleau@orange.com>
+Eldar Nugaev <eldr@ya.ru>
+François Charlier <francois.charlier@ecindernce.com>
+Gabriel Hurley <gabriel@strikeawe.com>
+Gaurav Gupta <gaurav@denali-systems.com>
+Hengqing Hu <hudayou@hotmail.com>
+Ilya Alekseyev <ilyaalekseyev@acm.org>
+Jake Dahn <admin@jakedahn.com>
+James E. Blair <james.blair@rackspace.com>
+Jason Kölker <jason@koelker.net>
+Jason Straw <jason.straw@rackspace.com>
+Jay Pipes <jaypipes@gmail.com>
+Jesse Andrews <anotherjesse@gmail.com>
+Johannes Erdfelt <johannes.erdfelt@rackspace.com>
+John Garbutt <john.garbutt@citrix.com>
+Josh Kearney <josh@jk0.org>
+Juan G. Hernando Rivero <ghe.rivero@stackops.com>
+Kevin L. Mitchell <kevin.mitchell@rackspace.com>
+Kiall Mac Innes <kiall@managedit.ie>
+Kirill Shileev <kshileev@griddynamics.com>
+Lvov Maxim <mlvov@mirantis.com>
+Matt Dietz <matt.dietz@rackspace.com>
+Matt Stephenson <mattstep@mattstep.net>
+Michael Basnight <mbasnight@gmail.com>
+Nicholas Mistry <nmistry@gmail.com>
+Nikolay Sokolov <nsokolov@griddynamics.com>
+Pádraig Brady <pbrady@redhat.com>
+Pavel Shkitin <pshkitin@griddynamics.com>
+Peng Yong <ppyy@pubyun.com>
+Rick Harris <rconradharris@gmail.com>
+Robie Basak <robie.basak@canonical.com>
+Russell Bryant <rbryant@redhat.com>
+Sandy Walsh <sandy@darksecretsoftware.com>
+Unmesh Gurjar <unmesh.gurjar@vertex.co.in>
+William Wolf <throughnothing@gmail.com>
+Yaguang Tang <heut2008@gmail.com>
+Zhongyue Luo <lzyeval@gmail.com>
+Scott Moser <smoser@ubuntu.com>
+Paul Voccio <paul@substation9.com>
diff --git a/HACKING b/HACKING
new file mode 100644
index 000000000..d9d1cb851
--- /dev/null
+++ b/HACKING
@@ -0,0 +1,115 @@
+Nova Style Commandments
+=======================
+
+Step 1: Read http://www.python.org/dev/peps/pep-0008/
+Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
+Step 3: Read on
+
+Imports
+-------
+- thou shalt not import objects, only modules
+- thou shalt not import more than one module per line
+- thou shalt not make relative imports
+- thou shalt organize your imports according to the following template
+
+::
+  # vim: tabstop=4 shiftwidth=4 softtabstop=4
+  {{stdlib imports in human alphabetical order}}
+  \n
+  {{cinder imports in human alphabetical order}}
+  \n
+  \n
+  {{begin your code}}
+
+
+General
+-------
+- thou shalt put two newlines twixt toplevel code (funcs, classes, etc)
+- thou shalt put one newline twixt methods in classes and anywhere else
+- thou shalt not write "except:", use "except Exception:" at the very least
+- thou shalt include your name with TODOs as in "TODO(termie)"
+- thou shalt not name anything the same name as a builtin or reserved word
+- thou shalt not violate causality in our time cone, or else
+
+
+Human Alphabetical Order Examples
+---------------------------------
+::
+  import httplib
+  import logging
+  import random
+  import StringIO
+  import time
+  import unittest
+
+  from cinder import flags
+  from cinder import test
+  from cinder.auth import users
+  from cinder.endpoint import api
+  from cinder.endpoint import cloud
+
+Docstrings
+----------
+  """A one line docstring looks like this and ends in a period."""
+
+
+  """A multiline docstring has a one-line summary, less than 80 characters.
+
+  Then a new paragraph after a newline that explains in more detail any
+  general information about the function, class or method. Example usages
+  are also great to have here if it is a complex class for function. After
+  you have finished your descriptions add an extra newline and close the
+  quotations.
+
+  When writing the docstring for a class, an extra line should be placed
+  after the closing quotations. For more in-depth explanations for these
+  decisions see http://www.python.org/dev/peps/pep-0257/
+
+  If you are going to describe parameters and return values, use Sphinx, the
+  appropriate syntax is as follows.
+
+  :param foo: the foo parameter
+  :param bar: the bar parameter
+  :returns: description of the return value
+
+  """
+
+Text encoding
+----------
+- All text within python code should be of type 'unicode'.
+
+    WRONG:
+
+    >>> s = 'foo'
+    >>> s
+    'foo'
+    >>> type(s)
+    <type 'str'>
+
+    RIGHT:
+
+    >>> u = u'foo'
+    >>> u
+    u'foo'
+    >>> type(u)
+    <type 'unicode'>
+
+- Transitions between internal unicode and external strings should always
+  be immediately and explicitly encoded or decoded.
+
+- All external text that is not explicitly encoded (database storage,
+  commandline arguments, etc.) should be presumed to be encoded as utf-8.
+
+    WRONG:
+
+    mystring = infile.readline()
+    myreturnstring = do_some_magic_with(mystring)
+    outfile.write(myreturnstring)
+
+    RIGHT:
+
+    mystring = infile.readline()
+    mytext = s.decode('utf-8')
+    returntext = do_some_magic_with(mytext)
+    returnstring = returntext.encode('utf-8')
+    outfile.write(returnstring)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..3ecd07361
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,208 @@
+Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1)
+Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1)
+All rights reserved.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+--- License for python-cinderclient versions prior to 2.1 ---
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of this project nor the names of its contributors may
+    be used to endorse or promote products derived from this software without
+    specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 000000000..c217ce17c
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,8 @@
+include AUTHORS
+include HACKING
+include LICENSE
+include README.rst
+include run_tests.sh tox.ini
+recursive-include docs *
+recursive-include tests *
+recursive-include tools *
diff --git a/README.rst b/README.rst
new file mode 100644
index 000000000..ede24bc70
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,155 @@
+Python bindings to the OpenStack Volume API
+===========================================
+
+This is a client for the OpenStack Volume API. There's a Python API (the
+``cinderclient`` module), and a command-line script (``cinder``). Each
+implements 100% of the OpenStack Volume API.
+
+[PENDING] `Full documentation is available`__.
+
+__ http://packages.python.org/python-cinderclient/
+
+You'll also probably want to read `OpenStack Compute Developer Guide API`__ --
+the first bit, at least -- to get an idea of the concepts. Rackspace is doing
+the cloud hosting thing a bit differently from Amazon, and if you get the
+concepts this library should make more sense.
+
+__ http://docs.openstack.org/api/
+
+The project is hosted on `Launchpad`_, where bugs can be filed. The code is
+hosted on `Github`_. Patches must be submitted using `Gerrit`_, *not* Github
+pull requests.
+
+.. _Github: https://github.com/openstack/python-cinderclient
+.. _Launchpad: https://launchpad.net/python-cinderclient
+.. _Gerrit: http://wiki.openstack.org/GerritWorkflow
+
+This code a fork of `Jacobian's python-cloudservers`__ If you need API support
+for the Rackspace API solely or the BSD license, you should use that repository.
+python-client is licensed under the Apache License like the rest of OpenStack.
+
+__ http://github.com/jacobian/python-cloudservers
+
+.. contents:: Contents:
+   :local:
+
+Command-line API
+----------------
+
+Installing this package gets you a shell command, ``cinder``, that you
+can use to interact with any Rackspace compatible API (including OpenStack).
+
+You'll need to provide your OpenStack username and password. You can do this
+with the ``--os_username``, ``--os_password`` and  ``--os_tenant_name``
+params, but it's easier to just set them as environment variables::
+
+    export OS_USERNAME=openstack
+    export OS_PASSWORD=yadayada
+    export OS_TENANT_NAME=myproject
+
+You will also need to define the authentication url with ``--os_auth_url``
+and the version of the API with ``--version``.  Or set them as an environment
+variables as well::
+
+    export OS_AUTH_URL=http://example.com:8774/v1.1/
+    export OS_COMPUTE_API_VERSION=1.1
+
+If you are using Keystone, you need to set the CINDER_URL to the keystone
+endpoint::
+
+    export OS_AUTH_URL=http://example.com:5000/v2.0/
+
+Since Keystone can return multiple regions in the Service Catalog, you
+can specify the one you want with ``--os_region_name`` (or
+``export OS_REGION_NAME``). It defaults to the first in the list returned.
+
+You'll find complete documentation on the shell by running
+``cinder help``::
+
+    usage: cinder [--debug] [--os_username OS_USERNAME] [--os_password OS_PASSWORD]
+                [--os_tenant_name OS_TENANT_NAME] [--os_auth_url OS_AUTH_URL]
+                [--os_region_name OS_REGION_NAME] [--service_type SERVICE_TYPE]
+                [--service_name SERVICE_NAME] [--endpoint_type ENDPOINT_TYPE]
+                [--version VERSION] [--username USERNAME]
+                [--region_name REGION_NAME] [--apikey APIKEY]
+                [--projectid PROJECTID] [--url URL]
+                <subcommand> ...
+
+    Command-line interface to the OpenStack Nova API.
+
+    Positional arguments:
+      <subcommand>
+        create              Add a new volume.
+        credentials         Show user credentials returned from auth
+        delete              Remove a volume.
+        endpoints           Discover endpoints that get returned from the
+                            authenticate services
+        list                List all the volumes.
+        show                Show details about a volume.
+        snapshot-create     Add a new snapshot.
+        snapshot-delete     Remove a snapshot.
+        snapshot-list       List all the snapshots.
+        snapshot-show       Show details about a snapshot.
+        type-create         Create a new volume type.
+        type-delete         Delete a specific flavor
+        type-list           Print a list of available 'volume types'.
+        bash-completion     Prints all of the commands and options to stdout so
+                            that the
+        help                Display help about this program or one of its
+                            subcommands.
+
+    Optional arguments:
+      --debug               Print debugging output
+      --os_username OS_USERNAME
+                            Defaults to env[OS_USERNAME].
+      --os_password OS_PASSWORD
+                            Defaults to env[OS_PASSWORD].
+      --os_tenant_name OS_TENANT_NAME
+                            Defaults to env[OS_TENANT_NAME].
+      --os_auth_url OS_AUTH_URL
+                            Defaults to env[OS_AUTH_URL].
+      --os_region_name OS_REGION_NAME
+                            Defaults to env[OS_REGION_NAME].
+      --service_type SERVICE_TYPE
+                            Defaults to compute for most actions
+      --service_name SERVICE_NAME
+                            Defaults to env[CINDER_SERVICE_NAME]
+      --endpoint_type ENDPOINT_TYPE
+                            Defaults to env[CINDER_ENDPOINT_TYPE] or publicURL.
+      --os_compute_api_version VERSION
+                            Accepts 1.1, defaults to env[OS_COMPUTE_API_VERSION].
+      --username USERNAME   Deprecated
+      --region_name REGION_NAME
+                            Deprecated
+      --apikey APIKEY, --password APIKEY
+                            Deprecated
+      --projectid PROJECTID, --tenant_name PROJECTID
+                            Deprecated
+      --url URL, --auth_url URL
+                            Deprecated
+
+    See "cinder help COMMAND" for help on a specific command.
+
+Python API
+----------
+
+[PENDING] There's also a `complete Python API`__.
+
+__ http://packages.python.org/python-cinderclient/
+
+Quick-start using keystone::
+
+    # use v2.0 auth with http://example.com:5000/v2.0/")
+    >>> from cinderclient.v1 import client
+    >>> nt = client.Client(USER, PASS, TENANT, AUTH_URL, service_type="compute")
+    >>> nt.flavors.list()
+    [...]
+    >>> nt.servers.list()
+    [...]
+    >>> nt.keypairs.list()
+    [...]
+
+What's new?
+-----------
+
+[PENDING] See `the release notes <http://packages.python.org/python-cinderclient/releases.html>`_.
diff --git a/cinderclient/__init__.py b/cinderclient/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/cinderclient/base.py b/cinderclient/base.py
new file mode 100644
index 000000000..02d35498b
--- /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 000000000..278e922f3
--- /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 000000000..91bf30ef9
--- /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 000000000..ced67f0c7
--- /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 000000000..a2c8b37ae
--- /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 000000000..25d55363b
--- /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 000000000..52f4da906
--- /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 000000000..cecfacd23
--- /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 000000000..cbee8ba91
--- /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 000000000..e69de29bb
diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py
new file mode 100644
index 000000000..6b8b7bbed
--- /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 000000000..fa6c4b465
--- /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 000000000..e6d644df4
--- /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 000000000..d465724f4
--- /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,))
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 000000000..c6a151b32
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1 @@
+_build/
\ No newline at end of file
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 000000000..c00452af9
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,89 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  dirhtml   to make HTML files named index.html in directories"
+	@echo "  pickle    to make pickle files"
+	@echo "  json      to make JSON files"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  qthelp    to make HTML files and a qthelp project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-cinderclient.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-cinderclient.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 000000000..1e184bbf4
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,67 @@
+The :mod:`cinderclient` Python API
+==================================
+
+.. module:: cinderclient
+   :synopsis: A client for the OpenStack Nova API.
+
+.. currentmodule:: cinderclient
+
+Usage
+-----
+
+First create an instance of :class:`OpenStack` with your credentials::
+
+    >>> from cinderclient import OpenStack
+    >>> cinder = OpenStack(USERNAME, PASSWORD, AUTH_URL)
+
+Then call methods on the :class:`OpenStack` object:
+
+.. class:: OpenStack
+
+    .. attribute:: backup_schedules
+
+        A :class:`BackupScheduleManager` -- manage automatic backup images.
+
+    .. attribute:: flavors
+
+        A :class:`FlavorManager` -- query available "flavors" (hardware
+        configurations).
+
+    .. attribute:: images
+
+        An :class:`ImageManager` -- query and create server disk images.
+
+    .. attribute:: ipgroups
+
+        A :class:`IPGroupManager` -- manage shared public IP addresses.
+
+    .. attribute:: servers
+
+        A :class:`ServerManager` -- start, stop, and manage virtual machines.
+
+    .. automethod:: authenticate
+
+For example::
+
+    >>> cinder.servers.list()
+    [<Server: buildslave-ubuntu-9.10>]
+
+    >>> cinder.flavors.list()
+    [<Flavor: 256 server>,
+     <Flavor: 512 server>,
+     <Flavor: 1GB server>,
+     <Flavor: 2GB server>,
+     <Flavor: 4GB server>,
+     <Flavor: 8GB server>,
+     <Flavor: 15.5GB server>]
+
+    >>> fl = cinder.flavors.find(ram=512)
+    >>> cinder.servers.create("my-server", flavor=fl)
+    <Server: my-server>
+
+For more information, see the reference:
+
+.. toctree::
+   :maxdepth: 2
+
+   ref/index
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 000000000..966b4de56
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+#
+# python-cinderclient documentation build configuration file, created by
+# sphinx-quickstart on Sun Dec  6 14:19:25 2009.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'python-cinderclient'
+copyright = u'Rackspace, based on work by Jacob Kaplan-Moss'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '2.6'
+# The full version, including alpha/beta/rc tags.
+release = '2.6.10'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'nature'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'python-cinderclientdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'python-cinderclient.tex', u'python-cinderclient Documentation',
+   u'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 000000000..d992f7c62
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,45 @@
+Python bindings to the OpenStack Nova API
+==================================================
+
+This is a client for OpenStack Nova API. There's :doc:`a Python API
+<api>` (the :mod:`cinderclient` module), and a :doc:`command-line script
+<shell>` (installed as :program:`cinder`). Each implements the entire
+OpenStack Nova API.
+
+You'll need an `OpenStack Nova` account, which you can get by using `cinder-manage`.
+
+.. seealso::
+
+    You may want to read `Rackspace's API guide`__ (PDF) -- the first bit, at
+    least -- to get an idea of the concepts. Rackspace is doing the cloud
+    hosting thing a bit differently from Amazon, and if you get the concepts
+    this library should make more sense.
+
+    __ http://docs.rackspacecloud.com/servers/api/cs-devguide-latest.pdf
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   shell
+   api
+   ref/index
+   releases
+
+Contributing
+============
+
+Development takes place `on GitHub`__; please file bugs/pull requests there.
+
+__ https://github.com/rackspace/python-cinderclient
+
+Run tests with ``python setup.py test``.
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/docs/ref/backup_schedules.rst b/docs/ref/backup_schedules.rst
new file mode 100644
index 000000000..cbd69e3dd
--- /dev/null
+++ b/docs/ref/backup_schedules.rst
@@ -0,0 +1,60 @@
+Backup schedules
+================
+
+.. currentmodule:: cinderclient
+
+Rackspace allows scheduling of weekly and/or daily backups for virtual
+servers. You can access these backup schedules either off the API object as
+:attr:`OpenStack.backup_schedules`, or directly off a particular
+:class:`Server` instance as :attr:`Server.backup_schedule`.
+
+Classes
+-------
+
+.. autoclass:: BackupScheduleManager
+   :members: create, delete, update, get
+   
+.. autoclass:: BackupSchedule
+   :members: update, delete
+   
+   .. attribute:: enabled
+   
+        Is this backup enabled? (boolean)
+        
+   .. attribute:: weekly
+   
+        The day of week upon which to perform a weekly backup.
+   
+   .. attribute:: daily
+   
+        The daily time period during which to perform a daily backup.
+        
+Constants
+---------
+
+Constants for selecting weekly backup days:
+
+    .. data:: BACKUP_WEEKLY_DISABLED
+    .. data:: BACKUP_WEEKLY_SUNDAY
+    .. data:: BACKUP_WEEKLY_MONDAY  
+    .. data:: BACKUP_WEEKLY_TUESDAY 
+    .. data:: BACKUP_WEEKLY_WEDNESDA
+    .. data:: BACKUP_WEEKLY_THURSDAY
+    .. data:: BACKUP_WEEKLY_FRIDAY  
+    .. data:: BACKUP_WEEKLY_SATURDAY
+    
+Constants for selecting hourly backup windows:
+
+    .. data:: BACKUP_DAILY_DISABLED   
+    .. data:: BACKUP_DAILY_H_0000_0200
+    .. data:: BACKUP_DAILY_H_0200_0400
+    .. data:: BACKUP_DAILY_H_0400_0600
+    .. data:: BACKUP_DAILY_H_0600_0800
+    .. data:: BACKUP_DAILY_H_0800_1000
+    .. data:: BACKUP_DAILY_H_1000_1200
+    .. data:: BACKUP_DAILY_H_1200_1400
+    .. data:: BACKUP_DAILY_H_1400_1600
+    .. data:: BACKUP_DAILY_H_1600_1800
+    .. data:: BACKUP_DAILY_H_1800_2000
+    .. data:: BACKUP_DAILY_H_2000_2200
+    .. data:: BACKUP_DAILY_H_2200_0000
diff --git a/docs/ref/exceptions.rst b/docs/ref/exceptions.rst
new file mode 100644
index 000000000..23618e3ef
--- /dev/null
+++ b/docs/ref/exceptions.rst
@@ -0,0 +1,14 @@
+Exceptions
+==========
+
+.. currentmodule:: cinderclient
+
+Exceptions
+----------
+
+Exceptions that the API might throw:
+
+.. automodule:: cinderclient
+   :members: OpenStackException, BadRequest, Unauthorized, Forbidden, 
+             NotFound, OverLimit
+
diff --git a/docs/ref/flavors.rst b/docs/ref/flavors.rst
new file mode 100644
index 000000000..12b396ac5
--- /dev/null
+++ b/docs/ref/flavors.rst
@@ -0,0 +1,35 @@
+Flavors
+=======
+
+From Rackspace's API documentation:
+
+    A flavor is an available hardware configuration for a server. Each flavor
+    has a unique combination of disk space, memory capacity and priority for
+    CPU time.
+    
+Classes
+-------
+
+.. currentmodule:: cinderclient
+
+.. autoclass:: FlavorManager
+   :members: get, list, find, findall
+
+.. autoclass:: Flavor
+   :members:
+   
+   .. attribute:: id
+   
+       This flavor's ID.
+       
+   .. attribute:: name
+   
+       A human-readable name for this flavor.
+   
+   .. attribute:: ram
+   
+       The amount of RAM this flavor has, in MB.
+       
+   .. attribute:: disk
+   
+       The amount of disk space this flavor has, in MB
diff --git a/docs/ref/images.rst b/docs/ref/images.rst
new file mode 100644
index 000000000..6ba6c24ea
--- /dev/null
+++ b/docs/ref/images.rst
@@ -0,0 +1,54 @@
+Images
+======
+
+.. currentmodule:: cinderclient
+
+An "image" is a snapshot from which you can create new server instances.
+
+From Rackspace's own API documentation:
+
+    An image is a collection of files used to create or rebuild a server.
+    Rackspace provides a number of pre-built OS images by default. You may
+    also create custom images from cloud servers you have launched. These
+    custom images are useful for backup purposes or for producing "gold"
+    server images if you plan to deploy a particular server configuration
+    frequently.
+
+Classes
+-------
+
+.. autoclass:: ImageManager
+   :members: get, list, find, findall, create, delete
+
+.. autoclass:: Image
+   :members: delete
+      
+   .. attribute:: id
+   
+       This image's ID.
+       
+   .. attribute:: name
+   
+       This image's name.
+       
+   .. attribute:: created
+   
+       The date/time this image was created.
+       
+   .. attribute:: updated
+   
+       The date/time this instance was updated.
+       
+   .. attribute:: status
+   
+       The status of this image (usually ``"SAVING"`` or ``ACTIVE``).
+       
+   .. attribute:: progress
+   
+       During saving of an image this'll be set to something between
+       0 and 100, representing a rough percentage done.
+       
+   .. attribute:: serverId
+   
+       If this image was created from a :class:`Server` then this attribute
+       will be set to the ID of the server whence this image came.
diff --git a/docs/ref/index.rst b/docs/ref/index.rst
new file mode 100644
index 000000000..c1fe136bb
--- /dev/null
+++ b/docs/ref/index.rst
@@ -0,0 +1,12 @@
+API Reference
+=============
+
+.. toctree::
+   :maxdepth: 1
+   
+   backup_schedules
+   exceptions
+   flavors
+   images
+   ipgroups
+   servers
\ No newline at end of file
diff --git a/docs/ref/ipgroups.rst b/docs/ref/ipgroups.rst
new file mode 100644
index 000000000..4c29f2ee9
--- /dev/null
+++ b/docs/ref/ipgroups.rst
@@ -0,0 +1,46 @@
+Shared IP addresses
+===================
+
+From the Rackspace API guide:
+
+    Public IP addresses can be shared across multiple servers for use in
+    various high availability scenarios. When an IP address is shared to
+    another server, the cloud network restrictions are modified to allow each
+    server to listen to and respond on that IP address (you may optionally
+    specify that the target server network configuration be modified). Shared
+    IP addresses can be used with many standard heartbeat facilities (e.g.
+    ``keepalived``) that monitor for failure and manage IP failover.
+
+    A shared IP group is a collection of servers that can share IPs with other
+    members of the group. Any server in a group can share one or more public
+    IPs with any other server in the group. With the exception of the first
+    server in a shared IP group, servers must be launched into shared IP
+    groups. A server may only be a member of one shared IP group.
+
+.. seealso::
+
+    Use :meth:`Server.share_ip` and `Server.unshare_ip` to share and unshare
+    IPs in a group.
+
+Classes
+-------
+
+.. currentmodule:: cinderclient
+
+.. autoclass:: IPGroupManager
+   :members: get, list, find, findall, create, delete
+   
+.. autoclass:: IPGroup
+   :members: delete
+   
+   .. attribute:: id
+   
+        Shared group ID.
+   
+   .. attribute:: name
+   
+        Name of the group.
+   
+   .. attribute:: servers
+   
+        A list of server IDs in this group.
diff --git a/docs/ref/servers.rst b/docs/ref/servers.rst
new file mode 100644
index 000000000..b02fca5be
--- /dev/null
+++ b/docs/ref/servers.rst
@@ -0,0 +1,73 @@
+Servers
+=======
+
+A virtual machine instance.
+
+Classes
+-------
+
+.. currentmodule:: cinderclient
+
+.. autoclass:: ServerManager
+   :members: get, list, find, findall, create, update, delete, share_ip,
+             unshare_ip, reboot, rebuild, resize, confirm_resize,
+             revert_resize
+
+.. autoclass:: Server
+   :members: update, delete, share_ip, unshare_ip, reboot, rebuild, resize,
+             confirm_resize, revert_resize
+
+   .. attribute:: id
+
+        This server's ID.
+
+   .. attribute:: name
+
+        The name you gave the server when you booted it.
+
+   .. attribute:: imageId
+
+        The :class:`Image` this server was booted with.
+
+   .. attribute:: flavorId
+
+        This server's current :class:`Flavor`.
+
+   .. attribute:: hostId
+
+        Rackspace doesn't document this value. It appears to be SHA1 hash.
+
+   .. attribute:: status
+
+        The server's status (``BOOTING``, ``ACTIVE``, etc).
+
+   .. attribute:: progress
+
+        When booting, resizing, updating, etc., this will be set to a
+        value between 0 and 100 giving a rough estimate of the progress
+        of the current operation.
+
+   .. attribute:: addresses
+
+        The public and private IP addresses of this server. This'll be a dict
+        of the form::
+
+            {
+              "public" : ["67.23.10.138"],
+              "private" : ["10.176.42.19"]
+            }
+
+        You *can* get more than one public/private IP provisioned, but not
+        directly from the API; you'll need to open a support ticket.
+
+   .. attribute:: metadata
+
+        The metadata dict you gave when creating the server.
+
+Constants
+---------
+
+Reboot types:
+
+.. data:: REBOOT_SOFT
+.. data:: REBOOT_HARD
diff --git a/docs/releases.rst b/docs/releases.rst
new file mode 100644
index 000000000..783b1cad5
--- /dev/null
+++ b/docs/releases.rst
@@ -0,0 +1,99 @@
+=============
+Release notes
+=============
+
+2.5.8 (July 11, 2011)
+=====================
+* returns all public/private ips, not just first one
+* better 'cinder list' search options
+
+2.5.7 - 2.5.6 = minor tweaks
+
+2.5.5 (June 21, 2011)
+=====================
+* zone-boot min/max instance count added thanks to comstud
+* create for user added thanks to cerberus
+* fixed tests
+
+2.5.3 (June 15, 2011)
+=====================
+* ProjectID can be None for backwards compatability.
+* README/docs updated for projectId thanks to usrleon
+
+2.5.1 (June 10, 2011)
+=====================
+* ProjectID now part of authentication
+
+2.5.0 (June 3, 2011)
+=================
+
+* better logging thanks to GridDynamics
+
+2.4.4 (June 1, 2011)
+=================
+
+* added support for GET /servers with reservation_id (and /servers/detail)
+
+2.4.3 (May 27, 2011)
+=================
+
+* added support for POST /zones/select (client only, not cmdline)
+
+2.4 (March 7, 2011)
+=================
+
+* added Jacob Kaplan-Moss copyright notices to older/untouched files.
+
+
+2.3 (March 2, 2011)
+=================
+
+* package renamed to python-cinderclient. Module to cinderclient
+
+
+2.2 (March 1, 2011)
+=================
+
+* removed some license/copywrite notices from source that wasn't
+  significantly changed.
+
+
+2.1 (Feb 28, 2011)
+=================
+
+* shell renamed to cinder from cindertools
+
+* license changed from BSD to Apache
+
+2.0 (Feb 7, 2011)
+=================
+
+* Forked from https://github.com/jacobian/python-cloudservers
+
+* Rebranded to python-cindertools
+
+* Auth URL support
+
+* New OpenStack specific commands added (pause, suspend, etc)
+
+1.2 (August 15, 2010)
+=====================
+
+* Support for Python 2.4 - 2.7.
+
+* Improved output of :program:`cloudservers ipgroup-list`.
+
+* Made ``cloudservers boot --ipgroup <name>`` work (as well as ``--ipgroup
+  <id>``).
+
+1.1 (May 6, 2010)
+=================
+
+* Added a ``--files`` option to :program:`cloudservers boot` supporting
+  the upload of (up to five) files at boot time.
+
+* Added a ``--key`` option to :program:`cloudservers boot` to key the server
+  with an SSH public key at boot time. This is just a shortcut for ``--files``,
+  but it's a useful shortcut.
+
+* Changed the default server image to Ubuntu 10.04 LTS.
diff --git a/docs/shell.rst b/docs/shell.rst
new file mode 100644
index 000000000..cff5cc73b
--- /dev/null
+++ b/docs/shell.rst
@@ -0,0 +1,52 @@
+The :program:`cinder` shell utility
+=========================================
+
+.. program:: cinder
+.. highlight:: bash
+
+The :program:`cinder` shell utility interacts with OpenStack Nova API
+from the command line. It supports the entirety of the OpenStack Nova API.
+
+First, you'll need an OpenStack Nova account and an API key. You get this
+by using the `cinder-manage` command in OpenStack Nova.
+
+You'll need to provide :program:`cinder` with your OpenStack username and
+API key. You can do this with the :option:`--os_username`, :option:`--os_password`
+and :option:`--os_tenant_id` options, but it's easier to just set them as
+environment variables by setting two environment variables:
+
+.. envvar:: OS_USERNAME
+
+    Your OpenStack Nova username.
+
+.. envvar:: OS_PASSWORD
+
+    Your password.
+
+.. envvar:: OS_TENANT_NAME
+
+    Project for work.
+
+.. envvar:: OS_AUTH_URL
+
+    The OpenStack API server URL.
+
+.. envvar:: OS_COMPUTE_API_VERSION
+
+    The OpenStack API version.
+
+For example, in Bash you'd use::
+
+    export OS_USERNAME=yourname
+    export OS_PASSWORD=yadayadayada
+    export OS_TENANT_NAME=myproject
+    export OS_AUTH_URL=http://...
+    export OS_COMPUTE_API_VERSION=1.1
+    
+From there, all shell commands take the form::
+    
+    cinder <command> [arguments...]
+
+Run :program:`cinder help` to get a full list of all possible commands,
+and run :program:`cinder help <command>` to get detailed help for that
+command.
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755
index 000000000..96bd437d6
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1,154 @@
+#!/bin/bash
+
+set -eu
+
+function usage {
+  echo "Usage: $0 [OPTION]..."
+  echo "Run python-cinderclient test suite"
+  echo ""
+  echo "  -V, --virtual-env        Always use virtualenv.  Install automatically if not present"
+  echo "  -N, --no-virtual-env     Don't use virtualenv.  Run tests in local environment"
+  echo "  -s, --no-site-packages   Isolate the virtualenv from the global Python environment"
+  echo "  -x, --stop               Stop running tests after the first error or failure."
+  echo "  -f, --force              Force a clean re-build of the virtual environment. Useful when dependencies have been added."
+  echo "  -p, --pep8               Just run pep8"
+  echo "  -P, --no-pep8            Don't run pep8"
+  echo "  -c, --coverage           Generate coverage report"
+  echo "  -h, --help               Print this usage message"
+  echo "  --hide-elapsed           Don't print the elapsed time for each test along with slow test list"
+  echo ""
+  echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
+  echo "      If no virtualenv is found, the script will ask if you would like to create one.  If you "
+  echo "      prefer to run tests NOT in a virtual environment, simply pass the -N option."
+  exit
+}
+
+function process_option {
+  case "$1" in
+    -h|--help) usage;;
+    -V|--virtual-env) always_venv=1; never_venv=0;;
+    -N|--no-virtual-env) always_venv=0; never_venv=1;;
+    -s|--no-site-packages) no_site_packages=1;;
+    -f|--force) force=1;;
+    -p|--pep8) just_pep8=1;;
+    -P|--no-pep8) no_pep8=1;;
+    -c|--coverage) coverage=1;;
+    -*) noseopts="$noseopts $1";;
+    *) noseargs="$noseargs $1"
+  esac
+}
+
+venv=.venv
+with_venv=tools/with_venv.sh
+always_venv=0
+never_venv=0
+force=0
+no_site_packages=0
+installvenvopts=
+noseargs=
+noseopts=
+wrapper=""
+just_pep8=0
+no_pep8=0
+coverage=0
+
+for arg in "$@"; do
+  process_option $arg
+done
+
+# If enabled, tell nose to collect coverage data
+if [ $coverage -eq 1 ]; then
+    noseopts="$noseopts --with-coverage --cover-package=cinderclient"
+fi
+
+if [ $no_site_packages -eq 1 ]; then
+  installvenvopts="--no-site-packages"
+fi
+
+function run_tests {
+  # Cleanup *.pyc
+  ${wrapper} find . -type f -name "*.pyc" -delete
+  # Just run the test suites in current environment
+  ${wrapper} $NOSETESTS
+  # If we get some short import error right away, print the error log directly
+  RESULT=$?
+  return $RESULT
+}
+
+function run_pep8 {
+  echo "Running pep8 ..."
+  srcfiles="cinderclient tests"
+  # Just run PEP8 in current environment
+  #
+  # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the
+  # following reasons:
+  #
+  #  1. It's needed to preserve traceback information when re-raising
+  #     exceptions; this is needed b/c Eventlet will clear exceptions when
+  #     switching contexts.
+  #
+  #  2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this
+  #     in Python 2 (in Python 3 `with_traceback` could be used).
+  #
+  #  3. Can find no corroborating evidence that this is deprecated in Python 2
+  #     other than what the PEP8 tool claims. It is deprecated in Python 3, so,
+  #     perhaps the mistake was thinking that the deprecation applied to Python 2
+  #     as well.
+  pep8_opts="--ignore=E202,W602 --repeat"
+  ${wrapper} pep8 ${pep8_opts} ${srcfiles}
+}
+
+NOSETESTS="nosetests $noseopts $noseargs"
+
+if [ $never_venv -eq 0 ]
+then
+  # Remove the virtual environment if --force used
+  if [ $force -eq 1 ]; then
+    echo "Cleaning virtualenv..."
+    rm -rf ${venv}
+  fi
+  if [ -e ${venv} ]; then
+    wrapper="${with_venv}"
+  else
+    if [ $always_venv -eq 1 ]; then
+      # Automatically install the virtualenv
+      python tools/install_venv.py $installvenvopts
+      wrapper="${with_venv}"
+    else
+      echo -e "No virtual environment found...create one? (Y/n) \c"
+      read use_ve
+      if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
+        # Install the virtualenv and run the test suite in it
+        python tools/install_venv.py $installvenvopts
+        wrapper=${with_venv}
+      fi
+    fi
+  fi
+fi
+
+# Delete old coverage data from previous runs
+if [ $coverage -eq 1 ]; then
+    ${wrapper} coverage erase
+fi
+
+if [ $just_pep8 -eq 1 ]; then
+    run_pep8
+    exit
+fi
+
+run_tests
+
+# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
+# not when we're running tests individually. To handle this, we need to
+# distinguish between options (noseopts), which begin with a '-', and
+# arguments (noseargs).
+if [ -z "$noseargs" ]; then
+  if [ $no_pep8 -eq 0 ]; then
+    run_pep8
+  fi
+fi
+
+if [ $coverage -eq 1 ]; then
+    echo "Generating coverage report in covhtml/"
+    ${wrapper} coverage html -d covhtml -i
+fi
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 000000000..dda281b3f
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,13 @@
+[nosetests]
+cover-package = cinderclient
+cover-html = true
+cover-erase = true
+cover-inclusive = true
+
+[build_sphinx]
+source-dir = docs/
+build-dir = docs/_build
+all_files = 1
+
+[upload_sphinx]
+upload-dir = docs/_build/html
diff --git a/setup.py b/setup.py
new file mode 100644
index 000000000..0c170136b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,56 @@
+# 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 setuptools
+import sys
+
+
+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):
+    return open(os.path.join(os.path.dirname(__file__), file_name)).read()
+
+
+setuptools.setup(
+    name="python-cinderclient",
+    version="2012.2",
+    author="Rackspace, based on work by Jacob Kaplan-Moss",
+    author_email="github@racklabs.com",
+    description="Client library for OpenStack Nova API.",
+    long_description=read_file("README.rst"),
+    license="Apache License, Version 2.0",
+    url="https://github.com/openstack/python-cinderclient",
+    packages=setuptools.find_packages(exclude=['tests', 'tests.*']),
+    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": ["cinder = cinderclient.shell:main"]
+    }
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/fakes.py b/tests/fakes.py
new file mode 100644
index 000000000..248214ff0
--- /dev/null
+++ b/tests/fakes.py
@@ -0,0 +1,71 @@
+"""
+A fake server that "responds" to API methods with pre-canned responses.
+
+All of these responses come from the spec, so if for some reason the spec's
+wrong the tests might raise AssertionError. I've indicated in comments the
+places where actual behavior differs from the spec.
+"""
+
+
+def assert_has_keys(dict, required=[], optional=[]):
+    keys = dict.keys()
+    for k in required:
+        try:
+            assert k in keys
+        except AssertionError:
+            extra_keys = set(keys).difference(set(required + optional))
+            raise AssertionError("found unexpected keys: %s" %
+                    list(extra_keys))
+
+
+class FakeClient(object):
+
+    def assert_called(self, method, url, body=None, pos=-1):
+        """
+        Assert than an API method was just called.
+        """
+        expected = (method, url)
+        called = self.client.callstack[pos][0:2]
+
+        assert self.client.callstack, \
+                       "Expected %s %s but no calls were made." % expected
+
+        assert expected == called, 'Expected %s %s; got %s %s' % \
+                                               (expected + called)
+
+        if body is not None:
+            assert self.client.callstack[pos][2] == body
+
+    def assert_called_anytime(self, method, url, body=None):
+        """
+        Assert than an API method was called anytime in the test.
+        """
+        expected = (method, url)
+
+        assert self.client.callstack, \
+                       "Expected %s %s but no calls were made." % expected
+
+        found = False
+        for entry in self.client.callstack:
+            if expected == entry[0:2]:
+                found = True
+                break
+
+        assert found, 'Expected %s %s; got %s' % \
+                              (expected, self.client.callstack)
+        if body is not None:
+            try:
+                assert entry[2] == body
+            except AssertionError:
+                print entry[2]
+                print "!="
+                print body
+                raise
+
+        self.client.callstack = []
+
+    def clear_callstack(self):
+        self.client.callstack = []
+
+    def authenticate(self):
+        pass
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 000000000..7eba9864a
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,48 @@
+from cinderclient import base
+from cinderclient import exceptions
+from cinderclient.v1 import volumes
+from tests import utils
+from tests.v1 import fakes
+
+
+cs = fakes.FakeClient()
+
+
+class BaseTest(utils.TestCase):
+
+    def test_resource_repr(self):
+        r = base.Resource(None, dict(foo="bar", baz="spam"))
+        self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
+
+    def test_getid(self):
+        self.assertEqual(base.getid(4), 4)
+
+        class TmpObject(object):
+            id = 4
+        self.assertEqual(base.getid(TmpObject), 4)
+
+    def test_eq(self):
+        # Two resources of the same type with the same id: equal
+        r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
+        r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
+        self.assertEqual(r1, r2)
+
+        # Two resoruces of different types: never equal
+        r1 = base.Resource(None, {'id': 1})
+        r2 = volumes.Volume(None, {'id': 1})
+        self.assertNotEqual(r1, r2)
+
+        # Two resources with no ID: equal if their info is equal
+        r1 = base.Resource(None, {'name': 'joe', 'age': 12})
+        r2 = base.Resource(None, {'name': 'joe', 'age': 12})
+        self.assertEqual(r1, r2)
+
+    def test_findall_invalid_attribute(self):
+        # Make sure findall with an invalid attribute doesn't cause errors.
+        # The following should not raise an exception.
+        cs.volumes.findall(vegetable='carrot')
+
+        # However, find() should raise an error
+        self.assertRaises(exceptions.NotFound,
+                          cs.volumes.find,
+                          vegetable='carrot')
diff --git a/tests/test_client.py b/tests/test_client.py
new file mode 100644
index 000000000..f5e4bab59
--- /dev/null
+++ b/tests/test_client.py
@@ -0,0 +1,18 @@
+
+import cinderclient.client
+import cinderclient.v1.client
+from tests import utils
+
+
+class ClientTest(utils.TestCase):
+
+    def setUp(self):
+        pass
+
+    def test_get_client_class_v1(self):
+        output = cinderclient.client.get_client_class('1')
+        self.assertEqual(output, cinderclient.v1.client.Client)
+
+    def test_get_client_class_unknown(self):
+        self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
+                          cinderclient.client.get_client_class, '0')
diff --git a/tests/test_http.py b/tests/test_http.py
new file mode 100644
index 000000000..13d744ede
--- /dev/null
+++ b/tests/test_http.py
@@ -0,0 +1,74 @@
+import httplib2
+import mock
+
+from cinderclient import client
+from cinderclient import exceptions
+from tests import utils
+
+
+fake_response = httplib2.Response({"status": 200})
+fake_body = '{"hi": "there"}'
+mock_request = mock.Mock(return_value=(fake_response, fake_body))
+
+
+def get_client():
+    cl = client.HTTPClient("username", "password",
+                           "project_id", "auth_test")
+    return cl
+
+
+def get_authed_client():
+    cl = get_client()
+    cl.management_url = "http://example.com"
+    cl.auth_token = "token"
+    return cl
+
+
+class ClientTest(utils.TestCase):
+
+    def test_get(self):
+        cl = get_authed_client()
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        @mock.patch('time.time', mock.Mock(return_value=1234))
+        def test_get_call():
+            resp, body = cl.get("/hi")
+            headers = {"X-Auth-Token": "token",
+                       "X-Auth-Project-Id": "project_id",
+                       "User-Agent": cl.USER_AGENT,
+                       'Accept': 'application/json',
+            }
+            mock_request.assert_called_with("http://example.com/hi",
+                                            "GET", headers=headers)
+            # Automatic JSON parsing
+            self.assertEqual(body, {"hi": "there"})
+
+        test_get_call()
+
+    def test_post(self):
+        cl = get_authed_client()
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_post_call():
+            cl.post("/hi", body=[1, 2, 3])
+            headers = {
+                "X-Auth-Token": "token",
+                "X-Auth-Project-Id": "project_id",
+                "Content-Type": "application/json",
+                'Accept': 'application/json',
+                "User-Agent": cl.USER_AGENT
+            }
+            mock_request.assert_called_with("http://example.com/hi", "POST",
+                                            headers=headers, body='[1, 2, 3]')
+
+        test_post_call()
+
+    def test_auth_failure(self):
+        cl = get_client()
+
+        # response must not have x-server-management-url header
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_auth_call():
+            self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate)
+
+        test_auth_call()
diff --git a/tests/test_service_catalog.py b/tests/test_service_catalog.py
new file mode 100644
index 000000000..bb93dcf8d
--- /dev/null
+++ b/tests/test_service_catalog.py
@@ -0,0 +1,127 @@
+from cinderclient import exceptions
+from cinderclient import service_catalog
+from tests import utils
+
+
+# Taken directly from keystone/content/common/samples/auth.json
+# Do not edit this structure. Instead, grab the latest from there.
+
+SERVICE_CATALOG = {
+    "access": {
+        "token": {
+            "id": "ab48a9efdfedb23ty3494",
+            "expires": "2010-11-01T03:32:15-05:00",
+            "tenant": {
+                "id": "345",
+                "name": "My Project"
+            }
+        },
+        "user": {
+            "id": "123",
+            "name": "jqsmith",
+            "roles": [
+                {
+                    "id": "234",
+                    "name": "compute:admin",
+                },
+                {
+                    "id": "235",
+                    "name": "object-store:admin",
+                    "tenantId": "1",
+                }
+            ],
+            "roles_links": [],
+        },
+        "serviceCatalog": [
+            {
+                "name": "Cloud Servers",
+                "type": "compute",
+                "endpoints": [
+                    {
+                        "tenantId": "1",
+                        "publicURL": "https://compute1.host/v1/1234",
+                        "internalURL": "https://compute1.host/v1/1234",
+                        "region": "North",
+                        "versionId": "1.0",
+                        "versionInfo": "https://compute1.host/v1/",
+                        "versionList": "https://compute1.host/"
+                    },
+                    {
+                        "tenantId": "2",
+                        "publicURL": "https://compute1.host/v1/3456",
+                        "internalURL": "https://compute1.host/v1/3456",
+                        "region": "North",
+                        "versionId": "1.1",
+                        "versionInfo": "https://compute1.host/v1/",
+                        "versionList": "https://compute1.host/"
+                    },
+                ],
+                "endpoints_links": [],
+            },
+            {
+                "name": "Nova Volumes",
+                "type": "volume",
+                "endpoints": [
+                    {
+                        "tenantId": "1",
+                        "publicURL": "https://volume1.host/v1/1234",
+                        "internalURL": "https://volume1.host/v1/1234",
+                        "region": "South",
+                        "versionId": "1.0",
+                        "versionInfo": "uri",
+                        "versionList": "uri"
+                    },
+                    {
+                        "tenantId": "2",
+                        "publicURL": "https://volume1.host/v1/3456",
+                        "internalURL": "https://volume1.host/v1/3456",
+                        "region": "South",
+                        "versionId": "1.1",
+                        "versionInfo": "https://volume1.host/v1/",
+                        "versionList": "https://volume1.host/"
+                    },
+                ],
+                "endpoints_links": [
+                    {
+                        "rel": "next",
+                        "href": "https://identity1.host/v2.0/endpoints"
+                    },
+                ],
+            },
+        ],
+        "serviceCatalog_links": [
+            {
+                "rel": "next",
+                "href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar",
+            },
+        ],
+    },
+}
+
+
+class ServiceCatalogTest(utils.TestCase):
+    def test_building_a_service_catalog(self):
+        sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
+
+        self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
+                          service_type='compute')
+        self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'),
+                            "https://compute1.host/v1/1234")
+        self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'),
+                            "https://compute1.host/v1/3456")
+
+        self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
+                          "region", "South", service_type='compute')
+
+    def test_alternate_service_type(self):
+        sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
+
+        self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
+                          service_type='volume')
+        self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'),
+                            "https://volume1.host/v1/1234")
+        self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'),
+                            "https://volume1.host/v1/3456")
+
+        self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
+                          "region", "North", service_type='volume')
diff --git a/tests/test_shell.py b/tests/test_shell.py
new file mode 100644
index 000000000..902aec5b8
--- /dev/null
+++ b/tests/test_shell.py
@@ -0,0 +1,75 @@
+import cStringIO
+import os
+import httplib2
+import sys
+
+from cinderclient import exceptions
+import cinderclient.shell
+from tests import utils
+
+
+class ShellTest(utils.TestCase):
+
+    # Patch os.environ to avoid required auth info.
+    def setUp(self):
+        global _old_env
+        fake_env = {
+            'OS_USERNAME': 'username',
+            'OS_PASSWORD': 'password',
+            'OS_TENANT_NAME': 'tenant_name',
+            'OS_AUTH_URL': 'http://no.where',
+        }
+        _old_env, os.environ = os.environ, fake_env.copy()
+
+    def shell(self, argstr):
+        orig = sys.stdout
+        try:
+            sys.stdout = cStringIO.StringIO()
+            _shell = cinderclient.shell.OpenStackCinderShell()
+            _shell.main(argstr.split())
+        except SystemExit:
+            exc_type, exc_value, exc_traceback = sys.exc_info()
+            self.assertEqual(exc_value.code, 0)
+        finally:
+            out = sys.stdout.getvalue()
+            sys.stdout.close()
+            sys.stdout = orig
+
+        return out
+
+    def tearDown(self):
+        global _old_env
+        os.environ = _old_env
+
+    def test_help_unknown_command(self):
+        self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
+
+    def test_debug(self):
+        httplib2.debuglevel = 0
+        self.shell('--debug help')
+        assert httplib2.debuglevel == 1
+
+    def test_help(self):
+        required = [
+            '^usage: ',
+            '(?m)^\s+create\s+Add a new volume.',
+            '(?m)^See "cinder help COMMAND" for help on a specific command',
+        ]
+        for argstr in ['--help', 'help']:
+            help_text = self.shell(argstr)
+            for r in required:
+                self.assertRegexpMatches(help_text, r)
+
+    def test_help_on_subcommand(self):
+        required = [
+            '^usage: cinder list',
+            '(?m)^List all the volumes.',
+        ]
+        argstrings = [
+            'list --help',
+            'help list',
+        ]
+        for argstr in argstrings:
+            help_text = self.shell(argstr)
+            for r in required:
+                self.assertRegexpMatches(help_text, r)
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 000000000..39fb2c913
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,74 @@
+
+from cinderclient import exceptions
+from cinderclient import utils
+from cinderclient import base
+from tests import utils as test_utils
+
+UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
+
+
+class FakeResource(object):
+
+    def __init__(self, _id, properties):
+        self.id = _id
+        try:
+            self.name = properties['name']
+        except KeyError:
+            pass
+        try:
+            self.display_name = properties['display_name']
+        except KeyError:
+            pass
+
+
+class FakeManager(base.ManagerWithFind):
+
+    resource_class = FakeResource
+
+    resources = [
+        FakeResource('1234', {'name': 'entity_one'}),
+        FakeResource(UUID, {'name': 'entity_two'}),
+        FakeResource('4242', {'display_name': 'entity_three'}),
+        FakeResource('5678', {'name': '9876'})
+    ]
+
+    def get(self, resource_id):
+        for resource in self.resources:
+            if resource.id == str(resource_id):
+                return resource
+        raise exceptions.NotFound(resource_id)
+
+    def list(self):
+        return self.resources
+
+
+class FindResourceTestCase(test_utils.TestCase):
+
+    def setUp(self):
+        self.manager = FakeManager(None)
+
+    def test_find_none(self):
+        self.assertRaises(exceptions.CommandError,
+                          utils.find_resource,
+                          self.manager,
+                          'asdf')
+
+    def test_find_by_integer_id(self):
+        output = utils.find_resource(self.manager, 1234)
+        self.assertEqual(output, self.manager.get('1234'))
+
+    def test_find_by_str_id(self):
+        output = utils.find_resource(self.manager, '1234')
+        self.assertEqual(output, self.manager.get('1234'))
+
+    def test_find_by_uuid(self):
+        output = utils.find_resource(self.manager, UUID)
+        self.assertEqual(output, self.manager.get(UUID))
+
+    def test_find_by_str_name(self):
+        output = utils.find_resource(self.manager, 'entity_one')
+        self.assertEqual(output, self.manager.get('1234'))
+
+    def test_find_by_str_displayname(self):
+        output = utils.find_resource(self.manager, 'entity_three')
+        self.assertEqual(output, self.manager.get('4242'))
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 000000000..7f1c5dc71
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,5 @@
+import unittest2
+
+
+class TestCase(unittest2.TestCase):
+    pass
diff --git a/tests/v1/__init__.py b/tests/v1/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/v1/fakes.py b/tests/v1/fakes.py
new file mode 100644
index 000000000..e430970d3
--- /dev/null
+++ b/tests/v1/fakes.py
@@ -0,0 +1,765 @@
+# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
+# 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 httplib2
+import urlparse
+
+from cinderclient import client as base_client
+from cinderclient.v1 import client
+from tests import fakes
+
+
+class FakeClient(fakes.FakeClient, client.Client):
+
+    def __init__(self, *args, **kwargs):
+        client.Client.__init__(self, 'username', 'password',
+                               'project_id', 'auth_url')
+        self.client = FakeHTTPClient(**kwargs)
+
+
+class FakeHTTPClient(base_client.HTTPClient):
+
+    def __init__(self, **kwargs):
+        self.username = 'username'
+        self.password = 'password'
+        self.auth_url = 'auth_url'
+        self.callstack = []
+
+    def _cs_request(self, url, method, **kwargs):
+        # Check that certain things are called correctly
+        if method in ['GET', 'DELETE']:
+            assert 'body' not in kwargs
+        elif method == 'PUT':
+            assert 'body' in kwargs
+
+        # Call the method
+        args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
+        kwargs.update(args)
+        munged_url = url.rsplit('?', 1)[0]
+        munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
+        munged_url = munged_url.replace('-', '_')
+
+        callback = "%s_%s" % (method.lower(), munged_url)
+
+        if not hasattr(self, callback):
+            raise AssertionError('Called unknown API method: %s %s, '
+                                 'expected fakes method name: %s' %
+                                 (method, url, callback))
+
+        # Note the call
+        self.callstack.append((method, url, kwargs.get('body', None)))
+
+        status, body = getattr(self, callback)(**kwargs)
+        if hasattr(status, 'items'):
+            return httplib2.Response(status), body
+        else:
+            return httplib2.Response({"status": status}), body
+
+    #
+    # Limits
+    #
+
+    def get_limits(self, **kw):
+        return (200, {"limits": {
+            "rate": [
+                {
+                    "uri": "*",
+                    "regex": ".*",
+                    "limit": [
+                        {
+                            "value": 10,
+                            "verb": "POST",
+                            "remaining": 2,
+                            "unit": "MINUTE",
+                            "next-available": "2011-12-15T22:42:45Z"
+                        },
+                        {
+                            "value": 10,
+                            "verb": "PUT",
+                            "remaining": 2,
+                            "unit": "MINUTE",
+                            "next-available": "2011-12-15T22:42:45Z"
+                        },
+                        {
+                            "value": 100,
+                            "verb": "DELETE",
+                            "remaining": 100,
+                            "unit": "MINUTE",
+                            "next-available": "2011-12-15T22:42:45Z"
+                        }
+                    ]
+                },
+                {
+                    "uri": "*/servers",
+                    "regex": "^/servers",
+                    "limit": [
+                        {
+                            "verb": "POST",
+                            "value": 25,
+                            "remaining": 24,
+                            "unit": "DAY",
+                            "next-available": "2011-12-15T22:42:45Z"
+                        }
+                    ]
+                }
+            ],
+            "absolute": {
+                "maxTotalRAMSize": 51200,
+                "maxServerMeta": 5,
+                "maxImageMeta": 5,
+                "maxPersonality": 5,
+                "maxPersonalitySize": 10240
+            },
+        },
+    })
+
+    #
+    # Servers
+    #
+
+    def get_volumes(self, **kw):
+        return (200, {"volumes": [
+            {'id': 1234, 'name': 'sample-volume'},
+            {'id': 5678, 'name': 'sample-volume2'}
+        ]})
+
+    # TODO(jdg): This will need to change
+    # at the very least it's not complete
+    def get_volumes_detail(self, **kw):
+        return (200, {"volumes": [
+            {'id': 1234,
+             'name': 'sample-volume',
+             'attachments': [{'server_id': 1234}]
+            },
+        ]})
+
+    def get_volumes_1234(self, **kw):
+        r = {'volume': self.get_volumes_detail()[1]['volumes'][0]}
+        return (200, r)
+
+    def post_servers(self, body, **kw):
+        assert set(body.keys()) <= set(['server', 'os:scheduler_hints'])
+        fakes.assert_has_keys(body['server'],
+                        required=['name', 'imageRef', 'flavorRef'],
+                        optional=['metadata', 'personality'])
+        if 'personality' in body['server']:
+            for pfile in body['server']['personality']:
+                fakes.assert_has_keys(pfile, required=['path', 'contents'])
+        return (202, self.get_servers_1234()[1])
+
+    def get_servers_1234(self, **kw):
+        r = {'server': self.get_servers_detail()[1]['servers'][0]}
+        return (200, r)
+
+    def get_servers_5678(self, **kw):
+        r = {'server': self.get_servers_detail()[1]['servers'][1]}
+        return (200, r)
+
+    def put_servers_1234(self, body, **kw):
+        assert body.keys() == ['server']
+        fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
+        return (204, None)
+
+    def delete_servers_1234(self, **kw):
+        return (202, None)
+
+    def delete_volumes_1234(self, **kw):
+        return (202, None)
+
+    def delete_servers_1234_metadata_test_key(self, **kw):
+        return (204, None)
+
+    def delete_servers_1234_metadata_key1(self, **kw):
+        return (204, None)
+
+    def delete_servers_1234_metadata_key2(self, **kw):
+        return (204, None)
+
+    def post_servers_1234_metadata(self, **kw):
+        return (204, {'metadata': {'test_key': 'test_value'}})
+
+    def get_servers_1234_diagnostics(self, **kw):
+        return (200, {'data': 'Fake diagnostics'})
+
+    def get_servers_1234_actions(self, **kw):
+        return (200, {'actions': [
+            {
+                'action': 'rebuild',
+                'error': None,
+                'created_at': '2011-12-30 11:45:36'
+            },
+            {
+                'action': 'reboot',
+                'error': 'Failed!',
+                'created_at': '2011-12-30 11:40:29'
+            },
+        ]})
+
+    #
+    # Server Addresses
+    #
+
+    def get_servers_1234_ips(self, **kw):
+        return (200, {'addresses':
+                      self.get_servers_1234()[1]['server']['addresses']})
+
+    def get_servers_1234_ips_public(self, **kw):
+        return (200, {'public':
+                      self.get_servers_1234_ips()[1]['addresses']['public']})
+
+    def get_servers_1234_ips_private(self, **kw):
+        return (200, {'private':
+                      self.get_servers_1234_ips()[1]['addresses']['private']})
+
+    def delete_servers_1234_ips_public_1_2_3_4(self, **kw):
+        return (202, None)
+
+    #
+    # Server actions
+    #
+
+    def post_servers_1234_action(self, body, **kw):
+        _body = None
+        resp = 202
+        assert len(body.keys()) == 1
+        action = body.keys()[0]
+        if action == 'reboot':
+            assert body[action].keys() == ['type']
+            assert body[action]['type'] in ['HARD', 'SOFT']
+        elif action == 'rebuild':
+            keys = body[action].keys()
+            if 'adminPass' in keys:
+                keys.remove('adminPass')
+            assert keys == ['imageRef']
+            _body = self.get_servers_1234()[1]
+        elif action == 'resize':
+            assert body[action].keys() == ['flavorRef']
+        elif action == 'confirmResize':
+            assert body[action] is None
+            # This one method returns a different response code
+            return (204, None)
+        elif action == 'revertResize':
+            assert body[action] is None
+        elif action == 'migrate':
+            assert body[action] is None
+        elif action == 'rescue':
+            assert body[action] is None
+        elif action == 'unrescue':
+            assert body[action] is None
+        elif action == 'lock':
+            assert body[action] is None
+        elif action == 'unlock':
+            assert body[action] is None
+        elif action == 'addFixedIp':
+            assert body[action].keys() == ['networkId']
+        elif action == 'removeFixedIp':
+            assert body[action].keys() == ['address']
+        elif action == 'addFloatingIp':
+            assert body[action].keys() == ['address']
+        elif action == 'removeFloatingIp':
+            assert body[action].keys() == ['address']
+        elif action == 'createImage':
+            assert set(body[action].keys()) == set(['name', 'metadata'])
+            resp = dict(status=202, location="http://blah/images/456")
+        elif action == 'changePassword':
+            assert body[action].keys() == ['adminPass']
+        elif action == 'os-getConsoleOutput':
+            assert body[action].keys() == ['length']
+            return (202, {'output': 'foo'})
+        elif action == 'os-getVNCConsole':
+            assert body[action].keys() == ['type']
+        elif action == 'os-migrateLive':
+            assert set(body[action].keys()) == set(['host',
+                                                    'block_migration',
+                                                    'disk_over_commit'])
+        else:
+            raise AssertionError("Unexpected server action: %s" % action)
+        return (resp, _body)
+
+    #
+    # Cloudpipe
+    #
+
+    def get_os_cloudpipe(self, **kw):
+        return (200, {'cloudpipes': [
+            {'project_id':1}
+        ]})
+
+    def post_os_cloudpipe(self, **ks):
+        return (202, {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'})
+
+    #
+    # Flavors
+    #
+
+    def get_flavors(self, **kw):
+        return (200, {'flavors': [
+            {'id': 1, 'name': '256 MB Server'},
+            {'id': 2, 'name': '512 MB Server'}
+        ]})
+
+    def get_flavors_detail(self, **kw):
+        return (200, {'flavors': [
+            {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10,
+             'OS-FLV-EXT-DATA:ephemeral': 10},
+            {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20,
+             'OS-FLV-EXT-DATA:ephemeral': 20}
+        ]})
+
+    def get_flavors_1(self, **kw):
+        return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][0]})
+
+    def get_flavors_2(self, **kw):
+        return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][1]})
+
+    def get_flavors_3(self, **kw):
+        # Diablo has no ephemeral
+        return (200, {'flavor': {'id': 3, 'name': '256 MB Server',
+                                 'ram': 256, 'disk': 10}})
+
+    def delete_flavors_flavordelete(self, **kw):
+        return (202, None)
+
+    def post_flavors(self, body, **kw):
+        return (202, {'flavor': self.get_flavors_detail()[1]['flavors'][0]})
+
+    #
+    # Floating ips
+    #
+
+    def get_os_floating_ip_pools(self):
+        return (200, {'floating_ip_pools': [{'name': 'foo', 'name': 'bar'}]})
+
+    def get_os_floating_ips(self, **kw):
+        return (200, {'floating_ips': [
+            {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'},
+            {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'},
+        ]})
+
+    def get_os_floating_ips_1(self, **kw):
+        return (200, {'floating_ip':
+            {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}
+        })
+
+    def post_os_floating_ips(self, body, **kw):
+        return (202, self.get_os_floating_ips_1()[1])
+
+    def post_os_floating_ips(self, body):
+        if body.get('pool'):
+            return (200, {'floating_ip':
+                {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1',
+                                                            'pool': 'cinder'}})
+        else:
+            return (200, {'floating_ip':
+                {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1',
+                                                            'pool': None}})
+
+    def delete_os_floating_ips_1(self, **kw):
+        return (204, None)
+
+    def get_os_floating_ip_dns(self, **kw):
+        return (205, {'domain_entries':
+                      [{'domain': 'example.org'},
+                       {'domain': 'example.com'}]})
+
+    def get_os_floating_ip_dns_testdomain_entries(self, **kw):
+        if kw.get('ip'):
+            return (205, {'dns_entries':
+                          [{'dns_entry':
+                             {'ip': kw.get('ip'),
+                              'name': "host1",
+                              'type': "A",
+                              'domain': 'testdomain'}},
+                           {'dns_entry':
+                             {'ip': kw.get('ip'),
+                              'name': "host2",
+                              'type': "A",
+                              'domain': 'testdomain'}}]})
+        else:
+            return (404, None)
+
+    def get_os_floating_ip_dns_testdomain_entries_testname(self, **kw):
+        return (205, {'dns_entry':
+                        {'ip': "10.10.10.10",
+                         'name': 'testname',
+                         'type': "A",
+                         'domain': 'testdomain'}})
+
+    def put_os_floating_ip_dns_testdomain(self, body, **kw):
+        if body['domain_entry']['scope'] == 'private':
+            fakes.assert_has_keys(body['domain_entry'],
+                            required=['availability_zone', 'scope'])
+        elif body['domain_entry']['scope'] == 'public':
+            fakes.assert_has_keys(body['domain_entry'],
+                            required=['project', 'scope'])
+
+        else:
+            fakes.assert_has_keys(body['domain_entry'],
+                            required=['project', 'scope'])
+        return (205, None)
+
+    def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw):
+        fakes.assert_has_keys(body['dns_entry'],
+                        required=['ip', 'dns_type'])
+        return (205, None)
+
+    def delete_os_floating_ip_dns_testdomain(self, **kw):
+        return (200, None)
+
+    def delete_os_floating_ip_dns_testdomain_entries_testname(self, **kw):
+        return (200, None)
+
+    #
+    # Images
+    #
+    def get_images(self, **kw):
+        return (200, {'images': [
+            {'id': 1, 'name': 'CentOS 5.2'},
+            {'id': 2, 'name': 'My Server Backup'}
+        ]})
+
+    def get_images_detail(self, **kw):
+        return (200, {'images': [
+            {
+                'id': 1,
+                'name': 'CentOS 5.2',
+                "updated": "2010-10-10T12:00:00Z",
+                "created": "2010-08-10T12:00:00Z",
+                "status": "ACTIVE",
+                "metadata": {
+                    "test_key": "test_value",
+                },
+                "links": {},
+            },
+            {
+                "id": 743,
+                "name": "My Server Backup",
+                "serverId": 1234,
+                "updated": "2010-10-10T12:00:00Z",
+                "created": "2010-08-10T12:00:00Z",
+                "status": "SAVING",
+                "progress": 80,
+                "links": {},
+            }
+        ]})
+
+    def get_images_1(self, **kw):
+        return (200, {'image': self.get_images_detail()[1]['images'][0]})
+
+    def get_images_2(self, **kw):
+        return (200, {'image': self.get_images_detail()[1]['images'][1]})
+
+    def post_images(self, body, **kw):
+        assert body.keys() == ['image']
+        fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
+        return (202, self.get_images_1()[1])
+
+    def post_images_1_metadata(self, body, **kw):
+        assert body.keys() == ['metadata']
+        fakes.assert_has_keys(body['metadata'],
+                              required=['test_key'])
+        return (200,
+            {'metadata': self.get_images_1()[1]['image']['metadata']})
+
+    def delete_images_1(self, **kw):
+        return (204, None)
+
+    def delete_images_1_metadata_test_key(self, **kw):
+        return (204, None)
+
+    #
+    # Keypairs
+    #
+    def get_os_keypairs(self, *kw):
+        return (200, {"keypairs": [
+            {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'}
+        ]})
+
+    def delete_os_keypairs_test(self, **kw):
+        return (202, None)
+
+    def post_os_keypairs(self, body, **kw):
+        assert body.keys() == ['keypair']
+        fakes.assert_has_keys(body['keypair'],
+                              required=['name'])
+        r = {'keypair': self.get_os_keypairs()[1]['keypairs'][0]}
+        return (202, r)
+
+    #
+    # Virtual Interfaces
+    #
+    def get_servers_1234_os_virtual_interfaces(self, **kw):
+        return (200, {"virtual_interfaces": [
+            {'id': 'fakeid', 'mac_address': 'fakemac'}
+        ]})
+
+    #
+    # Quotas
+    #
+
+    def get_os_quota_sets_test(self, **kw):
+        return (200, {'quota_set': {
+                      'tenant_id': 'test',
+                      'metadata_items': [],
+                      'injected_file_content_bytes': 1,
+                      'volumes': 1,
+                      'gigabytes': 1,
+                      'ram': 1,
+                      'floating_ips': 1,
+                      'instances': 1,
+                      'injected_files': 1,
+                      'cores': 1}})
+
+    def get_os_quota_sets_test_defaults(self):
+        return (200, {'quota_set': {
+                      'tenant_id': 'test',
+                      'metadata_items': [],
+                      'injected_file_content_bytes': 1,
+                      'volumes': 1,
+                      'gigabytes': 1,
+                      'ram': 1,
+                      'floating_ips': 1,
+                      'instances': 1,
+                      'injected_files': 1,
+                      'cores': 1}})
+
+    def put_os_quota_sets_test(self, body, **kw):
+        assert body.keys() == ['quota_set']
+        fakes.assert_has_keys(body['quota_set'],
+                              required=['tenant_id'])
+        return (200, {'quota_set': {
+                      'tenant_id': 'test',
+                      'metadata_items': [],
+                      'injected_file_content_bytes': 1,
+                      'volumes': 2,
+                      'gigabytes': 1,
+                      'ram': 1,
+                      'floating_ips': 1,
+                      'instances': 1,
+                      'injected_files': 1,
+                      'cores': 1}})
+
+    #
+    # Quota Classes
+    #
+
+    def get_os_quota_class_sets_test(self, **kw):
+        return (200, {'quota_class_set': {
+                      'class_name': 'test',
+                      'metadata_items': [],
+                      'injected_file_content_bytes': 1,
+                      'volumes': 1,
+                      'gigabytes': 1,
+                      'ram': 1,
+                      'floating_ips': 1,
+                      'instances': 1,
+                      'injected_files': 1,
+                      'cores': 1}})
+
+    def put_os_quota_class_sets_test(self, body, **kw):
+        assert body.keys() == ['quota_class_set']
+        fakes.assert_has_keys(body['quota_class_set'],
+                              required=['class_name'])
+        return (200, {'quota_class_set': {
+                      'class_name': 'test',
+                      'metadata_items': [],
+                      'injected_file_content_bytes': 1,
+                      'volumes': 2,
+                      'gigabytes': 1,
+                      'ram': 1,
+                      'floating_ips': 1,
+                      'instances': 1,
+                      'injected_files': 1,
+                      'cores': 1}})
+
+    #
+    # Security Groups
+    #
+    def get_os_security_groups(self, **kw):
+        return (200, {"security_groups": [
+                {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'}
+        ]})
+
+    def get_os_security_groups_1(self, **kw):
+        return (200, {"security_group":
+                {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'}
+        })
+
+    def delete_os_security_groups_1(self, **kw):
+        return (202, None)
+
+    def post_os_security_groups(self, body, **kw):
+        assert body.keys() == ['security_group']
+        fakes.assert_has_keys(body['security_group'],
+                              required=['name', 'description'])
+        r = {'security_group':
+                self.get_os_security_groups()[1]['security_groups'][0]}
+        return (202, r)
+
+    #
+    # Security Group Rules
+    #
+    def get_os_security_group_rules(self, **kw):
+        return (200, {"security_group_rules": [
+                {'id': 1, 'parent_group_id': 1, 'group_id': 2,
+                 'ip_protocol': 'TCP', 'from_port': '22', 'to_port': 22,
+                 'cidr': '10.0.0.0/8'}
+        ]})
+
+    def delete_os_security_group_rules_1(self, **kw):
+        return (202, None)
+
+    def post_os_security_group_rules(self, body, **kw):
+        assert body.keys() == ['security_group_rule']
+        fakes.assert_has_keys(body['security_group_rule'],
+            required=['parent_group_id'],
+            optional=['group_id', 'ip_protocol', 'from_port',
+                      'to_port', 'cidr'])
+        r = {'security_group_rule':
+            self.get_os_security_group_rules()[1]['security_group_rules'][0]}
+        return (202, r)
+
+    #
+    # Tenant Usage
+    #
+    def get_os_simple_tenant_usage(self, **kw):
+        return (200, {u'tenant_usages': [{
+            u'total_memory_mb_usage': 25451.762807466665,
+            u'total_vcpus_usage': 49.71047423333333,
+            u'total_hours': 49.71047423333333,
+            u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
+            u'stop': u'2012-01-22 19:48:41.750722',
+            u'server_usages': [{
+                u'hours': 49.71047423333333,
+                u'uptime': 27035, u'local_gb': 0, u'ended_at': None,
+                u'name': u'f15image1',
+                u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
+                u'vcpus': 1, u'memory_mb': 512, u'state': u'active',
+                u'flavor': u'm1.tiny',
+                u'started_at': u'2012-01-20 18:06:06.479998'}],
+            u'start': u'2011-12-25 19:48:41.750687',
+            u'total_local_gb_usage': 0.0}]})
+
+    def get_os_simple_tenant_usage_tenantfoo(self, **kw):
+        return (200, {u'tenant_usage': {
+            u'total_memory_mb_usage': 25451.762807466665,
+            u'total_vcpus_usage': 49.71047423333333,
+            u'total_hours': 49.71047423333333,
+            u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
+            u'stop': u'2012-01-22 19:48:41.750722',
+            u'server_usages': [{
+                u'hours': 49.71047423333333,
+                u'uptime': 27035, u'local_gb': 0, u'ended_at': None,
+                u'name': u'f15image1',
+                u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
+                u'vcpus': 1, u'memory_mb': 512, u'state': u'active',
+                u'flavor': u'm1.tiny',
+                u'started_at': u'2012-01-20 18:06:06.479998'}],
+            u'start': u'2011-12-25 19:48:41.750687',
+            u'total_local_gb_usage': 0.0}})
+
+    #
+    # Certificates
+    #
+    def get_os_certificates_root(self, **kw):
+        return (200, {'certificate': {'private_key': None, 'data': 'foo'}})
+
+    def post_os_certificates(self, **kw):
+        return (200, {'certificate': {'private_key': 'foo', 'data': 'bar'}})
+
+    #
+    # Aggregates
+    #
+    def get_os_aggregates(self, *kw):
+        return (200, {"aggregates": [
+            {'id':'1',
+             'name': 'test',
+             'availability_zone': 'cinder1'},
+            {'id':'2',
+             'name': 'test2',
+             'availability_zone': 'cinder1'},
+        ]})
+
+    def _return_aggregate(self):
+        r = {'aggregate': self.get_os_aggregates()[1]['aggregates'][0]}
+        return (200, r)
+
+    def get_os_aggregates_1(self, **kw):
+        return self._return_aggregate()
+
+    def post_os_aggregates(self, body, **kw):
+        return self._return_aggregate()
+
+    def put_os_aggregates_1(self, body, **kw):
+        return self._return_aggregate()
+
+    def put_os_aggregates_2(self, body, **kw):
+        return self._return_aggregate()
+
+    def post_os_aggregates_1_action(self, body, **kw):
+        return self._return_aggregate()
+
+    def post_os_aggregates_2_action(self, body, **kw):
+        return self._return_aggregate()
+
+    def delete_os_aggregates_1(self, **kw):
+        return (202, None)
+
+    #
+    # Hosts
+    #
+    def get_os_hosts_host(self, *kw):
+        return (200, {'host':
+                [{'resource': {'project': '(total)', 'host': 'dummy',
+                  'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}},
+                 {'resource': {'project': '(used_now)', 'host': 'dummy',
+                  'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}},
+                 {'resource': {'project': '(used_max)', 'host': 'dummy',
+                  'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}},
+                 {'resource': {'project': 'admin', 'host': 'dummy',
+                  'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}]})
+
+    def get_os_hosts_sample_host(self, *kw):
+        return (200, {'host': [{'resource': {'host': 'sample_host'}}], })
+
+    def put_os_hosts_sample_host_1(self, body, **kw):
+        return (200, {'host': 'sample-host_1',
+                      'status': 'enabled'})
+
+    def put_os_hosts_sample_host_2(self, body, **kw):
+        return (200, {'host': 'sample-host_2',
+                      'maintenance_mode': 'on_maintenance'})
+
+    def put_os_hosts_sample_host_3(self, body, **kw):
+        return (200, {'host': 'sample-host_3',
+                      'status': 'enabled',
+                      'maintenance_mode': 'on_maintenance'})
+
+    def get_os_hosts_sample_host_startup(self, **kw):
+        return (200, {'host': 'sample_host',
+                      'power_action': 'startup'})
+
+    def get_os_hosts_sample_host_reboot(self, **kw):
+        return (200, {'host': 'sample_host',
+                      'power_action': 'reboot'})
+
+    def get_os_hosts_sample_host_shutdown(self, **kw):
+        return (200, {'host': 'sample_host',
+                      'power_action': 'shutdown'})
+
+    def put_os_hosts_sample_host(self, body, **kw):
+        result = {'host': 'dummy'}
+        result.update(body)
+        return (200, result)
diff --git a/tests/v1/test_auth.py b/tests/v1/test_auth.py
new file mode 100644
index 000000000..6aeb5fc20
--- /dev/null
+++ b/tests/v1/test_auth.py
@@ -0,0 +1,297 @@
+import httplib2
+import json
+import mock
+
+from cinderclient.v1 import client
+from cinderclient import exceptions
+from tests import utils
+
+
+def to_http_response(resp_dict):
+    """Converts dict of response attributes to httplib response."""
+    resp = httplib2.Response(resp_dict)
+    for k, v in resp_dict['headers'].items():
+        resp[k] = v
+    return resp
+
+
+class AuthenticateAgainstKeystoneTests(utils.TestCase):
+    def test_authenticate_success(self):
+        cs = client.Client("username", "password", "project_id",
+                           "auth_url/v2.0", service_type='compute')
+        resp = {
+            "access": {
+                "token": {
+                    "expires": "12345",
+                    "id": "FAKE_ID",
+                },
+                "serviceCatalog": [
+                    {
+                        "type": "compute",
+                        "endpoints": [
+                            {
+                                "region": "RegionOne",
+                                "adminURL": "http://localhost:8774/v1",
+                                "internalURL": "http://localhost:8774/v1",
+                                "publicURL": "http://localhost:8774/v1/",
+                            },
+                        ],
+                    },
+                ],
+            },
+        }
+        auth_response = httplib2.Response({
+            "status": 200,
+            "body": json.dumps(resp),
+            })
+
+        mock_request = mock.Mock(return_value=(auth_response,
+                                               json.dumps(resp)))
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_auth_call():
+            cs.client.authenticate()
+            headers = {
+                'User-Agent': cs.client.USER_AGENT,
+                'Content-Type': 'application/json',
+                'Accept': 'application/json',
+            }
+            body = {
+                'auth': {
+                    'passwordCredentials': {
+                        'username': cs.client.user,
+                        'password': cs.client.password,
+                    },
+                    'tenantName': cs.client.projectid,
+                },
+            }
+
+            token_url = cs.client.auth_url + "/tokens"
+            mock_request.assert_called_with(token_url, "POST",
+                                            headers=headers,
+                                            body=json.dumps(body))
+
+            endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
+            public_url = endpoints[0]["publicURL"].rstrip('/')
+            self.assertEqual(cs.client.management_url, public_url)
+            token_id = resp["access"]["token"]["id"]
+            self.assertEqual(cs.client.auth_token, token_id)
+
+        test_auth_call()
+
+    def test_authenticate_failure(self):
+        cs = client.Client("username", "password", "project_id",
+                           "auth_url/v2.0")
+        resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
+        auth_response = httplib2.Response({
+            "status": 401,
+            "body": json.dumps(resp),
+            })
+
+        mock_request = mock.Mock(return_value=(auth_response,
+                                               json.dumps(resp)))
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_auth_call():
+            self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
+
+        test_auth_call()
+
+    def test_auth_redirect(self):
+        cs = client.Client("username", "password", "project_id",
+                           "auth_url/v1", service_type='compute')
+        dict_correct_response = {
+            "access": {
+                "token": {
+                    "expires": "12345",
+                    "id": "FAKE_ID",
+                },
+                "serviceCatalog": [
+                    {
+                        "type": "compute",
+                        "endpoints": [
+                            {
+                                "adminURL": "http://localhost:8774/v1",
+                                "region": "RegionOne",
+                                "internalURL": "http://localhost:8774/v1",
+                                "publicURL": "http://localhost:8774/v1/",
+                            },
+                        ],
+                    },
+                ],
+            },
+        }
+        correct_response = json.dumps(dict_correct_response)
+        dict_responses = [
+            {"headers": {'location':'http://127.0.0.1:5001'},
+             "status": 305,
+             "body": "Use proxy"},
+            # Configured on admin port, cinder redirects to v2.0 port.
+            # When trying to connect on it, keystone auth succeed by v1.0
+            # protocol (through headers) but tokens are being returned in
+            # body (looks like keystone bug). Leaved for compatibility.
+            {"headers": {},
+             "status": 200,
+             "body": correct_response},
+            {"headers": {},
+             "status": 200,
+             "body": correct_response}
+        ]
+
+        responses = [(to_http_response(resp), resp['body']) \
+                        for resp in dict_responses]
+
+        def side_effect(*args, **kwargs):
+            return responses.pop(0)
+
+        mock_request = mock.Mock(side_effect=side_effect)
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_auth_call():
+            cs.client.authenticate()
+            headers = {
+                'User-Agent': cs.client.USER_AGENT,
+                'Content-Type': 'application/json',
+                'Accept': 'application/json',
+            }
+            body = {
+                'auth': {
+                    'passwordCredentials': {
+                        'username': cs.client.user,
+                        'password': cs.client.password,
+                     },
+                     'tenantName': cs.client.projectid,
+                 },
+            }
+
+            token_url = cs.client.auth_url + "/tokens"
+            mock_request.assert_called_with(token_url, "POST",
+                                            headers=headers,
+                                            body=json.dumps(body))
+
+            resp = dict_correct_response
+            endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
+            public_url = endpoints[0]["publicURL"].rstrip('/')
+            self.assertEqual(cs.client.management_url, public_url)
+            token_id = resp["access"]["token"]["id"]
+            self.assertEqual(cs.client.auth_token, token_id)
+
+        test_auth_call()
+
+    def test_ambiguous_endpoints(self):
+        cs = client.Client("username", "password", "project_id",
+                           "auth_url/v2.0", service_type='compute')
+        resp = {
+            "access": {
+                "token": {
+                    "expires": "12345",
+                    "id": "FAKE_ID",
+                },
+                "serviceCatalog": [
+                    {
+                        "adminURL": "http://localhost:8774/v1",
+                        "type": "compute",
+                        "name": "Compute CLoud",
+                        "endpoints": [
+                            {
+                                "region": "RegionOne",
+                                "internalURL": "http://localhost:8774/v1",
+                                "publicURL": "http://localhost:8774/v1/",
+                            },
+                        ],
+                    },
+                    {
+                        "adminURL": "http://localhost:8774/v1",
+                        "type": "compute",
+                        "name": "Hyper-compute Cloud",
+                        "endpoints": [
+                            {
+                                "internalURL": "http://localhost:8774/v1",
+                                "publicURL": "http://localhost:8774/v1/",
+                            },
+                        ],
+                    },
+                ],
+            },
+        }
+        auth_response = httplib2.Response({
+            "status": 200,
+            "body": json.dumps(resp),
+            })
+
+        mock_request = mock.Mock(return_value=(auth_response,
+                                               json.dumps(resp)))
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_auth_call():
+            self.assertRaises(exceptions.AmbiguousEndpoints,
+                              cs.client.authenticate)
+
+        test_auth_call()
+
+
+class AuthenticationTests(utils.TestCase):
+    def test_authenticate_success(self):
+        cs = client.Client("username", "password", "project_id", "auth_url")
+        management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470'
+        auth_response = httplib2.Response({
+            'status': 204,
+            'x-server-management-url': management_url,
+            'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
+        })
+        mock_request = mock.Mock(return_value=(auth_response, None))
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_auth_call():
+            cs.client.authenticate()
+            headers = {
+                'Accept': 'application/json',
+                'X-Auth-User': 'username',
+                'X-Auth-Key': 'password',
+                'X-Auth-Project-Id': 'project_id',
+                'User-Agent': cs.client.USER_AGENT
+            }
+            mock_request.assert_called_with(cs.client.auth_url, 'GET',
+                                            headers=headers)
+            self.assertEqual(cs.client.management_url,
+                             auth_response['x-server-management-url'])
+            self.assertEqual(cs.client.auth_token,
+                             auth_response['x-auth-token'])
+
+        test_auth_call()
+
+    def test_authenticate_failure(self):
+        cs = client.Client("username", "password", "project_id", "auth_url")
+        auth_response = httplib2.Response({'status': 401})
+        mock_request = mock.Mock(return_value=(auth_response, None))
+
+        @mock.patch.object(httplib2.Http, "request", mock_request)
+        def test_auth_call():
+            self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
+
+        test_auth_call()
+
+    def test_auth_automatic(self):
+        cs = client.Client("username", "password", "project_id", "auth_url")
+        http_client = cs.client
+        http_client.management_url = ''
+        mock_request = mock.Mock(return_value=(None, None))
+
+        @mock.patch.object(http_client, 'request', mock_request)
+        @mock.patch.object(http_client, 'authenticate')
+        def test_auth_call(m):
+            http_client.get('/')
+            m.assert_called()
+            mock_request.assert_called()
+
+        test_auth_call()
+
+    def test_auth_manual(self):
+        cs = client.Client("username", "password", "project_id", "auth_url")
+
+        @mock.patch.object(cs.client, 'authenticate')
+        def test_auth_call(m):
+            cs.authenticate()
+            m.assert_called()
+
+        test_auth_call()
diff --git a/tests/v1/test_shell.py b/tests/v1/test_shell.py
new file mode 100644
index 000000000..7efad0e60
--- /dev/null
+++ b/tests/v1/test_shell.py
@@ -0,0 +1,77 @@
+# 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 os
+
+from cinderclient import client
+from cinderclient import shell
+from tests.v1 import fakes
+from tests import utils
+
+
+class ShellTest(utils.TestCase):
+
+    # Patch os.environ to avoid required auth info.
+    def setUp(self):
+        """Run before each test."""
+        self.old_environment = os.environ.copy()
+        os.environ = {
+            'CINDER_USERNAME': 'username',
+            'CINDER_PASSWORD': 'password',
+            'CINDER_PROJECT_ID': 'project_id',
+            'OS_COMPUTE_API_VERSION': '1.1',
+            'CINDER_URL': 'http://no.where',
+        }
+
+        self.shell = shell.OpenStackCinderShell()
+
+        #HACK(bcwaldon): replace this when we start using stubs
+        self.old_get_client_class = client.get_client_class
+        client.get_client_class = lambda *_: fakes.FakeClient
+
+    def tearDown(self):
+        os.environ = self.old_environment
+        # For some method like test_image_meta_bad_action we are
+        # testing a SystemExit to be thrown and object self.shell has
+        # no time to get instantatiated which is OK in this case, so
+        # we make sure the method is there before launching it.
+        if hasattr(self.shell, 'cs'):
+            self.shell.cs.clear_callstack()
+
+        #HACK(bcwaldon): replace this when we start using stubs
+        client.get_client_class = self.old_get_client_class
+
+    def run_command(self, cmd):
+        self.shell.main(cmd.split())
+
+    def assert_called(self, method, url, body=None, **kwargs):
+        return self.shell.cs.assert_called(method, url, body, **kwargs)
+
+    def assert_called_anytime(self, method, url, body=None):
+        return self.shell.cs.assert_called_anytime(method, url, body)
+
+    def test_list(self):
+        self.run_command('list')
+        # NOTE(jdg): we default to detail currently
+        self.assert_called('GET', '/volumes/detail')
+
+    def test_show(self):
+        self.run_command('show 1234')
+        self.assert_called('GET', '/volumes/1234')
+
+    def test_delete(self):
+        self.run_command('delete 1234')
diff --git a/tests/v1/testfile.txt b/tests/v1/testfile.txt
new file mode 100644
index 000000000..e4e860f38
--- /dev/null
+++ b/tests/v1/testfile.txt
@@ -0,0 +1 @@
+BLAH
diff --git a/tests/v1/utils.py b/tests/v1/utils.py
new file mode 100644
index 000000000..f878a5e26
--- /dev/null
+++ b/tests/v1/utils.py
@@ -0,0 +1,29 @@
+from nose.tools import ok_
+
+
+def fail(msg):
+    raise AssertionError(msg)
+
+
+def assert_in(thing, seq, msg=None):
+    msg = msg or "'%s' not found in %s" % (thing, seq)
+    ok_(thing in seq, msg)
+
+
+def assert_not_in(thing, seq, msg=None):
+    msg = msg or "unexpected '%s' found in %s" % (thing, seq)
+    ok_(thing not in seq, msg)
+
+
+def assert_has_keys(dict, required=[], optional=[]):
+    keys = dict.keys()
+    for k in required:
+        assert_in(k, keys, "required key %s missing from %s" % (k, dict))
+    allowed_keys = set(required) | set(optional)
+    extra_keys = set(keys).difference(set(required + optional))
+    if extra_keys:
+        fail("found unexpected keys: %s" % list(extra_keys))
+
+
+def assert_isinstance(thing, kls):
+    ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls))
diff --git a/tools/generate_authors.sh b/tools/generate_authors.sh
new file mode 100755
index 000000000..c41f07988
--- /dev/null
+++ b/tools/generate_authors.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+git shortlog -se | cut -c8-
diff --git a/tools/install_venv.py b/tools/install_venv.py
new file mode 100644
index 000000000..e2de0286f
--- /dev/null
+++ b/tools/install_venv.py
@@ -0,0 +1,244 @@
+# 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.
+#
+# Copyright 2010 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.
+
+"""
+Installation script for Nova's development virtualenv
+"""
+
+import optparse
+import os
+import subprocess
+import sys
+import platform
+
+
+ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+VENV = os.path.join(ROOT, '.venv')
+PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
+PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
+
+
+def die(message, *args):
+    print >> sys.stderr, message % args
+    sys.exit(1)
+
+
+def check_python_version():
+    if sys.version_info < (2, 6):
+        die("Need Python Version >= 2.6")
+
+
+def run_command_with_code(cmd, redirect_output=True, check_exit_code=True):
+    """
+    Runs a command in an out-of-process shell, returning the
+    output of that command.  Working directory is ROOT.
+    """
+    if redirect_output:
+        stdout = subprocess.PIPE
+    else:
+        stdout = None
+
+    proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
+    output = proc.communicate()[0]
+    if check_exit_code and proc.returncode != 0:
+        die('Command "%s" failed.\n%s', ' '.join(cmd), output)
+    return (output, proc.returncode)
+
+
+def run_command(cmd, redirect_output=True, check_exit_code=True):
+    return run_command_with_code(cmd, redirect_output, check_exit_code)[0]
+
+
+class Distro(object):
+
+    def check_cmd(self, cmd):
+        return bool(run_command(['which', cmd], check_exit_code=False).strip())
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if self.check_cmd('easy_install'):
+            print 'Installing virtualenv via easy_install...',
+            if run_command(['easy_install', 'virtualenv']):
+                print 'Succeeded'
+                return
+            else:
+                print 'Failed'
+
+        die('ERROR: virtualenv not found.\n\nDevelopment'
+            ' requires virtualenv, please install it using your'
+            ' favorite package management tool')
+
+    def post_process(self):
+        """Any distribution-specific post-processing gets done here.
+
+        In particular, this is useful for applying patches to code inside
+        the venv."""
+        pass
+
+
+class Debian(Distro):
+    """This covers all Debian-based distributions."""
+
+    def check_pkg(self, pkg):
+        return run_command_with_code(['dpkg', '-l', pkg],
+                                     check_exit_code=False)[1] == 0
+
+    def apt_install(self, pkg, **kwargs):
+        run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs)
+
+    def apply_patch(self, originalfile, patchfile):
+        run_command(['patch', originalfile, patchfile])
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if not self.check_pkg('python-virtualenv'):
+            self.apt_install('python-virtualenv', check_exit_code=False)
+
+        super(Debian, self).install_virtualenv()
+
+
+class Fedora(Distro):
+    """This covers all Fedora-based distributions.
+
+    Includes: Fedora, RHEL, CentOS, Scientific Linux"""
+
+    def check_pkg(self, pkg):
+        return run_command_with_code(['rpm', '-q', pkg],
+                                     check_exit_code=False)[1] == 0
+
+    def yum_install(self, pkg, **kwargs):
+        run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
+
+    def apply_patch(self, originalfile, patchfile):
+        run_command(['patch', originalfile, patchfile])
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if not self.check_pkg('python-virtualenv'):
+            self.yum_install('python-virtualenv', check_exit_code=False)
+
+        super(Fedora, self).install_virtualenv()
+
+
+def get_distro():
+    if os.path.exists('/etc/fedora-release') or \
+       os.path.exists('/etc/redhat-release'):
+        return Fedora()
+    elif os.path.exists('/etc/debian_version'):
+        return Debian()
+    else:
+        return Distro()
+
+
+def check_dependencies():
+    get_distro().install_virtualenv()
+
+
+def create_virtualenv(venv=VENV, no_site_packages=True):
+    """Creates the virtual environment and installs PIP only into the
+    virtual environment
+    """
+    print 'Creating venv...',
+    if no_site_packages:
+        run_command(['virtualenv', '-q', '--no-site-packages', VENV])
+    else:
+        run_command(['virtualenv', '-q', VENV])
+    print 'done.'
+    print 'Installing pip in virtualenv...',
+    if not run_command(['tools/with_venv.sh', 'easy_install',
+                        'pip>1.0']).strip():
+        die("Failed to install pip.")
+    print 'done.'
+
+
+def pip_install(*args):
+    run_command(['tools/with_venv.sh',
+                 'pip', 'install', '--upgrade'] + list(args),
+                redirect_output=False)
+
+
+def install_dependencies(venv=VENV):
+    print 'Installing dependencies with pip (this can take a while)...'
+
+    # First things first, make sure our venv has the latest pip and distribute.
+    pip_install('pip')
+    pip_install('distribute')
+
+    pip_install('-r', PIP_REQUIRES)
+
+    # Tell the virtual env how to "import cinder"
+    pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages",
+                        "cinderclient.pth")
+    f = open(pthfile, 'w')
+    f.write("%s\n" % ROOT)
+
+
+def post_process():
+    get_distro().post_process()
+
+
+def print_help():
+    help = """
+    python-cinderclient development environment setup is complete.
+
+    python-cinderclient development uses virtualenv to track and manage Python
+    dependencies while in development and testing.
+
+    To activate the python-cinderclient virtualenv for the extent of your current
+    shell session you can run:
+
+    $ source .venv/bin/activate
+
+    Or, if you prefer, you can run commands in the virtualenv on a case by case
+    basis by running:
+
+    $ tools/with_venv.sh <your command>
+
+    Also, make test will automatically use the virtualenv.
+    """
+    print help
+
+
+def parse_args():
+    """Parse command-line arguments"""
+    parser = optparse.OptionParser()
+    parser.add_option("-n", "--no-site-packages", dest="no_site_packages",
+        default=False, action="store_true",
+        help="Do not inherit packages from global Python install")
+    return parser.parse_args()
+
+
+def main(argv):
+    (options, args) = parse_args()
+    check_python_version()
+    check_dependencies()
+    create_virtualenv(no_site_packages=options.no_site_packages)
+    install_dependencies()
+    post_process()
+    print_help()
+
+if __name__ == '__main__':
+    main(sys.argv)
diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion
new file mode 100644
index 000000000..060bf1f7d
--- /dev/null
+++ b/tools/nova.bash_completion
@@ -0,0 +1,15 @@
+_cinder()
+{
+    local cur prev opts
+    COMPREPLY=()
+    cur="${COMP_WORDS[COMP_CWORD]}"
+    prev="${COMP_WORDS[COMP_CWORD-1]}"
+
+    opts="$(cinder bash_completion)"
+
+    COMPLETION_CACHE=~/.cinderclient/*/*-cache
+    opts+=" "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
+
+    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+}
+complete -F _cinder cinder
diff --git a/tools/pip-requires b/tools/pip-requires
new file mode 100644
index 000000000..510f2c193
--- /dev/null
+++ b/tools/pip-requires
@@ -0,0 +1,9 @@
+argparse
+coverage
+httplib2
+mock
+nose
+prettytable
+simplejson
+pep8==0.6.1
+unittest2
diff --git a/tools/rfc.sh b/tools/rfc.sh
new file mode 100755
index 000000000..d4dc59745
--- /dev/null
+++ b/tools/rfc.sh
@@ -0,0 +1,145 @@
+#!/bin/sh -e
+# Copyright (c) 2010-2011 Gluster, Inc. <http://www.gluster.com>
+# This initial version of this file was taken from the source tree
+# of GlusterFS. It was not directly attributed, but is assumed to be
+# Copyright (c) 2010-2011 Gluster, Inc and release GPLv3
+# Subsequent modifications are Copyright (c) 2011 OpenStack, LLC.
+#
+# GlusterFS is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; either version 3 of the License,
+# or (at your option) any later version.
+#
+# GlusterFS is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+
+branch="master";
+
+set_hooks_commit_msg()
+{
+    top_dir=`git rev-parse --show-toplevel`
+    f="${top_dir}/.git/hooks/commit-msg";
+    u="https://review.openstack.org/tools/hooks/commit-msg";
+
+    if [ -x "$f" ]; then
+        return;
+    fi
+
+    curl -o $f $u || wget -O $f $u;
+
+    chmod +x $f;
+
+    GIT_EDITOR=true git commit --amend
+}
+
+add_remote()
+{
+    username=$1
+    project=$2
+
+    echo "No remote set, testing ssh://$username@review.openstack.org:29418"
+    if project_list=`ssh -p29418 -o StrictHostKeyChecking=no $username@review.openstack.org gerrit ls-projects 2>/dev/null`
+    then
+        echo "$username@review.openstack.org:29418 worked."
+        if echo $project_list | grep $project >/dev/null
+        then
+            echo "Creating a git remote called gerrit that maps to:"
+            echo "  ssh://$username@review.openstack.org:29418/$project"
+            git remote add gerrit ssh://$username@review.openstack.org:29418/$project
+        else
+            echo "The current project name, $project, is not a known project."
+            echo "Please either reclone from github/gerrit or create a"
+            echo "remote named gerrit that points to the intended project."
+            return 1
+        fi
+
+        return 0
+    fi
+    return 1
+}
+
+check_remote()
+{
+    if ! git remote | grep gerrit >/dev/null 2>&1
+    then
+        origin_project=`git remote show origin | grep 'Fetch URL' | perl -nle '@fields = split(m|[:/]|); $len = $#fields; print $fields[$len-1], "/", $fields[$len];'`
+        if add_remote $USERNAME $origin_project
+        then
+            return 0
+        else
+            echo "Your local name doesn't work on Gerrit."
+            echo -n "Enter Gerrit username (same as launchpad): "
+            read gerrit_user
+            if add_remote $gerrit_user $origin_project
+            then
+                return 0
+            else
+                echo "Can't infer where gerrit is - please set a remote named"
+                echo "gerrit manually and then try again."
+                echo
+                echo "For more information, please see:"
+                echo "\thttp://wiki.openstack.org/GerritWorkflow"
+                exit 1
+            fi
+        fi
+    fi
+}
+
+rebase_changes()
+{
+    git fetch;
+
+    GIT_EDITOR=true git rebase -i origin/$branch || exit $?;
+}
+
+
+assert_diverge()
+{
+    if ! git diff origin/$branch..HEAD | grep -q .
+    then
+	echo "No changes between the current branch and origin/$branch."
+	exit 1
+    fi
+}
+
+
+main()
+{
+    set_hooks_commit_msg;
+
+    check_remote;
+
+    rebase_changes;
+
+    assert_diverge;
+
+    bug=$(git show --format='%s %b' | perl -nle 'if (/\b([Bb]ug|[Ll][Pp])\s*[#:]?\s*(\d+)/) {print "$2"; exit}')
+
+    bp=$(git show --format='%s %b' | perl -nle 'if (/\b([Bb]lue[Pp]rint|[Bb][Pp])\s*[#:]?\s*([0-9a-zA-Z-_]+)/) {print "$2"; exit}')
+
+    if [ "$DRY_RUN" = 1 ]; then
+        drier='echo -e Please use the following command to send your commits to review:\n\n'
+    else
+        drier=
+    fi
+
+    local_branch=`git branch | grep -Ei "\* (.*)" | cut -f2 -d' '`
+    if [ -z "$bug" ]; then
+	if [ -z "$bp" ]; then
+            $drier git push gerrit HEAD:refs/for/$branch/$local_branch;
+	else
+	    $drier git push gerrit HEAD:refs/for/$branch/bp/$bp;
+	fi
+    else
+        $drier git push gerrit HEAD:refs/for/$branch/bug/$bug;
+    fi
+}
+
+main "$@"
diff --git a/tools/with_venv.sh b/tools/with_venv.sh
new file mode 100755
index 000000000..c8d2940fc
--- /dev/null
+++ b/tools/with_venv.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+TOOLS=`dirname $0`
+VENV=$TOOLS/../.venv
+source $VENV/bin/activate && $@
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 000000000..561bf012e
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,14 @@
+[tox]
+envlist = py26,py27
+
+[testenv]
+deps = -r{toxinidir}/tools/pip-requires
+commands = /bin/bash run_tests.sh -N
+
+[testenv:pep8]
+deps = pep8
+commands = /bin/bash run_tests.sh -N --pep8
+
+[testenv:coverage]
+deps = coverage
+commands = /bin/bash run_tests.sh -N --coverage