From 454daa2cba9058d0e744b398ce6af0135b5b27b7 Mon Sep 17 00:00:00 2001
From: Brian Lamar <brian.lamar@rackspace.com>
Date: Wed, 3 Aug 2011 17:41:33 -0400
Subject: [PATCH] Tests working again...merged in some work we did earlier.

---
 novaclient/__init__.py              |   17 -
 novaclient/base.py                  |  235 +++++
 novaclient/client.py                |  157 +++
 novaclient/{v1_0 => }/exceptions.py |   25 +-
 novaclient/shell.py                 |  204 ++++
 novaclient/utils.py                 |   43 +
 novaclient/v1_0/__init__.py         |   74 --
 novaclient/v1_0/accounts.py         |    7 +-
 novaclient/v1_0/backup_schedules.py |    3 +-
 novaclient/v1_0/base.py             |  148 +--
 novaclient/v1_0/client.py           |  176 +---
 novaclient/v1_0/flavors.py          |    2 +-
 novaclient/v1_0/images.py           |   23 +-
 novaclient/v1_0/ipgroups.py         |    2 +-
 novaclient/v1_0/servers.py          |   44 +-
 novaclient/v1_0/shell.py            | 1417 ++++++++++++---------------
 novaclient/v1_0/zones.py            |    5 +-
 novaclient/v1_1/__init__.py         |   66 --
 novaclient/v1_1/base.py             |    2 +-
 novaclient/v1_1/client.py           |  168 +---
 novaclient/v1_1/exceptions.py       |  100 --
 novaclient/v1_1/flavors.py          |   10 +-
 novaclient/v1_1/images.py           |   38 +-
 novaclient/v1_1/servers.py          |  119 ++-
 novaclient/v1_1/shell.py            |  812 ++++++---------
 tests/fakes.py                      |   66 ++
 tests/test_base.py                  |   60 ++
 tests/test_http.py                  |   58 ++
 tests/test_shell.py                 |   39 +
 tests/utils.py                      |    5 +
 tests/v1_0/fakes.py                 |  166 ++--
 tests/v1_0/test_accounts.py         |   36 +-
 tests/v1_0/test_auth.py             |  105 +-
 tests/v1_0/test_backup_schedules.py |   96 +-
 tests/v1_0/test_base.py             |   61 --
 tests/v1_0/test_client.py           |   52 -
 tests/v1_0/test_flavors.py          |   56 +-
 tests/v1_0/test_images.py           |   70 +-
 tests/v1_0/test_ipgroups.py         |   76 +-
 tests/v1_0/test_servers.py          |  281 +++---
 tests/v1_0/test_shell.py            |  651 ++++++------
 tests/v1_0/test_zones.py            |  121 ++-
 tests/v1_0/testfile.txt             |    2 +-
 tests/v1_1/fakes.py                 |  263 +----
 tests/v1_1/test_auth.py             |   74 ++
 tests/v1_1/test_base.py             |   61 --
 tests/v1_1/test_client.py           |   52 -
 tests/v1_1/test_flavors.py          |   56 +-
 tests/v1_1/test_images.py           |   67 +-
 tests/v1_1/test_servers.py          |  191 ++--
 tests/v1_1/test_shell.py            |  366 ++++---
 tests/v1_1/testfile.txt             |    2 +-
 52 files changed, 3172 insertions(+), 3858 deletions(-)
 create mode 100644 novaclient/base.py
 create mode 100644 novaclient/client.py
 rename novaclient/{v1_0 => }/exceptions.py (81%)
 create mode 100644 novaclient/shell.py
 create mode 100644 novaclient/utils.py
 delete mode 100644 novaclient/v1_1/exceptions.py
 create mode 100644 tests/fakes.py
 create mode 100644 tests/test_base.py
 create mode 100644 tests/test_http.py
 create mode 100644 tests/test_shell.py
 create mode 100644 tests/utils.py
 delete mode 100644 tests/v1_0/test_base.py
 delete mode 100644 tests/v1_0/test_client.py
 create mode 100644 tests/v1_1/test_auth.py
 delete mode 100644 tests/v1_1/test_base.py
 delete mode 100644 tests/v1_1/test_client.py

diff --git a/novaclient/__init__.py b/novaclient/__init__.py
index 08bd9d9a7..e69de29bb 100644
--- a/novaclient/__init__.py
+++ b/novaclient/__init__.py
@@ -1,17 +0,0 @@
-# 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.
-
-__version__ = '2.5'
diff --git a/novaclient/base.py b/novaclient/base.py
new file mode 100644
index 000000000..7928f8d5c
--- /dev/null
+++ b/novaclient/base.py
@@ -0,0 +1,235 @@
+# 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.
+"""
+
+from novaclient import exceptions
+
+
+# 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
+    (UUID) as a parameter when dealing with relationships.
+    """
+
+    # Try to return the object's UUID first, if we have a UUID.
+    try:
+        if obj.uuid:
+            return obj.uuid
+    except AttributeError:
+        pass
+    try:
+        return obj.id
+    except AttributeError:
+        return obj
+
+
+class Manager(object):
+    """
+    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
+        return [obj_class(self, res)
+                for res in body[response_key] if res]
+
+    def _get(self, url, response_key):
+        resp, body = self.api.client.get(url)
+        return self.resource_class(self, body[response_key])
+
+    def _create(self, url, body, response_key, return_raw=False):
+        resp, body = self.api.client.post(url, body=body)
+        if return_raw:
+            return body[response_key]
+        return self.resource_class(self, body[response_key])
+
+    def _delete(self, url):
+        resp, body = self.api.client.delete(url)
+
+    def _update(self, url, body):
+        resp, body = self.api.client.put(url, body=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.
+        """
+        rl = self.findall(**kwargs)
+        try:
+            return rl[0]
+        except IndexError:
+            msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+            raise exceptions.NotFound(404, msg)
+
+    def findall(self, **kwargs):
+        """
+        Find all items with attributes matching ``**kwargs``.
+
+        This isn't very efficient: it loads the entire list then filters on
+        the Python side.
+        """
+        found = []
+        searches = kwargs.items()
+
+        for obj in self.list():
+            try:
+                if all(getattr(obj, attr) == value
+                                    for (attr, value) in searches):
+                    found.append(obj)
+            except AttributeError:
+                continue
+
+        return found
+
+
+class BootingManagerWithFind(ManagerWithFind):
+    """Like a `ManagerWithFind`, but has the ability to boot servers."""
+    def _boot(self, resource_url, response_key, name, image, flavor,
+              ipgroup=None, meta=None, files=None, zone_blob=None,
+              reservation_id=None, return_raw=False, min_count=None,
+              max_count=None):
+        """
+        Create (boot) a new server.
+
+        :param name: Something to name the server.
+        :param image: The :class:`Image` to boot with.
+        :param flavor: The :class:`Flavor` to boot onto.
+        :param ipgroup: An initial :class:`IPGroup` for this server.
+        :param meta: A dict of arbitrary key/value metadata to store for this
+                     server. A maximum of five entries is allowed, and both
+                     keys and values must be 255 characters or less.
+        :param files: A dict of files to overrwrite on the server upon boot.
+                      Keys are file names (i.e. ``/etc/passwd``) and values
+                      are the file contents (either as a string or as a
+                      file-like object). A maximum of five entries is allowed,
+                      and each file must be 10k or less.
+        :param zone_blob: a single (encrypted) string which is used internally
+                      by Nova for routing between Zones. Users cannot populate
+                      this field.
+        :param reservation_id: a UUID for the set of servers being requested.
+        :param return_raw: If True, don't try to coearse the result into
+                           a Resource object.
+        """
+        body = {"server": {
+            "name": name,
+            "imageId": getid(image),
+            "flavorId": getid(flavor),
+        }}
+        if ipgroup:
+            body["server"]["sharedIpGroupId"] = getid(ipgroup)
+        if meta:
+            body["server"]["metadata"] = meta
+        if reservation_id:
+            body["server"]["reservation_id"] = reservation_id
+        if zone_blob:
+            body["server"]["zone_blob"] = zone_blob
+
+        if not min_count:
+            min_count = 1
+        if not max_count:
+            max_count = min_count
+        body["server"]["min_count"] = min_count
+        body["server"]["max_count"] = max_count
+
+        # Files are a slight bit tricky. They're passed in a "personality"
+        # list to the POST. Each item is a dict giving a file name and the
+        # base64-encoded contents of the file. We want to allow passing
+        # either an open file *or* some contents as files here.
+        if files:
+            personality = body['server']['personality'] = []
+            for filepath, file_or_string in files.items():
+                if hasattr(file_or_string, 'read'):
+                    data = file_or_string.read()
+                else:
+                    data = file_or_string
+                personality.append({
+                    'path': filepath,
+                    'contents': data.encode('base64'),
+                })
+
+        return self._create(resource_url, body, response_key,
+                            return_raw=return_raw)
+
+
+class Resource(object):
+    """
+    A resource represents a particular instance of an object (server, flavor,
+    etc). This is pretty much just a bag for attributes.
+    """
+    def __init__(self, manager, info):
+        self.manager = manager
+        self._info = info
+        self._add_details(info)
+
+    def _add_details(self, info):
+        for (k, v) in info.iteritems():
+            setattr(self, k, v)
+
+    def __getattr__(self, k):
+        self.get()
+        if k not in self.__dict__:
+            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):
+        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
diff --git a/novaclient/client.py b/novaclient/client.py
new file mode 100644
index 000000000..fc1c98401
--- /dev/null
+++ b/novaclient/client.py
@@ -0,0 +1,157 @@
+# Copyright 2010 Jacob Kaplan-Moss
+"""
+OpenStack Client interface. Handles the REST calls and responses.
+"""
+
+import time
+import urlparse
+import urllib
+import httplib2
+import logging
+
+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 novaclient import exceptions
+
+
+_logger = logging.getLogger(__name__)
+
+
+class HTTPClient(httplib2.Http):
+
+    USER_AGENT = 'python-novaclient'
+
+    def __init__(self, user, apikey, projectid, auth_url, timeout=None):
+        super(HTTPClient, self).__init__(timeout=timeout)
+        self.user = user
+        self.apikey = apikey
+        self.projectid = projectid
+        self.auth_url = auth_url
+        self.version = 'v1.0'
+
+        self.management_url = None
+        self.auth_token = None
+
+        # httplib2 overrides
+        self.force_exception_to_status_code = True
+
+    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))
+        _logger.debug("RESP:%s %s\n", resp, body)
+
+    def request(self, *args, **kwargs):
+        kwargs.setdefault('headers', {})
+        kwargs['headers']['User-Agent'] = self.USER_AGENT
+        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, e:
+                pass
+        else:
+            body = None
+
+        if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
+            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):
+        url = self._munge_get_url(url)
+        return self._cs_request(url, 'GET', **kwargs)
+
+    def post(self, url, **kwargs):
+        return self._cs_request(url, 'POST', **kwargs)
+
+    def put(self, url, **kwargs):
+        return self._cs_request(url, 'PUT', **kwargs)
+
+    def delete(self, url, **kwargs):
+        return self._cs_request(url, 'DELETE', **kwargs)
+
+    def authenticate(self):
+        scheme, netloc, path, query, frag = urlparse.urlsplit(
+                                                    self.auth_url)
+        path_parts = path.split('/')
+        for part in path_parts:
+            if len(part) > 0 and part[0] == 'v':
+                self.version = part
+                break
+
+        headers = {'X-Auth-User': self.user,
+                   'X-Auth-Key': self.apikey}
+        if self.projectid:
+            headers['X-Auth-Project-Id'] = self.projectid
+        resp, body = self.request(self.auth_url, 'GET', headers=headers)
+        self.management_url = resp['x-server-management-url']
+
+        self.auth_token = resp['x-auth-token']
+
+    def _munge_get_url(self, url):
+        """
+        Munge GET URLs to always return uncached content.
+
+        The OpenStack Compute API caches data *very* agressively and doesn't
+        respect cache headers. To avoid stale data, then, we append a little
+        bit of nonsense onto GET parameters; this appears to force the data not
+        to be cached.
+        """
+        scheme, netloc, path, query, frag = urlparse.urlsplit(url)
+        query = urlparse.parse_qsl(query)
+        query.append(('fresh', str(time.time())))
+        query = urllib.urlencode(query)
+        return urlparse.urlunsplit((scheme, netloc, path, query, frag))
diff --git a/novaclient/v1_0/exceptions.py b/novaclient/exceptions.py
similarity index 81%
rename from novaclient/v1_0/exceptions.py
rename to novaclient/exceptions.py
index 1709d806f..ae456decd 100644
--- a/novaclient/v1_0/exceptions.py
+++ b/novaclient/exceptions.py
@@ -3,7 +3,12 @@
 Exception definitions.
 """
 
-class OpenStackException(Exception):
+
+class CommandError(Exception):
+    pass
+
+
+class ClientException(Exception):
     """
     The base exception class for all exceptions this library raises.
     """
@@ -16,7 +21,7 @@ class OpenStackException(Exception):
         return "%s (HTTP %s)" % (self.message, self.code)
 
 
-class BadRequest(OpenStackException):
+class BadRequest(ClientException):
     """
     HTTP 400 - Bad request: you sent some malformed data.
     """
@@ -24,7 +29,7 @@ class BadRequest(OpenStackException):
     message = "Bad request"
 
 
-class Unauthorized(OpenStackException):
+class Unauthorized(ClientException):
     """
     HTTP 401 - Unauthorized: bad credentials.
     """
@@ -32,7 +37,7 @@ class Unauthorized(OpenStackException):
     message = "Unauthorized"
 
 
-class Forbidden(OpenStackException):
+class Forbidden(ClientException):
     """
     HTTP 403 - Forbidden: your credentials don't give you access to this
     resource.
@@ -41,7 +46,7 @@ class Forbidden(OpenStackException):
     message = "Forbidden"
 
 
-class NotFound(OpenStackException):
+class NotFound(ClientException):
     """
     HTTP 404 - Not found
     """
@@ -49,7 +54,7 @@ class NotFound(OpenStackException):
     message = "Not found"
 
 
-class OverLimit(OpenStackException):
+class OverLimit(ClientException):
     """
     HTTP 413 - Over limit: you're over the API limits for this time period.
     """
@@ -58,7 +63,7 @@ class OverLimit(OpenStackException):
 
 
 # NotImplemented is a python keyword.
-class HTTPNotImplemented(OpenStackException):
+class HTTPNotImplemented(ClientException):
     """
     HTTP 501 - Not Implemented: the server does not support this operation.
     """
@@ -69,7 +74,7 @@ class HTTPNotImplemented(OpenStackException):
 # 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 OpenStackException.__subclasses__())
+#                      for c in ClientException.__subclasses__())
 #
 # Instead, we have to hardcode it:
 _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
@@ -78,7 +83,7 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
 
 def from_response(response, body):
     """
-    Return an instance of an OpenStackException or subclass
+    Return an instance of an ClientException or subclass
     based on an httplib2 response.
 
     Usage::
@@ -87,7 +92,7 @@ def from_response(response, body):
         if resp.status != 200:
             raise exception_from_response(resp, body)
     """
-    cls = _code_map.get(response.status, OpenStackException)
+    cls = _code_map.get(response.status, ClientException)
     if body:
         message = "n/a"
         details = "n/a"
diff --git a/novaclient/shell.py b/novaclient/shell.py
new file mode 100644
index 000000000..0ab0d6945
--- /dev/null
+++ b/novaclient/shell.py
@@ -0,0 +1,204 @@
+# 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.
+
+"""
+Command-line interface to the OpenStack Nova API.
+"""
+
+import argparse
+import httplib2
+import os
+import prettytable
+import sys
+
+from novaclient import exceptions
+from novaclient import utils
+from novaclient.v1_0 import shell as shell_v1_0
+from novaclient.v1_1 import shell as shell_v1_1
+
+
+def env(e):
+    return os.environ.get(e, '')
+
+
+class OpenStackComputeShell(object):
+
+    # Hook for the test suite to inject a fake server.
+    _api_class = None
+
+    def get_base_parser(self):
+        parser = argparse.ArgumentParser(
+            prog='nova',
+            description=__doc__.strip(),
+            epilog='See "nova help COMMAND" '\
+                   'for help on a specific command.',
+            add_help=False,
+            formatter_class=OpenStackHelpFormatter,
+        )
+
+        # Global arguments
+        parser.add_argument('-h', '--help',
+            action='help',
+            help=argparse.SUPPRESS,
+        )
+
+        parser.add_argument('--debug',
+            default=False,
+            action='store_true',
+            help=argparse.SUPPRESS)
+
+        parser.add_argument('--username',
+            default=env('NOVA_USERNAME'),
+            help='Defaults to env[NOVA_USERNAME].')
+
+        parser.add_argument('--apikey',
+            default=env('NOVA_API_KEY'),
+            help='Defaults to env[NOVA_API_KEY].')
+
+        parser.add_argument('--projectid',
+            default=env('NOVA_PROJECT_ID'),
+            help='Defaults to env[NOVA_PROJECT_ID].')
+
+        parser.add_argument('--url',
+            default=env('NOVA_URL'),
+            help='Defaults to env[NOVA_URL].')
+
+        parser.add_argument('--version',
+            default='1.1',
+            help='Accepts 1.0 or 1.1, defaults to 1.1')
+
+        return parser
+
+    def get_subcommand_parser(self, version):
+        parser = self.get_base_parser()
+
+        self.subcommands = {}
+        subparsers = parser.add_subparsers(metavar='<subcommand>')
+
+        actions_module = {
+            '1.0': shell_v1_0,
+            '1.1': shell_v1_1,
+        }[version]
+
+        self._find_actions(subparsers, actions_module)
+        self._find_actions(subparsers, self)
+
+        return parser
+
+    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 main(self, argv):
+        # Parse args once to find version
+        parser = self.get_base_parser()
+        (options, args) = parser.parse_known_args(argv)
+
+        # build available subcommands based on version
+        subcommand_parser = self.get_subcommand_parser(options.version)
+        self.parser = subcommand_parser
+
+        # Parse args again and call whatever callback was selected
+        args = subcommand_parser.parse_args(argv)
+
+        # Deal with global arguments
+        if args.debug:
+            httplib2.debuglevel = 1
+
+        # Short-circuit and deal with help right away.
+        if args.func == self.do_help:
+            self.do_help(args)
+            return 0
+
+        user, apikey, projectid, url = args.username, args.apikey, \
+                                       args.projectid, args.url
+
+        #FIXME(usrleon): Here should be restrict for project id same as
+        # for username or apikey but for compatibility it is not.
+
+        if not user:
+            raise exceptions.CommandError("You must provide a username, either via "
+                               "--username or via env[NOVA_USERNAME]")
+        if not apikey:
+            raise exceptions.CommandError("You must provide an API key, either via "
+                               "--apikey or via env[NOVA_API_KEY]")
+
+        self.cs = self.get_api_class()(user, apikey, projectid, url)
+        try:
+            self.cs.authenticate()
+        except exceptions.Unauthorized:
+            raise exceptions.CommandError("Invalid OpenStack Nova credentials.")
+
+        args.func(self.cs, args)
+
+    def get_api_class(self):
+        return self._api_class or shell_v1_0.CLIENT_CLASS
+
+    @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 exceptions.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:
+        OpenStackComputeShell().main(sys.argv[1:])
+
+    except Exception, e:
+        if httplib2.debuglevel == 1:
+            raise  # dump stack.
+        else:
+            print >> sys.stderr, e
+        sys.exit(1)
diff --git a/novaclient/utils.py b/novaclient/utils.py
new file mode 100644
index 000000000..4397046d0
--- /dev/null
+++ b/novaclient/utils.py
@@ -0,0 +1,43 @@
+
+import prettytable
+
+
+# Decorator for cli-args
+def arg(*args, **kwargs):
+    def _decorator(func):
+        # Because of the sematics of decorator composition if we just append
+        # to the options list positional options will appear to be backwards.
+        func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
+        return func
+    return _decorator
+
+
+def pretty_choice_list(l):
+    return ', '.join("'%s'" % i for i in l)
+
+
+def print_list(objs, fields, formatters={}):
+    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:
+                field_name = field.lower().replace(' ', '_')
+                data = getattr(o, field_name, '')
+                row.append(data)
+        pt.add_row(row)
+
+    pt.printt(sortby=fields[0])
+
+
+def print_dict(d):
+    pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
+    pt.aligns = ['l', 'l']
+    [pt.add_row(list(r)) for r in d.iteritems()]
+    pt.printt(sortby='Property')
+
+
diff --git a/novaclient/v1_0/__init__.py b/novaclient/v1_0/__init__.py
index 7b2c9737f..e69de29bb 100644
--- a/novaclient/v1_0/__init__.py
+++ b/novaclient/v1_0/__init__.py
@@ -1,74 +0,0 @@
-# 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.
-
-from novaclient.v1_0 import accounts
-from novaclient.v1_0 import backup_schedules
-from novaclient.v1_0 import client
-from novaclient.v1_0 import exceptions
-from novaclient.v1_0 import flavors
-from novaclient.v1_0 import images
-from novaclient.v1_0 import ipgroups
-from novaclient.v1_0 import servers
-from novaclient.v1_0 import zones
-
-
-class Client(object):
-    """
-    Top-level object to access the OpenStack Compute v1.0 API.
-
-    Create an instance with your creds::
-
-        >>> os = novaclient.v1_0.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
-
-    Then call methods on its managers::
-
-        >>> os.servers.list()
-        ...
-        >>> os.flavors.list()
-        ...
-
-    &c.
-    """
-
-    def __init__(self, username, apikey, projectid, auth_url=None, timeout=None):
-        """Initialize v1.0 Openstack Client."""
-        self.backup_schedules = backup_schedules.BackupScheduleManager(self)
-        self.flavors = flavors.FlavorManager(self)
-        self.images = images.ImageManager(self)
-        self.ipgroups = ipgroups.IPGroupManager(self)
-        self.servers = servers.ServerManager(self)
-        self.zones = zones.ZoneManager(self)
-        self.accounts = accounts.AccountManager(self)
-
-        auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0"
-
-        self.client = client.HTTPClient(username,
-                                        apikey,
-                                        projectid,
-                                        auth_url,
-                                        timeout=timeout)
-
-    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:`novaclient.Unauthorized` if the
-        credentials are wrong.
-        """
-        self.client.authenticate()
diff --git a/novaclient/v1_0/accounts.py b/novaclient/v1_0/accounts.py
index 264ce8431..be162d9bf 100644
--- a/novaclient/v1_0/accounts.py
+++ b/novaclient/v1_0/accounts.py
@@ -1,10 +1,13 @@
-from novaclient.v1_0 import base
+
+from novaclient import base
+from novaclient.v1_0 import base as local_base
+
 
 class Account(base.Resource):
     pass
 
-class AccountManager(base.BootingManagerWithFind):
 
+class AccountManager(local_base.BootingManagerWithFind):
     resource_class = Account
 
     def create_instance_for(self, account_id, name, image, flavor,
diff --git a/novaclient/v1_0/backup_schedules.py b/novaclient/v1_0/backup_schedules.py
index 78f4d49f2..2d8aea824 100644
--- a/novaclient/v1_0/backup_schedules.py
+++ b/novaclient/v1_0/backup_schedules.py
@@ -3,7 +3,8 @@
 Backup Schedule interface.
 """
 
-from novaclient.v1_0 import base
+from novaclient import base
+
 
 BACKUP_WEEKLY_DISABLED = 'DISABLED'
 BACKUP_WEEKLY_SUNDAY = 'SUNDAY'
diff --git a/novaclient/v1_0/base.py b/novaclient/v1_0/base.py
index 3dbec636d..3ff7ac2d6 100644
--- a/novaclient/v1_0/base.py
+++ b/novaclient/v1_0/base.py
@@ -19,7 +19,9 @@
 Base utilities to build API operation managers and objects on top of.
 """
 
-from novaclient.v1_0 import exceptions
+from novaclient import base
+from novaclient import exceptions
+
 
 # Python 2.4 compat
 try:
@@ -29,103 +31,7 @@ except NameError:
         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
-    (UUID) as a parameter when dealing with relationships.
-    """
-
-    # Try to return the object's UUID first, if we have a UUID.
-    try:
-        if obj.uuid:
-            return obj.uuid
-    except AttributeError:
-        pass
-    try:
-        return obj.id
-    except AttributeError:
-        return obj
-
-
-class Manager(object):
-    """
-    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
-        return [obj_class(self, res)
-                for res in body[response_key] if res]
-
-    def _get(self, url, response_key):
-        resp, body = self.api.client.get(url)
-        return self.resource_class(self, body[response_key])
-
-    def _create(self, url, body, response_key, return_raw=False):
-        resp, body = self.api.client.post(url, body=body)
-        if return_raw:
-            return body[response_key]
-        return self.resource_class(self, body[response_key])
-
-    def _delete(self, url):
-        resp, body = self.api.client.delete(url)
-
-    def _update(self, url, body):
-        resp, body = self.api.client.put(url, body=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.
-        """
-        rl = self.findall(**kwargs)
-        try:
-            return rl[0]
-        except IndexError:
-            raise exceptions.NotFound(404, "No %s matching %s." %
-                    (self.resource_class.__name__, kwargs))
-
-    def findall(self, **kwargs):
-        """
-        Find all items with attributes matching ``**kwargs``.
-
-        This isn't very efficient: it loads the entire list then filters on
-        the Python side.
-        """
-        found = []
-        searches = kwargs.items()
-
-        for obj in self.list():
-            try:
-                if all(getattr(obj, attr) == value
-                                    for (attr, value) in searches):
-                    found.append(obj)
-            except AttributeError:
-                continue
-
-        return found
-
-
-class BootingManagerWithFind(ManagerWithFind):
+class BootingManagerWithFind(base.ManagerWithFind):
     """Like a `ManagerWithFind`, but has the ability to boot servers."""
     def _boot(self, resource_url, response_key, name, image, flavor,
               ipgroup=None, meta=None, files=None, zone_blob=None,
@@ -155,11 +61,11 @@ class BootingManagerWithFind(ManagerWithFind):
         """
         body = {"server": {
             "name": name,
-            "imageId": getid(image),
-            "flavorId": getid(flavor),
+            "imageId": base.getid(image),
+            "flavorId": base.getid(flavor),
         }}
         if ipgroup:
-            body["server"]["sharedIpGroupId"] = getid(ipgroup)
+            body["server"]["sharedIpGroupId"] = base.getid(ipgroup)
         if meta:
             body["server"]["metadata"] = meta
         if reservation_id:
@@ -192,43 +98,3 @@ class BootingManagerWithFind(ManagerWithFind):
 
         return self._create(resource_url, body, response_key,
                             return_raw=return_raw)
-
-
-class Resource(object):
-    """
-    A resource represents a particular instance of an object (server, flavor,
-    etc). This is pretty much just a bag for attributes.
-    """
-    def __init__(self, manager, info):
-        self.manager = manager
-        self._info = info
-        self._add_details(info)
-
-    def _add_details(self, info):
-        for (k, v) in info.iteritems():
-            setattr(self, k, v)
-
-    def __getattr__(self, k):
-        self.get()
-        if k not in self.__dict__:
-            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):
-        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
diff --git a/novaclient/v1_0/client.py b/novaclient/v1_0/client.py
index 202f6da56..f7e203fae 100644
--- a/novaclient/v1_0/client.py
+++ b/novaclient/v1_0/client.py
@@ -1,154 +1,60 @@
-# Copyright 2010 Jacob Kaplan-Moss
-"""
-OpenStack Client interface. Handles the REST calls and responses.
-"""
 
-import time
-import urlparse
-import urllib
-import httplib2
-import logging
 
-try:
-    import json
-except ImportError:
-    import simplejson as json
+from novaclient import client
+from novaclient.v1_0 import accounts
+from novaclient.v1_0 import backup_schedules
+from novaclient.v1_0 import flavors
+from novaclient.v1_0 import images
+from novaclient.v1_0 import ipgroups
+from novaclient.v1_0 import servers
+from novaclient.v1_0 import zones
 
-# Python 2.5 compat fix
-if not hasattr(urlparse, 'parse_qsl'):
-    import cgi
-    urlparse.parse_qsl = cgi.parse_qsl
 
-import novaclient
-from novaclient.v1_0 import exceptions
 
-_logger = logging.getLogger(__name__)
+class Client(object):
+    """
+    Top-level object to access the OpenStack Compute API.
 
-class HTTPClient(httplib2.Http):
+    Create an instance with your creds::
 
-    USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
+        >>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
 
-    def __init__(self, user, apikey, projectid, auth_url, timeout=None):
-        super(HTTPClient, self).__init__(timeout=timeout)
-        self.user = user
-        self.apikey = apikey
-        self.projectid = projectid
-        self.auth_url = auth_url
-        self.version = 'v1.0'
+    Then call methods on its managers::
 
-        self.management_url = None
-        self.auth_token = None
+        >>> client.servers.list()
+        ...
+        >>> client.flavors.list()
+        ...
 
-        # httplib2 overrides
-        self.force_exception_to_status_code = True
+    """
 
-    def http_log(self, args, kwargs, resp, body):
-        if not _logger.isEnabledFor(logging.DEBUG):
-            return
+    def __init__(self, username, api_key, project_id, auth_url=None,
+                 timeout=None):
 
-        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)
+        self.accounts = accounts.AccountManager(self)
+        self.backup_schedules = backup_schedules.BackupScheduleManager(self)
+        self.flavors = flavors.FlavorManager(self)
+        self.images = images.ImageManager(self)
+        self.ipgroups = ipgroups.IPGroupManager(self)
+        self.servers = servers.ServerManager(self)
+        self.zones = zones.ZoneManager(self)
 
-        for element in kwargs['headers']:
-            string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element]))
+        _auth_url = auth_url or 'https://auth.api.rackspacecloud.com/v1.0'
 
-        _logger.debug("REQ: %s\n" % "".join(string_parts))
-        _logger.debug("RESP:%s %s\n", resp,body)
-
-    def request(self, *args, **kwargs):
-        kwargs.setdefault('headers', {})
-        kwargs['headers']['User-Agent'] = self.USER_AGENT
-        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, e:
-                pass
-        else:
-            body = None
-
-        if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
-            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):
-        url = self._munge_get_url(url)
-        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)
+        self.client = client.HTTPClient(username,
+                                        api_key,
+                                        project_id,
+                                        _auth_url,
+                                        timeout=timeout)
 
     def authenticate(self):
-        scheme, netloc, path, query, frag = urlparse.urlsplit(
-                                                    self.auth_url)
-        path_parts = path.split('/')
-        for part in path_parts:
-            if len(part) > 0 and part[0] == 'v':
-                self.version = part
-                break
-
-        headers = {'X-Auth-User': self.user,
-                   'X-Auth-Key': self.apikey}
-        if self.projectid:
-            headers['X-Auth-Project-Id'] = self.projectid
-        resp, body = self.request(self.auth_url, 'GET', headers=headers)
-        self.management_url = resp['x-server-management-url']
-
-        self.auth_token = resp['x-auth-token']
-
-    def _munge_get_url(self, url):
         """
-        Munge GET URLs to always return uncached content.
+        Authenticate against the server.
 
-        The OpenStack Nova API caches data *very* agressively and doesn't
-        respect cache headers. To avoid stale data, then, we append a little
-        bit of nonsense onto GET parameters; this appears to force the data not
-        to be cached.
+        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.
         """
-        scheme, netloc, path, query, frag = urlparse.urlsplit(url)
-        query = urlparse.parse_qsl(query)
-        query.append(('fresh', str(time.time())))
-        query = urllib.urlencode(query)
-        return urlparse.urlunsplit((scheme, netloc, path, query, frag))
+        self.client.authenticate()
diff --git a/novaclient/v1_0/flavors.py b/novaclient/v1_0/flavors.py
index 4dc4ac994..f1b495804 100644
--- a/novaclient/v1_0/flavors.py
+++ b/novaclient/v1_0/flavors.py
@@ -3,7 +3,7 @@
 Flavor interface.
 """
 
-from novaclient.v1_0 import base
+from novaclient import base
 
 
 class Flavor(base.Resource):
diff --git a/novaclient/v1_0/images.py b/novaclient/v1_0/images.py
index ec36fe34a..706915828 100644
--- a/novaclient/v1_0/images.py
+++ b/novaclient/v1_0/images.py
@@ -3,7 +3,7 @@
 Image interface.
 """
 
-from novaclient.v1_0 import base
+from novaclient import base
 
 
 class Image(base.Resource):
@@ -46,8 +46,7 @@ class ImageManager(base.ManagerWithFind):
             detail = "/detail"
         return self._list("/images%s" % detail, "images")
 
-
-    def create(self, server, name, image_type=None, backup_type=None, rotation=None):
+    def create(self, server, name):
         """
         Create a new image by snapshotting a running :class:`Server`
 
@@ -55,23 +54,7 @@ class ImageManager(base.ManagerWithFind):
         :param server: The :class:`Server` (or its ID) to make a snapshot of.
         :rtype: :class:`Image`
         """
-        if image_type is None:
-            image_type = "snapshot"
-
-        if image_type not in ("backup", "snapshot"):
-            raise Exception("Invalid image_type: must be backup or snapshot")
-
-        if image_type == "backup":
-            if not rotation:
-                raise Exception("rotation is required for backups")
-            elif not backup_type:
-                raise Exception("backup_type required for backups")
-            elif backup_type not in ("daily", "weekly"):
-                raise Exception("Invalid backup_type: must be daily or weekly")
-
-        data = {"image": {"serverId": base.getid(server), "name": name,
-                          "image_type": image_type, "backup_type": backup_type,
-                          "rotation": rotation}}
+        data = {"image": {"serverId": base.getid(server), "name": name}}
         return self._create("/images", data, "image")
 
     def delete(self, image):
diff --git a/novaclient/v1_0/ipgroups.py b/novaclient/v1_0/ipgroups.py
index 66821ee70..86cd3cb43 100644
--- a/novaclient/v1_0/ipgroups.py
+++ b/novaclient/v1_0/ipgroups.py
@@ -3,7 +3,7 @@
 IP Group interface.
 """
 
-from novaclient.v1_0 import base
+from novaclient import base
 
 
 class IPGroup(base.Resource):
diff --git a/novaclient/v1_0/servers.py b/novaclient/v1_0/servers.py
index a2b014f05..83cbebab1 100644
--- a/novaclient/v1_0/servers.py
+++ b/novaclient/v1_0/servers.py
@@ -20,7 +20,10 @@ Server interface.
 """
 
 import urllib
-from novaclient.v1_0 import base
+
+from novaclient import base
+from novaclient.v1_0 import base as local_base
+
 
 REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
 
@@ -154,6 +157,18 @@ class Server(base.Resource):
         """
         self.manager.resize(self, flavor)
 
+    def backup(self, image_name, backup_type, rotation):
+        """
+        Create a server backup.
+
+        :param server: The :class:`Server` (or its ID).
+        :param image_name: The name to assign the newly create image.
+        :param backup_type: 'daily' or 'weekly'
+        :param rotation: number of backups of type 'backup_type' to keep
+        :returns Newly created :class:`Image` object
+        """
+        return self.manager.backup(self, image_name, backup_type, rotation)
+
     def confirm_resize(self):
         """
         Confirm that the resize worked, thus removing the original server.
@@ -198,7 +213,7 @@ class Server(base.Resource):
         return self.addresses['private']
 
 
-class ServerManager(base.BootingManagerWithFind):
+class ServerManager(local_base.BootingManagerWithFind):
     resource_class = Server
 
     def get(self, server):
@@ -370,6 +385,31 @@ class ServerManager(base.BootingManagerWithFind):
         """
         self._action('resize', server, {'flavorId': base.getid(flavor)})
 
+    def backup(self, server, image_name, backup_type, rotation):
+        """
+        Create a server backup.
+
+        :param server: The :class:`Server` (or its ID).
+        :param image_name: The name to assign the newly create image.
+        :param backup_type: 'daily' or 'weekly'
+        :param rotation: number of backups of type 'backup_type' to keep
+        :returns Newly created :class:`Image` object
+        """
+        if not rotation:
+            raise Exception("rotation is required for backups")
+        elif not backup_type:
+            raise Exception("backup_type required for backups")
+        elif backup_type not in ("daily", "weekly"):
+            raise Exception("Invalid backup_type: must be daily or weekly")
+
+        data = {
+            "name": image_name,
+            "rotation": rotation,
+            "backup_type": backup_type,
+        }
+
+        self._action('createBackup', server, data)
+
     def pause(self, server):
         """
         Pause the server.
diff --git a/novaclient/v1_0/shell.py b/novaclient/v1_0/shell.py
index b4dcef2d2..f652e3db4 100644
--- a/novaclient/v1_0/shell.py
+++ b/novaclient/v1_0/shell.py
@@ -15,24 +15,19 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-"""
-Command-line interface to the OpenStack Nova API.
-"""
-
-import argparse
 import getpass
-import httplib2
 import os
-import prettytable
-import sys
-import textwrap
 import uuid
 
-import novaclient.v1_0
+from novaclient import exceptions
+from novaclient import utils
+from novaclient.v1_0 import client
 from novaclient.v1_0 import backup_schedules
-from novaclient.v1_0 import exceptions
 from novaclient.v1_0 import servers
 
+
+CLIENT_CLASS = client.Client
+
 # Choices for flags.
 DAY_CHOICES = [getattr(backup_schedules, i).lower()
                for i in dir(backup_schedules)
@@ -42,872 +37,678 @@ HOUR_CHOICES = [getattr(backup_schedules, i).lower()
                 if i.startswith('BACKUP_DAILY_')]
 
 
-def pretty_choice_list(l):
-    return ', '.join("'%s'" % i for i in l)
-
 # Sentinal for boot --key
 AUTO_KEY = object()
 
 
-# Decorator for args
-def arg(*args, **kwargs):
-    def _decorator(func):
-        # Because of the sematics of decorator composition if we just append
-        # to the options list positional options will appear to be backwards.
-        func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
-        return func
-    return _decorator
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('--enable', dest='enabled', default=None, action='store_true',
+                                               help='Enable backups.')
+@utils.arg('--disable', dest='enabled', action='store_false',
+                                  help='Disable backups.')
+@utils.arg('--weekly', metavar='<day>', choices=DAY_CHOICES,
+     help='Schedule a weekly backup for <day> (one of: %s).' %
+                              utils.pretty_choice_list(DAY_CHOICES))
+@utils.arg('--daily', metavar='<time-window>', choices=HOUR_CHOICES,
+     help='Schedule a daily backup during <time-window> (one of: %s).' %
+                                       utils.pretty_choice_list(HOUR_CHOICES))
+def do_backup_schedule(cs, args):
+    """
+    Show or edit the backup schedule for a server.
 
+    With no flags, the backup schedule will be shown. If flags are given,
+    the backup schedule will be modified accordingly.
+    """
+    server = _find_server(cs, args.server)
 
-class CommandError(Exception):
-    pass
-
-
-def env(e):
-    return os.environ.get(e, '')
-
-
-class OpenStackShell(object):
-
-    # Hook for the test suite to inject a fake server.
-    _api_class = novaclient.v1_0.Client
-
-    def __init__(self):
-        self.parser = argparse.ArgumentParser(
-            prog='nova',
-            description=__doc__.strip(),
-            epilog='See "nova help COMMAND" '\
-                   'for help on a specific command.',
-            add_help=False,
-            formatter_class=OpenStackHelpFormatter,
-        )
-
-        # Global arguments
-        self.parser.add_argument('-h', '--help',
-            action='help',
-            help=argparse.SUPPRESS,
-        )
-
-        self.parser.add_argument('--debug',
-            default=False,
-            action='store_true',
-            help=argparse.SUPPRESS)
-
-        self.parser.add_argument('--username',
-            default=env('NOVA_USERNAME'),
-            help='Defaults to env[NOVA_USERNAME].')
-
-        self.parser.add_argument('--apikey',
-            default=env('NOVA_API_KEY'),
-            help='Defaults to env[NOVA_API_KEY].')
-
-        self.parser.add_argument('--projectid',
-            default=env('NOVA_PROJECT_ID'),
-            help='Defaults to env[NOVA_PROJECT_ID].')
-
-        auth_url = env('NOVA_URL')
-        if auth_url == '':
-            auth_url = 'https://auth.api.rackspacecloud.com/v1.0'
-        self.parser.add_argument('--url',
-            default=auth_url,
-            help='Defaults to env[NOVA_URL].')
-
-        # Subcommands
-        subparsers = self.parser.add_subparsers(metavar='<subcommand>')
-        self.subcommands = {}
-
-        # Everything that's do_* is a subcommand.
-        for attr in (a for a in dir(self) if a.startswith('do_')):
-            # I prefer to be hypen-separated instead of underscores.
-            command = attr[3:].replace('_', '-')
-            callback = getattr(self, 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 main(self, argv):
-        # Parse args and call whatever callback was selected
-        args = self.parser.parse_args(argv)
-
-        # Short-circuit and deal with help right away.
-        if args.func == self.do_help:
-            self.do_help(args)
-            return 0
-
-        # Deal with global arguments
-        if args.debug:
-            httplib2.debuglevel = 1
-
-        user, apikey, projectid, url = args.username, args.apikey, \
-                                       args.projectid, args.url
-
-        #FIXME(usrleon): Here should be restrict for project id same as
-        # for username or apikey but for compatibility it is not.
-
-        if not user:
-            raise CommandError("You must provide a username, either via "
-                               "--username or via env[NOVA_USERNAME]")
-        if not apikey:
-            raise CommandError("You must provide an API key, either via "
-                               "--apikey or via env[NOVA_API_KEY]")
-
-        self.cs = self._api_class(user, apikey, projectid, url)
-        try:
-            self.cs.authenticate()
-        except exceptions.Unauthorized:
-            raise CommandError("Invalid OpenStack Nova credentials.")
-
-        args.func(args)
-
-    @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 CommandError("'%s' is not a valid subcommand." %
-                                                         args.command)
-        else:
-            self.parser.print_help()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('--enable', dest='enabled', default=None, action='store_true',
-                                                   help='Enable backups.')
-    @arg('--disable', dest='enabled', action='store_false',
-                                      help='Disable backups.')
-    @arg('--weekly', metavar='<day>', choices=DAY_CHOICES,
-         help='Schedule a weekly backup for <day> (one of: %s).' %
-                                  pretty_choice_list(DAY_CHOICES))
-    @arg('--daily', metavar='<time-window>', choices=HOUR_CHOICES,
-         help='Schedule a daily backup during <time-window> (one of: %s).' %
-                                           pretty_choice_list(HOUR_CHOICES))
-    def do_backup_schedule(self, args):
-        """
-        Show or edit the backup schedule for a server.
-
-        With no flags, the backup schedule will be shown. If flags are given,
-        the backup schedule will be modified accordingly.
-        """
-        server = self._find_server(args.server)
-
-        # If we have some flags, update the backup
-        backup = {}
-        if args.daily:
-            backup['daily'] = getattr(backup_schedules, 'BACKUP_DAILY_%s' %
+    # If we have some flags, update the backup
+    backup = {}
+    if args.daily:
+        backup['daily'] = getattr(backup_schedules, 'BACKUP_DAILY_%s' %
                                                     args.daily.upper())
-        if args.weekly:
-            backup['weekly'] = getattr(backup_schedules, 'BACKUP_WEEKLY_%s' %
+    if args.weekly:
+        backup['weekly'] = getattr(backup_schedules, 'BACKUP_WEEKLY_%s' %
                                                      args.weekly.upper())
-        if args.enabled is not None:
-            backup['enabled'] = args.enabled
-        if backup:
-            server.backup_schedule.update(**backup)
+    if args.enabled is not None:
+        backup['enabled'] = args.enabled
+    if backup:
+        server.backup_schedule.update(**backup)
+    else:
+        utils.print_dict(server.backup_schedule._info)
+
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_backup_schedule_delete(cs, args):
+    """
+    Delete the backup schedule for a server.
+    """
+    server = _find_server(cs, args.server)
+    server.backup_schedule.delete()
+
+def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
+    """Boot a new server."""
+    if min_count is None:
+        min_count = 1
+    if max_count is None:
+        max_count = min_count
+    if min_count > max_count:
+        raise exceptions.CommandError("min_instances should be <= max_instances")
+    if not min_count or not max_count:
+        raise exceptions.CommandError("min_instances nor max_instances should be 0")
+
+    flavor = args.flavor or cs.flavors.find(ram=256)
+    image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\
+                                                   "(lucid)")
+
+    # Map --ipgroup <name> to an ID.
+    # XXX do this for flavor/image?
+    if args.ipgroup:
+        ipgroup = _find_ipgroup(cs, args.ipgroup)
+    else:
+        ipgroup = None
+
+    metadata = dict(v.split('=') for v in args.meta)
+
+    files = {}
+    for f in args.files:
+        dst, src = f.split('=', 1)
+        try:
+            files[dst] = open(src)
+        except IOError, e:
+            raise exceptions.CommandError("Can't open '%s': %s" % (src, e))
+
+    if args.key is AUTO_KEY:
+        possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
+                         for k in ('id_dsa.pub', 'id_rsa.pub')]
+        for k in possible_keys:
+            if os.path.exists(k):
+                keyfile = k
+                break
         else:
-            print_dict(server.backup_schedule._info)
+            raise exceptions.CommandError("Couldn't find a key file: tried "
+                               "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
+    elif args.key:
+        keyfile = args.key
+    else:
+        keyfile = None
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_backup_schedule_delete(self, args):
-        """
-        Delete the backup schedule for a server.
-        """
-        server = self._find_server(args.server)
-        server.backup_schedule.delete()
+    if keyfile:
+        try:
+            files['/root/.ssh/authorized_keys2'] = open(keyfile)
+        except IOError, e:
+            raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e))
 
-    def _boot(self, args, reservation_id=None, min_count=None, max_count=None):
-        """Boot a new server."""
-        if min_count is None:
-            min_count = 1
-        if max_count is None:
-            max_count = min_count
-        if min_count > max_count:
-            raise CommandError("min_instances should be <= max_instances")
-        if not min_count or not max_count:
-            raise CommandError("min_instances nor max_instances should be 0")
+    return (args.name, image, flavor, ipgroup, metadata, files,
+            reservation_id, min_count, max_count)
 
-        flavor = args.flavor or self.cs.flavors.find(ram=256)
-        image = args.image or self.cs.images.find(name="Ubuntu 10.04 LTS "\
-                                                       "(lucid)")
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'osc flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'osc images'). "\
+          "Defaults to Ubuntu 10.04 LTS.")
+@utils.arg('--ipgroup',
+     default=None,
+     metavar='<group>',
+     help="IP group name or ID (see 'osc ipgroup-list').")
+@utils.arg('--meta',
+     metavar="<key=value>",
+     action='append',
+     default=[],
+     help="Record arbitrary key/value metadata. "\
+          "May be give multiple times.")
+@utils.arg('--file',
+     metavar="<dst-path=src-path>",
+     action='append',
+     dest='files',
+     default=[],
+     help="Store arbitrary files from <src-path> locally to <dst-path> "\
+          "on the new server. You may store up to 5 files.")
+@utils.arg('--key',
+     metavar='<path>',
+     nargs='?',
+     const=AUTO_KEY,
+     help="Key the server with an SSH keypair. "\
+          "Looks in ~/.ssh for a key, "\
+          "or takes an explicit <path> to one.")
+@utils.arg('name', metavar='<name>', help='Name for the new server')
+def do_boot(cs, args):
+    """Boot a new server."""
+    name, image, flavor, ipgroup, metadata, files, reservation_id, \
+                min_count, max_count = _boot(cs, args)
 
-        # Map --ipgroup <name> to an ID.
-        # XXX do this for flavor/image?
-        if args.ipgroup:
-            ipgroup = self._find_ipgroup(args.ipgroup)
-        else:
-            ipgroup = None
+    server = cs.servers.create(args.name, image, flavor,
+                                    ipgroup=ipgroup,
+                                    meta=metadata,
+                                    files=files,
+                                    min_count=min_count,
+                                    max_count=max_count)
+    utils.print_dict(server._info)
 
-        metadata = dict(v.split('=') for v in args.meta)
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'osc flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'osc images'). "\
+          "Defaults to Ubuntu 10.04 LTS.")
+@utils.arg('--ipgroup',
+     default=None,
+     metavar='<group>',
+     help="IP group name or ID (see 'osc ipgroup-list').")
+@utils.arg('--meta',
+     metavar="<key=value>",
+     action='append',
+     default=[],
+     help="Record arbitrary key/value metadata. "\
+          "May be give multiple times.")
+@utils.arg('--file',
+     metavar="<dst-path=src-path>",
+     action='append',
+     dest='files',
+     default=[],
+     help="Store arbitrary files from <src-path> locally to <dst-path> "\
+          "on the new server. You may store up to 5 files.")
+@utils.arg('--key',
+     metavar='<path>',
+     nargs='?',
+     const=AUTO_KEY,
+     help="Key the server with an SSH keypair. "\
+          "Looks in ~/.ssh for a key, "\
+          "or takes an explicit <path> to one.")
+@utils.arg('account', metavar='<account>', help='Account to build this'\
+     ' server for')
+@utils.arg('name', metavar='<name>', help='Name for the new server')
+def do_boot_for_account(cs, args):
+    """Boot a new server in an account."""
+    name, image, flavor, ipgroup, metadata, files, reservation_id, \
+            min_count, max_count = _boot(cs, args)
 
-        files = {}
-        for f in args.files:
-            dst, src = f.split('=', 1)
-            try:
-                files[dst] = open(src)
-            except IOError, e:
-                raise CommandError("Can't open '%s': %s" % (src, e))
+    server = cs.accounts.create_instance_for(args.account, args.name,
+                image, flavor,
+                ipgroup=ipgroup,
+                meta=metadata,
+                files=files)
+    utils.print_dict(server._info)
 
-        if args.key is AUTO_KEY:
-            possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
-                             for k in ('id_dsa.pub', 'id_rsa.pub')]
-            for k in possible_keys:
-                if os.path.exists(k):
-                    keyfile = k
-                    break
-            else:
-                raise CommandError("Couldn't find a key file: tried "
-                                   "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
-        elif args.key:
-            keyfile = args.key
-        else:
-            keyfile = None
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'osc flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'osc images'). "\
+          "Defaults to Ubuntu 10.04 LTS.")
+@utils.arg('--ipgroup',
+     default=None,
+     metavar='<group>',
+     help="IP group name or ID (see 'osc ipgroup-list').")
+@utils.arg('--meta',
+     metavar="<key=value>",
+     action='append',
+     default=[],
+     help="Record arbitrary key/value metadata. "\
+          "May be give multiple times.")
+@utils.arg('--file',
+     metavar="<dst-path=src-path>",
+     action='append',
+     dest='files',
+     default=[],
+     help="Store arbitrary files from <src-path> locally to <dst-path> "\
+          "on the new server. You may store up to 5 files.")
+@utils.arg('--key',
+     metavar='<path>',
+     nargs='?',
+     const=AUTO_KEY,
+     help="Key the server with an SSH keypair. "\
+          "Looks in ~/.ssh for a key, "\
+          "or takes an explicit <path> to one.")
+@utils.arg('--reservation_id',
+     default=None,
+     metavar='<reservation_id>',
+     help="Reservation ID (a UUID). "\
+          "If unspecified will be generated by the server.")
+@utils.arg('--min_instances',
+     default=None,
+     type=int,
+     metavar='<number>',
+     help="The minimum number of instances to build. "\
+             "Defaults to 1.")
+@utils.arg('--max_instances',
+     default=None,
+     type=int,
+     metavar='<number>',
+     help="The maximum number of instances to build. "\
+             "Defaults to 'min_instances' setting.")
+@utils.arg('name', metavar='<name>', help='Name for the new server')
+def do_zone_boot(cs, args):
+    """Boot a new server, potentially across Zones."""
+    reservation_id = args.reservation_id
+    min_count = args.min_instances
+    max_count = args.max_instances
+    name, image, flavor, ipgroup, metadata, \
+            files, reservation_id, min_count, max_count = \
+                             _boot(cs, args,
+                                        reservation_id=reservation_id,
+                                        min_count=min_count,
+                                        max_count=max_count)
 
-        if keyfile:
-            try:
-                files['/root/.ssh/authorized_keys2'] = open(keyfile)
-            except IOError, e:
-                raise CommandError("Can't open '%s': %s" % (keyfile, e))
-
-        return (args.name, image, flavor, ipgroup, metadata, files,
-                reservation_id, min_count, max_count)
-
-    @arg('--flavor',
-         default=None,
-         metavar='<flavor>',
-         help="Flavor ID (see 'novaclient flavors'). "\
-              "Defaults to 256MB RAM instance.")
-    @arg('--image',
-         default=None,
-         metavar='<image>',
-         help="Image ID (see 'novaclient images'). "\
-              "Defaults to Ubuntu 10.04 LTS.")
-    @arg('--ipgroup',
-         default=None,
-         metavar='<group>',
-         help="IP group name or ID (see 'novaclient ipgroup-list').")
-    @arg('--meta',
-         metavar="<key=value>",
-         action='append',
-         default=[],
-         help="Record arbitrary key/value metadata. "\
-              "May be give multiple times.")
-    @arg('--file',
-         metavar="<dst-path=src-path>",
-         action='append',
-         dest='files',
-         default=[],
-         help="Store arbitrary files from <src-path> locally to <dst-path> "\
-              "on the new server. You may store up to 5 files.")
-    @arg('--key',
-         metavar='<path>',
-         nargs='?',
-         const=AUTO_KEY,
-         help="Key the server with an SSH keypair. "\
-              "Looks in ~/.ssh for a key, "\
-              "or takes an explicit <path> to one.")
-    @arg('name', metavar='<name>', help='Name for the new server')
-    def do_boot(self, args):
-        """Boot a new server."""
-        name, image, flavor, ipgroup, metadata, files, reservation_id, \
-                    min_count, max_count = self._boot(args)
-
-        server = self.cs.servers.create(args.name, image, flavor,
+    reservation_id = cs.zones.boot(args.name, image, flavor,
                                         ipgroup=ipgroup,
                                         meta=metadata,
                                         files=files,
+                                        reservation_id=reservation_id,
                                         min_count=min_count,
                                         max_count=max_count)
-        print_dict(server._info)
+    print "Reservation ID=", reservation_id
 
-    @arg('--flavor',
-         default=None,
-         metavar='<flavor>',
-         help="Flavor ID (see 'novaclient flavors'). "\
-              "Defaults to 256MB RAM instance.")
-    @arg('--image',
-         default=None,
-         metavar='<image>',
-         help="Image ID (see 'novaclient images'). "\
-              "Defaults to Ubuntu 10.04 LTS.")
-    @arg('--ipgroup',
-         default=None,
-         metavar='<group>',
-         help="IP group name or ID (see 'novaclient ipgroup-list').")
-    @arg('--meta',
-         metavar="<key=value>",
-         action='append',
-         default=[],
-         help="Record arbitrary key/value metadata. "\
-              "May be give multiple times.")
-    @arg('--file',
-         metavar="<dst-path=src-path>",
-         action='append',
-         dest='files',
-         default=[],
-         help="Store arbitrary files from <src-path> locally to <dst-path> "\
-              "on the new server. You may store up to 5 files.")
-    @arg('--key',
-         metavar='<path>',
-         nargs='?',
-         const=AUTO_KEY,
-         help="Key the server with an SSH keypair. "\
-              "Looks in ~/.ssh for a key, "\
-              "or takes an explicit <path> to one.")
-    @arg('account', metavar='<account>', help='Account to build this'\
-         ' server for')
-    @arg('name', metavar='<name>', help='Name for the new server')
-    def do_boot_for_account(self, args):
-        """Boot a new server in an account."""
-        name, image, flavor, ipgroup, metadata, files, reservation_id, \
-                min_count, max_count = self._boot(args)
+def _translate_flavor_keys(collection):
+    convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
+    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])
 
-        server = self.cs.accounts.create_instance_for(args.account, args.name,
-                    image, flavor,
-                    ipgroup=ipgroup,
-                    meta=metadata,
-                    files=files)
-        print_dict(server._info)
+def do_flavor_list(cs, args):
+    """Print a list of available 'flavors' (sizes of servers)."""
+    flavors = cs.flavors.list()
+    _translate_flavor_keys(flavors)
+    utils.print_list(flavors, [
+        'ID',
+        'Name',
+        'Memory_MB',
+        'Swap',
+        'Local_GB',
+        'VCPUs',
+        'RXTX_Quota',
+        'RXTX_Cap'])
 
-    @arg('--flavor',
-         default=None,
-         metavar='<flavor>',
-         help="Flavor ID (see 'novaclient flavors'). "\
-              "Defaults to 256MB RAM instance.")
-    @arg('--image',
-         default=None,
-         metavar='<image>',
-         help="Image ID (see 'novaclient images'). "\
-              "Defaults to Ubuntu 10.04 LTS.")
-    @arg('--ipgroup',
-         default=None,
-         metavar='<group>',
-         help="IP group name or ID (see 'novaclient ipgroup-list').")
-    @arg('--meta',
-         metavar="<key=value>",
-         action='append',
-         default=[],
-         help="Record arbitrary key/value metadata. "\
-              "May be give multiple times.")
-    @arg('--file',
-         metavar="<dst-path=src-path>",
-         action='append',
-         dest='files',
-         default=[],
-         help="Store arbitrary files from <src-path> locally to <dst-path> "\
-              "on the new server. You may store up to 5 files.")
-    @arg('--key',
-         metavar='<path>',
-         nargs='?',
-         const=AUTO_KEY,
-         help="Key the server with an SSH keypair. "\
-              "Looks in ~/.ssh for a key, "\
-              "or takes an explicit <path> to one.")
-    @arg('--reservation_id',
-         default=None,
-         metavar='<reservation_id>',
-         help="Reservation ID (a UUID). "\
-              "If unspecified will be generated by the server.")
-    @arg('--min_instances',
-         default=None,
-         type=int,
-         metavar='<number>',
-         help="The minimum number of instances to build. "\
-                 "Defaults to 1.")
-    @arg('--max_instances',
-         default=None,
-         type=int,
-         metavar='<number>',
-         help="The maximum number of instances to build. "\
-                 "Defaults to 'min_instances' setting.")
-    @arg('name', metavar='<name>', help='Name for the new server')
-    def do_zone_boot(self, args):
-        """Boot a new server, potentially across Zones."""
-        reservation_id = args.reservation_id
-        min_count = args.min_instances
-        max_count = args.max_instances
-        name, image, flavor, ipgroup, metadata, \
-                files, reservation_id, min_count, max_count = \
-                                 self._boot(args,
-                                            reservation_id=reservation_id,
-                                            min_count=min_count,
-                                            max_count=max_count)
+def do_image_list(cs, args):
+    """Print a list of available images to boot from."""
+    utils.print_list(cs.images.list(), ['ID', 'Name', 'Status'])
 
-        reservation_id = self.cs.zones.boot(args.name, image, flavor,
-                                            ipgroup=ipgroup,
-                                            meta=metadata,
-                                            files=files,
-                                            reservation_id=reservation_id,
-                                            min_count=min_count,
-                                            max_count=max_count)
-        print "Reservation ID=", reservation_id
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('name', metavar='<name>', help='Name of snapshot.')
+def do_image_create(cs, args):
+    """Create a new image by taking a snapshot of a running server."""
+    server = _find_server(cs, args.server)
+    image = cs.images.create(server, args.name)
+    utils.print_dict(image._info)
 
-    def _translate_flavor_keys(self, collection):
-        convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
-        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.arg('image', metavar='<image>', help='Name or ID of image.')
+def do_image_delete(cs, args):
+    """
+    Delete an image.
 
-    def do_flavor_list(self, args):
-        """Print a list of available 'flavors' (sizes of servers)."""
-        flavors = self.cs.flavors.list()
-        self._translate_flavor_keys(flavors)
-        print_list(flavors, [
-            'ID',
-            'Name',
-            'Memory_MB',
-            'Swap',
-            'Local_GB',
-            'VCPUs',
-            'RXTX_Quota',
-            'RXTX_Cap'])
+    It should go without saying, but you can only delete images you
+    created.
+    """
+    image = _find_image(cs, args.image)
+    image.delete()
 
-    def do_image_list(self, args):
-        """Print a list of available images to boot from."""
-        print_list(self.cs.images.list(), ['ID', 'Name', 'Status'])
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('group', metavar='<group>', help='Name or ID of group.')
+@utils.arg('address', metavar='<address>', help='IP address to share.')
+def do_ip_share(cs, args):
+    """Share an IP address from the given IP group onto a server."""
+    server = _find_server(cs, args.server)
+    group = _find_ipgroup(cs, args.group)
+    server.share_ip(group, args.address)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('name', metavar='<name>', help='Name of backup or snapshot.')
-    @arg('--image-type',
-         metavar='<backup|snapshot>',
-         default='snapshot',
-         help='type of image (default: snapshot)')
-    @arg('--backup-type',
-         metavar='<daily|weekly>',
-         default=None,
-         help='type of backup')
-    @arg('--rotation',
-         default=None,
-         type=int,
-         metavar='<rotation>',
-         help="Number of backups to retain. Used for backup image_type.")
-    def do_image_create(self, args):
-        """Create a new image by taking a snapshot of a running server."""
-        server = self._find_server(args.server)
-        image = self.cs.images.create(server, args.name,
-                                      image_type=args.image_type,
-                                      backup_type=args.backup_type,
-                                      rotation=args.rotation)
-        print_dict(image._info)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('address', metavar='<address>',
+                help='Shared IP address to remove from the server.')
+def do_ip_unshare(cs, args):
+    """Stop sharing an given address with a server."""
+    server = _find_server(cs, args.server)
+    server.unshare_ip(args.address)
 
-    @arg('image', metavar='<image>', help='Name or ID of image.')
-    def do_image_delete(self, args):
-        """
-        Delete an image.
+def do_ipgroup_list(cs, args):
+    """Show IP groups."""
+    def pretty_server_list(ipgroup):
+        return ", ".join(cs.servers.get(id).name
+                         for id in ipgroup.servers)
 
-        It should go without saying, but you can only delete images you
-        created.
-        """
-        image = self._find_image(args.image)
-        image.delete()
+    utils.print_list(cs.ipgroups.list(),
+               fields=['ID', 'Name', 'Server List'],
+               formatters={'Server List': pretty_server_list})
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('group', metavar='<group>', help='Name or ID of group.')
-    @arg('address', metavar='<address>', help='IP address to share.')
-    def do_ip_share(self, args):
-        """Share an IP address from the given IP group onto a server."""
-        server = self._find_server(args.server)
-        group = self._find_ipgroup(args.group)
-        server.share_ip(group, args.address)
+@utils.arg('group', metavar='<group>', help='Name or ID of group.')
+def do_ipgroup_show(cs, args):
+    """Show details about a particular IP group."""
+    group = _find_ipgroup(cs, args.group)
+    utils.print_dict(group._info)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('address', metavar='<address>',
-                    help='Shared IP address to remove from the server.')
-    def do_ip_unshare(self, args):
-        """Stop sharing an given address with a server."""
-        server = self._find_server(args.server)
-        server.unshare_ip(args.address)
+@utils.arg('name', metavar='<name>', help='What to name this new group.')
+@utils.arg('server', metavar='<server>', nargs='?',
+     help='Server (name or ID) to make a member of this new group.')
+def do_ipgroup_create(cs, args):
+    """Create a new IP group."""
+    if args.server:
+        server = _find_server(cs, args.server)
+    else:
+        server = None
+    group = cs.ipgroups.create(args.name, server)
+    utils.print_dict(group._info)
 
-    def do_ipgroup_list(self, args):
-        """Show IP groups."""
-        def pretty_server_list(ipgroup):
-            return ", ".join(self.cs.servers.get(id).name
-                             for id in ipgroup.servers)
+@utils.arg('group', metavar='<group>', help='Name or ID of group.')
+def do_ipgroup_delete(cs, args):
+    """Delete an IP group."""
+    _find_ipgroup(cs, args.group).delete()
 
-        print_list(self.cs.ipgroups.list(),
-                   fields=['ID', 'Name', 'Server List'],
-                   formatters={'Server List': pretty_server_list})
+@utils.arg('--fixed_ip',
+    dest='fixed_ip',
+    metavar='<fixed_ip>',
+    default=None,
+    help='Only match against fixed IP.')
+@utils.arg('--reservation_id',
+    dest='reservation_id',
+    metavar='<reservation_id>',
+    default=None,
+    help='Only return instances that match reservation_id.')
+@utils.arg('--recurse_zones',
+    dest='recurse_zones',
+    metavar='<0|1>',
+    nargs='?',
+    type=int,
+    const=1,
+    default=0,
+    help='Recurse through all zones if set.')
+@utils.arg('--ip',
+    dest='ip',
+    metavar='<ip_regexp>',
+    default=None,
+    help='Search with regular expression match by IP address')
+@utils.arg('--ip6',
+    dest='ip6',
+    metavar='<ip6_regexp>',
+    default=None,
+    help='Search with regular expression match by IPv6 address')
+@utils.arg('--server_name',
+    dest='server_name',
+    metavar='<name_regexp>',
+    default=None,
+    help='Search with regular expression match by server name')
+@utils.arg('--name',
+    dest='display_name',
+    metavar='<name_regexp>',
+    default=None,
+    help='Search with regular expression match by display name')
+@utils.arg('--instance_name',
+    dest='name',
+    metavar='<name_regexp>',
+    default=None,
+    help='Search with regular expression match by instance name')
+def do_list(cs, args):
+    """List active servers."""
+    recurse_zones = args.recurse_zones
+    search_opts = {
+            'reservation_id': args.reservation_id,
+            'fixed_ip': args.fixed_ip,
+            'recurse_zones': recurse_zones,
+            'ip': args.ip,
+            'ip6': args.ip6,
+            'name': args.name,
+            'server_name': args.server_name,
+            'display_name': args.display_name}
+    if recurse_zones:
+        to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
+    else:
+        to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
+    utils.print_list(cs.servers.list(search_opts=search_opts),
+            to_print)
 
-    @arg('group', metavar='<group>', help='Name or ID of group.')
-    def do_ipgroup_show(self, args):
-        """Show details about a particular IP group."""
-        group = self._find_ipgroup(args.group)
-        print_dict(group._info)
+@utils.arg('--hard',
+    dest='reboot_type',
+    action='store_const',
+    const=servers.REBOOT_HARD,
+    default=servers.REBOOT_SOFT,
+    help='Perform a hard reboot (instead of a soft one).')
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_reboot(cs, args):
+    """Reboot a server."""
+    _find_server(cs, args.server).reboot(args.reboot_type)
 
-    @arg('name', metavar='<name>', help='What to name this new group.')
-    @arg('server', metavar='<server>', nargs='?',
-         help='Server (name or ID) to make a member of this new group.')
-    def do_ipgroup_create(self, args):
-        """Create a new IP group."""
-        if args.server:
-            server = self._find_server(args.server)
-        else:
-            server = None
-        group = self.cs.ipgroups.create(args.name, server)
-        print_dict(group._info)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('image', metavar='<image>', help="Name or ID of new image.")
+def do_rebuild(cs, args):
+    """Shutdown, re-image, and re-boot a server."""
+    server = _find_server(cs, args.server)
+    image = _find_image(cs, args.image)
+    server.rebuild(image)
 
-    @arg('group', metavar='<group>', help='Name or ID of group.')
-    def do_ipgroup_delete(self, args):
-        """Delete an IP group."""
-        self._find_ipgroup(args.group).delete()
+@utils.arg('server', metavar='<server>', help='Name (old name) or ID of server.')
+@utils.arg('name', metavar='<name>', help='New name for the server.')
+def do_rename(cs, args):
+    """Rename a server."""
+    _find_server(cs, args.server).update(name=args.name)
 
-    @arg('--fixed_ip',
-        dest='fixed_ip',
-        metavar='<fixed_ip>',
-        default=None,
-        help='Only match against fixed IP.')
-    @arg('--reservation_id',
-        dest='reservation_id',
-        metavar='<reservation_id>',
-        default=None,
-        help='Only return instances that match reservation_id.')
-    @arg('--recurse_zones',
-        dest='recurse_zones',
-        metavar='<0|1>',
-        nargs='?',
-        type=int,
-        const=1,
-        default=0,
-        help='Recurse through all zones if set.')
-    @arg('--ip',
-        dest='ip',
-        metavar='<ip_regexp>',
-        default=None,
-        help='Search with regular expression match by IP address')
-    @arg('--ip6',
-        dest='ip6',
-        metavar='<ip6_regexp>',
-        default=None,
-        help='Search with regular expression match by IPv6 address')
-    @arg('--server_name',
-        dest='server_name',
-        metavar='<name_regexp>',
-        default=None,
-        help='Search with regular expression match by server name')
-    @arg('--name',
-        dest='display_name',
-        metavar='<name_regexp>',
-        default=None,
-        help='Search with regular expression match by display name')
-    @arg('--instance_name',
-        dest='name',
-        metavar='<name_regexp>',
-        default=None,
-        help='Search with regular expression match by instance name')
-    def do_list(self, args):
-        """List active servers."""
-        recurse_zones = args.recurse_zones
-        search_opts = {
-                'reservation_id': args.reservation_id,
-                'fixed_ip': args.fixed_ip,
-                'recurse_zones': recurse_zones,
-                'ip': args.ip,
-                'ip6': args.ip6,
-                'name': args.name,
-                'server_name': args.server_name,
-                'display_name': args.display_name}
-        if recurse_zones:
-            to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
-        else:
-            to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
-        print_list(self.cs.servers.list(search_opts=search_opts),
-                to_print)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
+def do_resize(cs, args):
+    """Resize a server."""
+    server = _find_server(cs, args.server)
+    flavor = _find_flavor(cs, args.flavor)
+    server.resize(flavor)
 
-    @arg('--hard',
-        dest='reboot_type',
-        action='store_const',
-        const=servers.REBOOT_HARD,
-        default=servers.REBOOT_SOFT,
-        help='Perform a hard reboot (instead of a soft one).')
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_reboot(self, args):
-        """Reboot a server."""
-        self._find_server(args.server).reboot(args.reboot_type)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('name', metavar='<name>', help='Name of snapshot.')
+@utils.arg('backup_type', metavar='<daily|weekly>', help='type of backup')
+@utils.arg('rotation', type=int, metavar='<rotation>',
+     help="Number of backups to retain. Used for backup image_type.")
+def do_backup(cs, args):
+    """Resize a server."""
+    server = _find_server(cs, args.server)
+    server.backup(args.name, args.backup_type, args.rotation)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('image', metavar='<image>', help="Name or ID of new image.")
-    def do_rebuild(self, args):
-        """Shutdown, re-image, and re-boot a server."""
-        server = self._find_server(args.server)
-        image = self._find_image(args.image)
-        server.rebuild(image)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_migrate(cs, args):
+    """Migrate a server."""
+    _find_server(cs, args.server).migrate()
 
-    @arg('server', metavar='<server>', help='Name (old name) or ID of server.')
-    @arg('name', metavar='<name>', help='New name for the server.')
-    def do_rename(self, args):
-        """Rename a server."""
-        self._find_server(args.server).update(name=args.name)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_pause(cs, args):
+    """Pause a server."""
+    _find_server(cs, args.server).pause()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
-    def do_resize(self, args):
-        """Resize a server."""
-        server = self._find_server(args.server)
-        flavor = self._find_flavor(args.flavor)
-        server.resize(flavor)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_unpause(cs, args):
+    """Unpause a server."""
+    _find_server(cs, args.server).unpause()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_migrate(self, args):
-        """Migrate a server."""
-        self._find_server(args.server).migrate()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_suspend(cs, args):
+    """Suspend a server."""
+    _find_server(cs, args.server).suspend()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_pause(self, args):
-        """Pause a server."""
-        self._find_server(args.server).pause()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_resume(cs, args):
+    """Resume a server."""
+    _find_server(cs, args.server).resume()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_unpause(self, args):
-        """Unpause a server."""
-        self._find_server(args.server).unpause()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_rescue(cs, args):
+    """Rescue a server."""
+    _find_server(cs, args.server).rescue()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_suspend(self, args):
-        """Suspend a server."""
-        self._find_server(args.server).suspend()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_unrescue(cs, args):
+    """Unrescue a server."""
+    _find_server(cs, args.server).unrescue()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_resume(self, args):
-        """Resume a server."""
-        self._find_server(args.server).resume()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_diagnostics(cs, args):
+    """Retrieve server diagnostics."""
+    utils.print_dict(cs.servers.diagnostics(args.server)[1])
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_rescue(self, args):
-        """Rescue a server."""
-        self._find_server(args.server).rescue()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_actions(cs, args):
+    """Retrieve server actions."""
+    utils.print_list(
+        cs.servers.actions(args.server),
+        ["Created_At", "Action", "Error"])
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_unrescue(self, args):
-        """Unrescue a server."""
-        self._find_server(args.server).unrescue()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_resize_confirm(cs, args):
+    """Confirm a previous resize."""
+    _find_server(cs, args.server).confirm_resize()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_diagnostics(self, args):
-        """Retrieve server diagnostics."""
-        print_dict(self.cs.servers.diagnostics(args.server)[1])
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_resize_revert(cs, args):
+    """Revert a previous resize (and return to the previous VM)."""
+    _find_server(cs, args.server).revert_resize()
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_actions(self, args):
-        """Retrieve server actions."""
-        print_list(
-            self.cs.servers.actions(args.server),
-            ["Created_At", "Action", "Error"])
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_root_password(cs, args):
+    """
+    Change the root password for a server.
+    """
+    server = _find_server(cs, args.server)
+    p1 = getpass.getpass('New password: ')
+    p2 = getpass.getpass('Again: ')
+    if p1 != p2:
+        raise exceptions.CommandError("Passwords do not match.")
+    server.update(password=p1)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_resize_confirm(self, args):
-        """Confirm a previous resize."""
-        self._find_server(args.server).confirm_resize()
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_show(cs, args):
+    """Show details about the given server."""
+    s = _find_server(cs, args.server)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_resize_revert(self, args):
-        """Revert a previous resize (and return to the previous VM)."""
-        self._find_server(args.server).revert_resize()
+    info = s._info.copy()
+    addresses = info.pop('addresses')
+    for addrtype in addresses:
+        info['%s ip' % addrtype] = ', '.join(addresses[addrtype])
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_root_password(self, args):
-        """
-        Change the root password for a server.
-        """
-        server = self._find_server(args.server)
-        p1 = getpass.getpass('New password: ')
-        p2 = getpass.getpass('Again: ')
-        if p1 != p2:
-            raise CommandError("Passwords do not match.")
-        server.update(password=p1)
+    flavorId = info.get('flavorId', None)
+    if flavorId:
+        info['flavor'] = _find_flavor(cs, info.pop('flavorId')).name
+    imageId = info.get('imageId', None)
+    if imageId:
+        info['image'] = _find_image(cs, info.pop('imageId')).name
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_show(self, args):
-        """Show details about the given server."""
-        s = self._find_server(args.server)
+    utils.print_dict(info)
 
-        info = s._info.copy()
-        addresses = info.pop('addresses')
-        for addrtype in addresses:
-            info['%s ip' % addrtype] = ', '.join(addresses[addrtype])
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_delete(cs, args):
+    """Immediately shut down and delete a server."""
+    _find_server(cs, args.server).delete()
 
-        flavorId = info.get('flavorId', None)
-        if flavorId:
-            info['flavor'] = self._find_flavor(info.pop('flavorId')).name
-        imageId = info.get('imageId', None)
-        if imageId:
-            info['image'] = self._find_image(info.pop('imageId')).name
+# --zone_username is required since --username is already used.
+@utils.arg('zone', metavar='<zone_id>', help='ID of the zone', default=None)
+@utils.arg('--api_url', dest='api_url', default=None, help='New URL.')
+@utils.arg('--zone_username', dest='zone_username', default=None,
+                        help='New zone username.')
+@utils.arg('--password', dest='password', default=None, help='New password.')
+@utils.arg('--weight_offset', dest='weight_offset', default=None,
+                        help='Child Zone weight offset.')
+@utils.arg('--weight_scale', dest='weight_scale', default=None,
+                        help='Child Zone weight scale.')
+def do_zone(cs, args):
+    """Show or edit a child zone. No zone arg for this zone."""
+    zone = cs.zones.get(args.zone)
 
-        print_dict(info)
+    # If we have some flags, update the zone
+    zone_delta = {}
+    if args.api_url:
+        zone_delta['api_url'] = args.api_url
+    if args.zone_username:
+        zone_delta['username'] = args.zone_username
+    if args.password:
+        zone_delta['password'] = args.password
+    if args.weight_offset:
+        zone_delta['weight_offset'] = args.weight_offset
+    if args.weight_scale:
+        zone_delta['weight_scale'] = args.weight_scale
+    if zone_delta:
+        zone.update(**zone_delta)
+    else:
+        utils.print_dict(zone._info)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_delete(self, args):
-        """Immediately shut down and delete a server."""
-        self._find_server(args.server).delete()
+def do_zone_info(cs, args):
+    """Get this zones name and capabilities."""
+    zone = cs.zones.info()
+    utils.print_dict(zone._info)
 
-    # --zone_username is required since --username is already used.
-    @arg('zone', metavar='<zone_id>', help='ID of the zone', default=None)
-    @arg('--api_url', dest='api_url', default=None, help='New URL.')
-    @arg('--zone_username', dest='zone_username', default=None,
-                            help='New zone username.')
-    @arg('--password', dest='password', default=None, help='New password.')
-    @arg('--weight_offset', dest='weight_offset', default=None,
-                            help='Child Zone weight offset.')
-    @arg('--weight_scale', dest='weight_scale', default=None,
-                            help='Child Zone weight scale.')
-    def do_zone(self, args):
-        """Show or edit a child zone. No zone arg for this zone."""
-        zone = self.cs.zones.get(args.zone)
+@utils.arg('api_url', metavar='<api_url>', help="URL for the Zone's API")
+@utils.arg('zone_username', metavar='<zone_username>',
+                      help='Authentication username.')
+@utils.arg('password', metavar='<password>', help='Authentication password.')
+@utils.arg('weight_offset', metavar='<weight_offset>',
+                        help='Child Zone weight offset (typically 0.0).')
+@utils.arg('weight_scale', metavar='<weight_scale>',
+                        help='Child Zone weight scale (typically 1.0).')
+def do_zone_add(cs, args):
+    """Add a new child zone."""
+    zone = cs.zones.create(args.api_url, args.zone_username,
+                                args.password, args.weight_offset,
+                                args.weight_scale)
+    utils.print_dict(zone._info)
 
-        # If we have some flags, update the zone
-        zone_delta = {}
-        if args.api_url:
-            zone_delta['api_url'] = args.api_url
-        if args.zone_username:
-            zone_delta['username'] = args.zone_username
-        if args.password:
-            zone_delta['password'] = args.password
-        if args.weight_offset:
-            zone_delta['weight_offset'] = args.weight_offset
-        if args.weight_scale:
-            zone_delta['weight_scale'] = args.weight_scale
-        if zone_delta:
-            zone.update(**zone_delta)
-        else:
-            print_dict(zone._info)
+@utils.arg('zone', metavar='<zone>', help='Name or ID of the zone')
+def do_zone_delete(cs, args):
+    """Delete a zone."""
+    cs.zones.delete(args.zone)
 
-    def do_zone_info(self, args):
-        """Get this zones name and capabilities."""
-        zone = self.cs.zones.info()
-        print_dict(zone._info)
+def do_zone_list(cs, args):
+    """List the children of a zone."""
+    utils.print_list(cs.zones.list(), ['ID', 'Name', 'Is Active', \
+                        'API URL', 'Weight Offset', 'Weight Scale'])
 
-    @arg('api_url', metavar='<api_url>', help="URL for the Zone's API")
-    @arg('zone_username', metavar='<zone_username>',
-                          help='Authentication username.')
-    @arg('password', metavar='<password>', help='Authentication password.')
-    @arg('weight_offset', metavar='<weight_offset>',
-                            help='Child Zone weight offset (typically 0.0).')
-    @arg('weight_scale', metavar='<weight_scale>',
-                            help='Child Zone weight scale (typically 1.0).')
-    def do_zone_add(self, args):
-        """Add a new child zone."""
-        zone = self.cs.zones.create(args.api_url, args.zone_username,
-                                    args.password, args.weight_offset,
-                                    args.weight_scale)
-        print_dict(zone._info)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('network_id', metavar='<network_id>', help='Network ID.')
+def do_add_fixed_ip(cs, args):
+    """Add new IP address to network."""
+    server = _find_server(cs, args.server)
+    server.add_fixed_ip(args.network_id)
 
-    @arg('zone', metavar='<zone>', help='Name or ID of the zone')
-    def do_zone_delete(self, args):
-        """Delete a zone."""
-        self.cs.zones.delete(args.zone)
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('address', metavar='<address>', help='IP Address.')
+def do_remove_fixed_ip(cs, args):
+    """Remove an IP address from a server."""
+    server = _find_server(cs, args.server)
+    server.remove_fixed_ip(args.address)
 
-    def do_zone_list(self, args):
-        """List the children of a zone."""
-        print_list(self.cs.zones.list(), ['ID', 'Name', 'Is Active', \
-                            'API URL', 'Weight Offset', 'Weight Scale'])
+def _find_server(cs, server):
+    """Get a server by name or ID."""
+    return _find_resource(cs.servers, server)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('network_id', metavar='<network_id>', help='Network ID.')
-    def do_add_fixed_ip(self, args):
-        """Add new IP address to network."""
-        server = self._find_server(args.server)
-        server.add_fixed_ip(args.network_id)
+def _find_ipgroup(cs, group):
+    """Get an IP group by name or ID."""
+    return _find_resource(cs.ipgroups, group)
 
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('address', metavar='<address>', help='IP Address.')
-    def do_remove_fixed_ip(self, args):
-        """Remove an IP address from a server."""
-        server = self._find_server(args.server)
-        server.remove_fixed_ip(args.address)
+def _find_image(cs, image):
+    """Get an image by name or ID."""
+    return _find_resource(cs.images, image)
 
-    def _find_server(self, server):
-        """Get a server by name or ID."""
-        return self._find_resource(self.cs.servers, server)
-
-    def _find_ipgroup(self, group):
-        """Get an IP group by name or ID."""
-        return self._find_resource(self.cs.ipgroups, group)
-
-    def _find_image(self, image):
-        """Get an image by name or ID."""
-        return self._find_resource(self.cs.images, image)
-
-    def _find_flavor(self, flavor):
-        """Get a flavor by name, ID, or RAM size."""
-        try:
-            return self._find_resource(self.cs.flavors, flavor)
-        except exceptions.NotFound:
-            return self.cs.flavors.find(ram=flavor)
-
-    def _find_resource(self, manager, name_or_id):
-        """Helper for the _find_* methods."""
-        try:
-            if isinstance(name_or_id, int) or name_or_id.isdigit():
-                return manager.get(int(name_or_id))
-
-            try:
-                uuid.UUID(name_or_id)
-                return manager.get(name_or_id)
-            except ValueError:
-                return manager.find(name=name_or_id)
-        except exceptions.NotFound:
-            raise CommandError("No %s with a name or ID of '%s' exists." %
-                         (manager.resource_class.__name__.lower(), name_or_id))
-
-
-# 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)
-
-
-# Helpers
-def print_list(objs, fields, formatters={}):
-    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:
-                field_name = field.lower().replace(' ', '_')
-                data = getattr(o, field_name, '')
-                row.append(data)
-        pt.add_row(row)
-
-    pt.printt(sortby=fields[0])
-
-
-def print_dict(d):
-    pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
-    pt.aligns = ['l', 'l']
-    [pt.add_row(list(r)) for r in d.iteritems()]
-    pt.printt(sortby='Property')
-
-
-def main():
+def _find_flavor(cs, flavor):
+    """Get a flavor by name, ID, or RAM size."""
     try:
-        OpenStackShell().main(sys.argv[1:])
+        return _find_resource(cs.flavors, flavor)
+    except exceptions.NotFound:
+        return cs.flavors.find(ram=flavor)
+
+def _find_resource(manager, name_or_id):
+    """Helper for the _find_* methods."""
+    try:
+        if isinstance(name_or_id, int) or name_or_id.isdigit():
+            return manager.get(int(name_or_id))
+
+        try:
+            uuid.UUID(name_or_id)
+            return manager.get(name_or_id)
+        except ValueError:
+            return manager.find(name=name_or_id)
+    except exceptions.NotFound:
+        raise exceptions.CommandError("No %s with a name or ID of '%s' exists." %
+                     (manager.resource_class.__name__.lower(), name_or_id))
 
-    except Exception, e:
-        if httplib2.debuglevel == 1:
-            raise  # dump stack.
-        else:
-            print >> sys.stderr, e
-        sys.exit(1)
diff --git a/novaclient/v1_0/zones.py b/novaclient/v1_0/zones.py
index f711ec219..01d128f69 100644
--- a/novaclient/v1_0/zones.py
+++ b/novaclient/v1_0/zones.py
@@ -17,7 +17,8 @@
 Zone interface.
 """
 
-from novaclient.v1_0 import base
+from novaclient import base
+from novaclient.v1_0 import base as local_base
 
 
 class Weighting(base.Resource):
@@ -64,7 +65,7 @@ class Zone(base.Resource):
                             weight_offset, weight_scale)
 
 
-class ZoneManager(base.BootingManagerWithFind):
+class ZoneManager(local_base.BootingManagerWithFind):
     resource_class = Zone
 
     def info(self):
diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py
index 955b27e53..e69de29bb 100644
--- a/novaclient/v1_1/__init__.py
+++ b/novaclient/v1_1/__init__.py
@@ -1,66 +0,0 @@
-# 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.
-
-from novaclient.v1_1 import client
-from novaclient.v1_1 import exceptions
-from novaclient.v1_1 import flavors
-from novaclient.v1_1 import images
-from novaclient.v1_1 import servers
-
-
-class Client(object):
-    """
-    Top-level object to access the OpenStack Compute v1.0 API.
-
-    Create an instance with your creds::
-
-        >>> os = novaclient.v1_1.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
-
-    Then call methods on its managers::
-
-        >>> os.servers.list()
-        ...
-        >>> os.flavors.list()
-        ...
-
-    &c.
-    """
-
-    def __init__(self, username, apikey, projectid, auth_url=None, timeout=None):
-        """Initialize v1.0 Openstack Client."""
-        self.flavors = flavors.FlavorManager(self)
-        self.images = images.ImageManager(self)
-        self.servers = servers.ServerManager(self)
-
-        auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0"
-
-        self.client = client.HTTPClient(username,
-                                        apikey,
-                                        projectid,
-                                        auth_url,
-                                        timeout=timeout)
-
-    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:`novaclient.Unauthorized` if the
-        credentials are wrong.
-        """
-        self.client.authenticate()
diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py
index f283d8596..9113d53bb 100644
--- a/novaclient/v1_1/base.py
+++ b/novaclient/v1_1/base.py
@@ -19,7 +19,7 @@
 Base utilities to build API operation managers and objects on top of.
 """
 
-from novaclient.v1_1 import exceptions
+from novaclient import exceptions
 
 # Python 2.4 compat
 try:
diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py
index a805826d8..ee7195896 100644
--- a/novaclient/v1_1/client.py
+++ b/novaclient/v1_1/client.py
@@ -1,154 +1,48 @@
-# Copyright 2010 Jacob Kaplan-Moss
-"""
-OpenStack Client interface. Handles the REST calls and responses.
-"""
 
-import time
-import urlparse
-import urllib
-import httplib2
-import logging
 
-try:
-    import json
-except ImportError:
-    import simplejson as json
+from novaclient import client
+from novaclient.v1_1 import flavors
+from novaclient.v1_1 import images
+from novaclient.v1_1 import servers
 
-# Python 2.5 compat fix
-if not hasattr(urlparse, 'parse_qsl'):
-    import cgi
-    urlparse.parse_qsl = cgi.parse_qsl
 
-import novaclient
-from novaclient.v1_1 import exceptions
 
-_logger = logging.getLogger(__name__)
+class Client(object):
+    """
+    Top-level object to access the OpenStack Compute API.
 
-class HTTPClient(httplib2.Http):
+    Create an instance with your creds::
 
-    USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
+        >>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
 
-    def __init__(self, user, apikey, projectid, auth_url, timeout=None):
-        super(HTTPClient, self).__init__(timeout=timeout)
-        self.user = user
-        self.apikey = apikey
-        self.projectid = projectid
-        self.auth_url = auth_url
-        self.version = 'v1.0'
+    Then call methods on its managers::
 
-        self.management_url = None
-        self.auth_token = None
+        >>> client.servers.list()
+        ...
+        >>> client.flavors.list()
+        ...
 
-        # httplib2 overrides
-        self.force_exception_to_status_code = True
+    """
 
-    def http_log(self, args, kwargs, resp, body):
-        if not _logger.isEnabledFor(logging.DEBUG):
-            return
+    def __init__(self, username, api_key, project_id, auth_url, timeout=None):
+        self.flavors = flavors.FlavorManager(self)
+        self.images = images.ImageManager(self)
+        self.servers = servers.ServerManager(self)
 
-        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']:
-            string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element]))
-
-        _logger.debug("REQ: %s\n" % "".join(string_parts))
-        _logger.debug("RESP:%s %s\n", resp,body)
-
-    def request(self, *args, **kwargs):
-        kwargs.setdefault('headers', {})
-        kwargs['headers']['User-Agent'] = self.USER_AGENT
-        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, e:
-                pass
-        else:
-            body = None
-
-        if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
-            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):
-        url = self._munge_get_url(url)
-        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)
+        self.client = client.HTTPClient(username,
+                                        api_key,
+                                        project_id,
+                                        auth_url,
+                                        timeout=timeout)
 
     def authenticate(self):
-        scheme, netloc, path, query, frag = urlparse.urlsplit(
-                                                    self.auth_url)
-        path_parts = path.split('/')
-        for part in path_parts:
-            if len(part) > 0 and part[0] == 'v':
-                self.version = part
-                break
-
-        headers = {'X-Auth-User': self.user,
-                   'X-Auth-Key': self.apikey}
-        if self.projectid:
-            headers['X-Auth-Project-Id'] = self.projectid
-        resp, body = self.request(self.auth_url, 'GET', headers=headers)
-        self.management_url = resp['x-server-management-url']
-
-        self.auth_token = resp['x-auth-token']
-
-    def _munge_get_url(self, url):
         """
-        Munge GET URLs to always return uncached content.
+        Authenticate against the server.
 
-        The OpenStack Nova API caches data *very* agressively and doesn't
-        respect cache headers. To avoid stale data, then, we append a little
-        bit of nonsense onto GET parameters; this appears to force the data not
-        to be cached.
+        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.
         """
-        scheme, netloc, path, query, frag = urlparse.urlsplit(url)
-        query = urlparse.parse_qsl(query)
-        query.append(('fresh', str(time.time())))
-        query = urllib.urlencode(query)
-        return urlparse.urlunsplit((scheme, netloc, path, query, frag))
+        self.client.authenticate()
diff --git a/novaclient/v1_1/exceptions.py b/novaclient/v1_1/exceptions.py
deleted file mode 100644
index 1709d806f..000000000
--- a/novaclient/v1_1/exceptions.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Copyright 2010 Jacob Kaplan-Moss
-"""
-Exception definitions.
-"""
-
-class OpenStackException(Exception):
-    """
-    The base exception class for all exceptions this library raises.
-    """
-    def __init__(self, code, message=None, details=None):
-        self.code = code
-        self.message = message or self.__class__.message
-        self.details = details
-
-    def __str__(self):
-        return "%s (HTTP %s)" % (self.message, self.code)
-
-
-class BadRequest(OpenStackException):
-    """
-    HTTP 400 - Bad request: you sent some malformed data.
-    """
-    http_status = 400
-    message = "Bad request"
-
-
-class Unauthorized(OpenStackException):
-    """
-    HTTP 401 - Unauthorized: bad credentials.
-    """
-    http_status = 401
-    message = "Unauthorized"
-
-
-class Forbidden(OpenStackException):
-    """
-    HTTP 403 - Forbidden: your credentials don't give you access to this
-    resource.
-    """
-    http_status = 403
-    message = "Forbidden"
-
-
-class NotFound(OpenStackException):
-    """
-    HTTP 404 - Not found
-    """
-    http_status = 404
-    message = "Not found"
-
-
-class OverLimit(OpenStackException):
-    """
-    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(OpenStackException):
-    """
-    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 OpenStackException.__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 OpenStackException 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, OpenStackException)
-    if body:
-        message = "n/a"
-        details = "n/a"
-        if hasattr(body, 'keys'):
-            error = body[body.keys()[0]]
-            message = error.get('message', None)
-            details = error.get('details', None)
-        return cls(code=response.status, message=message, details=details)
-    else:
-        return cls(code=response.status)
diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py
index f4a82a962..6eb1e2c4b 100644
--- a/novaclient/v1_1/flavors.py
+++ b/novaclient/v1_1/flavors.py
@@ -3,7 +3,7 @@
 Flavor interface.
 """
 
-from novaclient.v1_1 import base
+from novaclient import base
 
 
 class Flavor(base.Resource):
@@ -26,10 +26,10 @@ class FlavorManager(base.ManagerWithFind):
 
         :rtype: list of :class:`Flavor`.
         """
-        detail = ""
-        if detailed:
-            detail = "/detail"
-        return self._list("/flavors%s" % detail, "flavors")
+        if detailed is True:
+            return self._list("/flavors/detail", "flavors")
+        else:
+            return self._list("/flavors", "flavors")
 
     def get(self, flavor):
         """
diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py
index 48d86ac8d..e25c237e7 100644
--- a/novaclient/v1_1/images.py
+++ b/novaclient/v1_1/images.py
@@ -3,7 +3,7 @@
 Image interface.
 """
 
-from novaclient.v1_1 import base
+from novaclient import base
 
 
 class Image(base.Resource):
@@ -41,38 +41,10 @@ class ImageManager(base.ManagerWithFind):
 
         :rtype: list of :class:`Image`
         """
-        detail = ""
-        if detailed:
-            detail = "/detail"
-        return self._list("/images%s" % detail, "images")
-
-
-    def create(self, server, name, image_type=None, backup_type=None, rotation=None):
-        """
-        Create a new image by snapshotting a running :class:`Server`
-
-        :param name: An (arbitrary) name for the new image.
-        :param server: The :class:`Server` (or its ID) to make a snapshot of.
-        :rtype: :class:`Image`
-        """
-        if image_type is None:
-            image_type = "snapshot"
-
-        if image_type not in ("backup", "snapshot"):
-            raise Exception("Invalid image_type: must be backup or snapshot")
-
-        if image_type == "backup":
-            if not rotation:
-                raise Exception("rotation is required for backups")
-            elif not backup_type:
-                raise Exception("backup_type required for backups")
-            elif backup_type not in ("daily", "weekly"):
-                raise Exception("Invalid backup_type: must be daily or weekly")
-
-        data = {"image": {"serverId": base.getid(server), "name": name,
-                          "image_type": image_type, "backup_type": backup_type,
-                          "rotation": rotation}}
-        return self._create("/images", data, "image")
+        if detailed is True:
+            return self._list("/images/detail", "images")
+        else:
+            return self._list("/images", "images")
 
     def delete(self, image):
         """
diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py
index fbba4ae18..e51ece70d 100644
--- a/novaclient/v1_1/servers.py
+++ b/novaclient/v1_1/servers.py
@@ -20,7 +20,10 @@ Server interface.
 """
 
 import urllib
-from novaclient.v1_1 import base
+
+from novaclient import base
+from novaclient.v1_0 import base as local_base
+
 
 REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
 
@@ -42,22 +45,11 @@ class Server(base.Resource):
         :param name: Update the server's name.
         :param password: Update the root password.
         """
-        self.manager.update(self, name)
-
-    def create_image(self, name, metadata=None):
-        """
-        Create an image based on this server.
-
-        :param name: The name of the image to create
-        :param metadata: The metadata to associated with the image.
-        """
-        self.manager.create_image(self, name, metadata)
+        self.manager.update(self, name=name)
 
     def change_password(self, password):
         """
-        Update the root password on this server.
-
-        :param password: The password to set.
+        Update the password for a server.
         """
         self.manager.change_password(self, password)
 
@@ -91,6 +83,15 @@ class Server(base.Resource):
         """
         self.manager.resize(self, flavor)
 
+    def create_image(self, image_name, metadata):
+        """
+        Create an image based on this server.
+
+        :param image_name: The name to assign the newly create image.
+        :param metadata: Metadata to assign to the image.
+        """
+        self.manager.create_image(self, image_name, metadata)
+
     def confirm_resize(self):
         """
         Confirm that the resize worked, thus removing the original server.
@@ -121,24 +122,8 @@ class Server(base.Resource):
             return ""
         return self.addresses['private']
 
-    @property
-    def image_id(self):
-        """
-        Shortcut to get the image identifier.
-        """
-        return self.image["id"]
 
-    @property
-    def flavor_id(self):
-        """
-        Shortcut to get the flavor identifier.
-        """
-        return self.flavor["id"]
-
-
-
-
-class ServerManager(base.BootingManagerWithFind):
+class ServerManager(local_base.BootingManagerWithFind):
     resource_class = Server
 
     def get(self, server):
@@ -161,8 +146,9 @@ class ServerManager(base.BootingManagerWithFind):
         """
         if search_opts is None:
             search_opts = {}
+
         qparams = {}
-        # only use values in query string if they are set
+
         for opt, val in search_opts.iteritems():
             if val:
                 qparams[opt] = val
@@ -190,8 +176,30 @@ class ServerManager(base.BootingManagerWithFind):
                       file-like object). A maximum of five entries is allowed,
                       and each file must be 10k or less.
         """
-        return self._boot("/servers", "server", name, image, flavor,
-                          meta=meta, files=files)
+        personality = []
+
+        for file_path, filelike in files.items():
+            try:
+                data = filelike.read()
+            except AttributeError:
+                data = str(filelike)
+
+            personality.append({
+                "path": file_path,
+                "contents": data.encode("base64"),
+            })
+
+        body = {
+            "server": {
+                "name": name,
+                "imageRef": base.getid(image),
+                "flavorRef": base.getid(flavor),
+                "metadata": meta or {},
+                "personality": personality,
+            },
+        }
+
+        return self._create("/servers", body, "server", return_raw=False)
 
     def update(self, server, name=None):
         """
@@ -211,6 +219,12 @@ class ServerManager(base.BootingManagerWithFind):
 
         self._update("/servers/%s" % base.getid(server), body)
 
+    def change_password(self, server, password):
+        """
+        Update the password for a server.
+        """
+        self._action("changePassword", server, {"adminPass": password})
+
     def delete(self, server):
         """
         Delete (i.e. shut down and delete the image) this server.
@@ -227,32 +241,6 @@ class ServerManager(base.BootingManagerWithFind):
         """
         self._action('reboot', server, {'type': type})
 
-    def create_image(self, server, name, metadata=None):
-        """
-        Create an image based on this server.
-
-        :param server: The :class:`Server` (or its ID) to create image from.
-        :param name: The name of the image to create
-        :param metadata: The metadata to associated with the image.
-        """
-        body = {
-            "name": name,
-            "metadata": metadata or {},
-        }
-        self._action('createImage', server, body)
-
-    def change_password(self, server, password):
-        """
-        Update the root password on a server.
-
-        :param server: The :class:`Server` (or its ID) to share onto.
-        :param password: The password to set.
-        """
-        body = {
-            "adminPass": password,
-        }
-        self._action('changePassword', server, body)
-
     def rebuild(self, server, image):
         """
         Rebuild -- shut down and then re-image -- a server.
@@ -292,6 +280,17 @@ class ServerManager(base.BootingManagerWithFind):
         """
         self._action('revertResize', server)
 
+    def create_image(self, server, image_name, metadata=None):
+        """
+        Snapshot a server.
+
+        :param server: The :class:`Server` (or its ID) to share onto.
+        :param image_name: Name to give the snapshot image
+        :param meta: Metadata to give newly-created image entity
+        """
+        self._action('createImage', server,
+                     {'name': image_name, 'metadata': metadata or {}})
+
     def _action(self, action, server, info=None):
         """
         Perform a server "action" -- reboot/rebuild/resize/etc.
diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py
index 7f9217e09..c4e79f120 100644
--- a/novaclient/v1_1/shell.py
+++ b/novaclient/v1_1/shell.py
@@ -15,570 +15,316 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-"""
-Command-line interface to the OpenStack Nova API.
-"""
-
-import argparse
 import getpass
-import httplib2
 import os
-import prettytable
-import sys
-import textwrap
 import uuid
 
-import novaclient.v1_1
-from novaclient.v1_1 import exceptions
+from novaclient import exceptions
+from novaclient import utils
+from novaclient.v1_1 import client
 from novaclient.v1_1 import servers
 
 
-def pretty_choice_list(l):
-    return ', '.join("'%s'" % i for i in l)
+CLIENT_CLASS = client.Client
+
 
-# Sentinal for boot --key
 AUTO_KEY = object()
 
 
-# Decorator for args
-def arg(*args, **kwargs):
-    def _decorator(func):
-        # Because of the sematics of decorator composition if we just append
-        # to the options list positional options will appear to be backwards.
-        func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
-        return func
-    return _decorator
+def _translate_flavor_keys(collection):
+    convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
+    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.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'osc flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'osc images'). "\
+          "Defaults to Ubuntu 10.04 LTS.")
+@utils.arg('--meta',
+     metavar="<key=value>",
+     action='append',
+     default=[],
+     help="Record arbitrary key/value metadata. "\
+          "May be give multiple times.")
+@utils.arg('--file',
+     metavar="<dst-path=src-path>",
+     action='append',
+     dest='files',
+     default=[],
+     help="Store arbitrary files from <src-path> locally to <dst-path> "\
+          "on the new server. You may store up to 5 files.")
+@utils.arg('--key',
+     metavar='<path>',
+     nargs='?',
+     const=AUTO_KEY,
+     help="Key the server with an SSH keypair. "\
+          "Looks in ~/.ssh for a key, "\
+          "or takes an explicit <path> to one.")
+@utils.arg('name', metavar='<name>', help='Name for the new server')
+def do_boot(cs, args):
+    """Boot a new server."""
+    name, image, flavor, metadata, files = _boot(cs, args)
+    server = cs.servers.create(args.name, image, flavor,
+                               meta=metadata, files=files)
+    utils.print_dict(server._info)
 
 
-class CommandError(Exception):
-    pass
+def _boot(cs, args):
+    """Boot a new server."""
 
+    flavor = args.flavor or cs.flavors.find(ram=256)
+    image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\
+                                                   "(lucid)")
 
-def env(e):
-    return os.environ.get(e, '')
+    metadata = dict(v.split('=') for v in args.meta)
 
-
-class OpenStackShell(object):
-
-    # Hook for the test suite to inject a fake server.
-    _api_class = novaclient.v1_1.Client
-
-    def __init__(self):
-        self.parser = argparse.ArgumentParser(
-            prog='nova',
-            description=__doc__.strip(),
-            epilog='See "nova help COMMAND" '\
-                   'for help on a specific command.',
-            add_help=False,
-            formatter_class=OpenStackHelpFormatter,
-        )
-
-        # Global arguments
-        self.parser.add_argument('-h', '--help',
-            action='help',
-            help=argparse.SUPPRESS,
-        )
-
-        self.parser.add_argument('--debug',
-            default=False,
-            action='store_true',
-            help=argparse.SUPPRESS)
-
-        self.parser.add_argument('--username',
-            default=env('NOVA_USERNAME'),
-            help='Defaults to env[NOVA_USERNAME].')
-
-        self.parser.add_argument('--apikey',
-            default=env('NOVA_API_KEY'),
-            help='Defaults to env[NOVA_API_KEY].')
-
-        self.parser.add_argument('--projectid',
-            default=env('NOVA_PROJECT_ID'),
-            help='Defaults to env[NOVA_PROJECT_ID].')
-
-        auth_url = env('NOVA_URL')
-        if auth_url == '':
-            auth_url = 'https://auth.api.rackspacecloud.com/v1.0'
-        self.parser.add_argument('--url',
-            default=auth_url,
-            help='Defaults to env[NOVA_URL].')
-
-        # Subcommands
-        subparsers = self.parser.add_subparsers(metavar='<subcommand>')
-        self.subcommands = {}
-
-        # Everything that's do_* is a subcommand.
-        for attr in (a for a in dir(self) if a.startswith('do_')):
-            # I prefer to be hypen-separated instead of underscores.
-            command = attr[3:].replace('_', '-')
-            callback = getattr(self, 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 main(self, argv):
-        # Parse args and call whatever callback was selected
-        args = self.parser.parse_args(argv)
-
-        # Short-circuit and deal with help right away.
-        if args.func == self.do_help:
-            self.do_help(args)
-            return 0
-
-        # Deal with global arguments
-        if args.debug:
-            httplib2.debuglevel = 1
-
-        user, apikey, projectid, url = args.username, args.apikey, \
-                                       args.projectid, args.url
-
-        #FIXME(usrleon): Here should be restrict for project id same as
-        # for username or apikey but for compatibility it is not.
-
-        if not user:
-            raise CommandError("You must provide a username, either via "
-                               "--username or via env[NOVA_USERNAME]")
-        if not apikey:
-            raise CommandError("You must provide an API key, either via "
-                               "--apikey or via env[NOVA_API_KEY]")
-
-        self.cs = self._api_class(user, apikey, projectid, url)
+    files = {}
+    for f in args.files:
+        dst, src = f.split('=', 1)
         try:
-            self.cs.authenticate()
-        except exceptions.Unauthorized:
-            raise CommandError("Invalid OpenStack Nova credentials.")
+            files[dst] = open(src)
+        except IOError, e:
+            raise exceptions.CommandError("Can't open '%s': %s" % (src, e))
 
-        args.func(args)
-
-    @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 CommandError("'%s' is not a valid subcommand." %
-                                                         args.command)
+    if args.key is AUTO_KEY:
+        possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
+                         for k in ('id_dsa.pub', 'id_rsa.pub')]
+        for k in possible_keys:
+            if os.path.exists(k):
+                keyfile = k
+                break
         else:
-            self.parser.print_help()
+            raise exceptions.CommandError("Couldn't find a key file: tried "
+                               "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
+    elif args.key:
+        keyfile = args.key
+    else:
+        keyfile = None
 
-    def _boot(self, args, reservation_id=None, min_count=None, max_count=None):
-        """Boot a new server."""
-        flavor = args.flavor or self.cs.flavors.find(ram=256)
-        image = args.image or self.cs.images.find(name="Ubuntu 10.04 LTS "\
-                                                       "(lucid)")
-
-        metadata = dict(v.split('=') for v in args.meta)
-
-        files = {}
-        for f in args.files:
-            dst, src = f.split('=', 1)
-            try:
-                files[dst] = open(src)
-            except IOError, e:
-                raise CommandError("Can't open '%s': %s" % (src, e))
-
-        if args.key is AUTO_KEY:
-            possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k)
-                             for k in ('id_dsa.pub', 'id_rsa.pub')]
-            for k in possible_keys:
-                if os.path.exists(k):
-                    keyfile = k
-                    break
-            else:
-                raise CommandError("Couldn't find a key file: tried "
-                                   "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub")
-        elif args.key:
-            keyfile = args.key
-        else:
-            keyfile = None
-
-        if keyfile:
-            try:
-                files['/root/.ssh/authorized_keys2'] = open(keyfile)
-            except IOError, e:
-                raise CommandError("Can't open '%s': %s" % (keyfile, e))
-
-        return (args.name, image, flavor, metadata, files)
-
-    @arg('--flavor',
-         default=None,
-         metavar='<flavor>',
-         help="Flavor ID (see 'novaclient flavors'). "\
-              "Defaults to 256MB RAM instance.")
-    @arg('--image',
-         default=None,
-         metavar='<image>',
-         help="Image ID (see 'novaclient images'). "\
-              "Defaults to Ubuntu 10.04 LTS.")
-    @arg('--meta',
-         metavar="<key=value>",
-         action='append',
-         default=[],
-         help="Record arbitrary key/value metadata. "\
-              "May be give multiple times.")
-    @arg('--file',
-         metavar="<dst-path=src-path>",
-         action='append',
-         dest='files',
-         default=[],
-         help="Store arbitrary files from <src-path> locally to <dst-path> "\
-              "on the new server. You may store up to 5 files.")
-    @arg('--key',
-         metavar='<path>',
-         nargs='?',
-         const=AUTO_KEY,
-         help="Key the server with an SSH keypair. "\
-              "Looks in ~/.ssh for a key, "\
-              "or takes an explicit <path> to one.")
-    @arg('name', metavar='<name>', help='Name for the new server')
-    def do_boot(self, args):
-        """Boot a new server."""
-        name, image, flavor, metadata, files = self._boot(args)
-
-        server = self.cs.servers.create(args.name, image, flavor,
-                                        meta=metadata,
-                                        files=files)
-        print_dict(server._info)
-
-    def _translate_flavor_keys(self, collection):
-        convert = [('ram', 'memory_mb'), ('disk', 'local_gb')]
-        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])
-
-    @arg('--fixed_ip',
-        dest='fixed_ip',
-        metavar='<fixed_ip>',
-        default=None,
-        help='Only match against fixed IP.')
-    @arg('--reservation_id',
-        dest='reservation_id',
-        metavar='<reservation_id>',
-        default=None,
-        help='Only return instances that match reservation_id.')
-    @arg('--recurse_zones',
-        dest='recurse_zones',
-        metavar='<0|1>',
-        nargs='?',
-        type=int,
-        const=1,
-        default=0,
-        help='Recurse through all zones if set.')
-    @arg('--ip',
-        dest='ip',
-        metavar='<ip_regexp>',
-        default=None,
-        help='Search with regular expression match by IP address')
-    @arg('--ip6',
-        dest='ip6',
-        metavar='<ip6_regexp>',
-        default=None,
-        help='Search with regular expression match by IPv6 address')
-    @arg('--server_name',
-        dest='server_name',
-        metavar='<name_regexp>',
-        default=None,
-        help='Search with regular expression match by server name')
-    @arg('--name',
-        dest='display_name',
-        metavar='<name_regexp>',
-        default=None,
-        help='Search with regular expression match by display name')
-    @arg('--instance_name',
-        dest='name',
-        metavar='<name_regexp>',
-        default=None,
-        help='Search with regular expression match by instance name')
-    def do_list(self, args):
-        """List active servers."""
-        recurse_zones = args.recurse_zones
-        search_opts = {
-                'reservation_id': args.reservation_id,
-                'fixed_ip': args.fixed_ip,
-                'recurse_zones': recurse_zones,
-                'ip': args.ip,
-                'ip6': args.ip6,
-                'name': args.name,
-                'server_name': args.server_name,
-                'display_name': args.display_name}
-        if recurse_zones:
-            to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
-        else:
-            to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
-        print_list(self.cs.servers.list(search_opts=search_opts),
-                to_print)
-
-    def do_flavor_list(self, args):
-        """Print a list of available 'flavors' (sizes of servers)."""
-        flavors = self.cs.flavors.list()
-        self._translate_flavor_keys(flavors)
-        print_list(flavors, [
-            'ID',
-            'Name',
-            'Memory_MB',
-            'Swap',
-            'Local_GB',
-            'VCPUs',
-            'RXTX_Quota',
-            'RXTX_Cap'])
-
-    def do_image_list(self, args):
-        """Print a list of available images to boot from."""
-        print_list(self.cs.images.list(), ['ID', 'Name', 'Status'])
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('name', metavar='<name>', help='Name of backup or snapshot.')
-    @arg('--image-type',
-         metavar='<backup|snapshot>',
-         default='snapshot',
-         help='type of image (default: snapshot)')
-    @arg('--backup-type',
-         metavar='<daily|weekly>',
-         default=None,
-         help='type of backup')
-    @arg('--rotation',
-         default=None,
-         type=int,
-         metavar='<rotation>',
-         help="Number of backups to retain. Used for backup image_type.")
-    def do_create_image(self, args):
-        """Create a new image by taking a snapshot of a running server."""
-        server = self._find_server(args.server)
-        server.create_image(args.name)
-
-    @arg('image', metavar='<image>', help='Name or ID of image.')
-    def do_image_delete(self, args):
-        """
-        Delete an image.
-
-        It should go without saying, but you can only delete images you
-        created.
-        """
-        image = self._find_image(args.image)
-        image.delete()
-
-    @arg('--hard',
-        dest='reboot_type',
-        action='store_const',
-        const=servers.REBOOT_HARD,
-        default=servers.REBOOT_SOFT,
-        help='Perform a hard reboot (instead of a soft one).')
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_reboot(self, args):
-        """Reboot a server."""
-        self._find_server(args.server).reboot(args.reboot_type)
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('image', metavar='<image>', help="Name or ID of new image.")
-    def do_rebuild(self, args):
-        """Shutdown, re-image, and re-boot a server."""
-        server = self._find_server(args.server)
-        image = self._find_image(args.image)
-        server.rebuild(image)
-
-    @arg('server', metavar='<server>', help='Name (old name) or ID of server.')
-    @arg('name', metavar='<name>', help='New name for the server.')
-    def do_rename(self, args):
-        """Rename a server."""
-        self._find_server(args.server).update(name=args.name)
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    @arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
-    def do_resize(self, args):
-        """Resize a server."""
-        server = self._find_server(args.server)
-        flavor = self._find_flavor(args.flavor)
-        server.resize(flavor)
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_migrate(self, args):
-        """Migrate a server."""
-        self._find_server(args.server).migrate()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_pause(self, args):
-        """Pause a server."""
-        self._find_server(args.server).pause()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_unpause(self, args):
-        """Unpause a server."""
-        self._find_server(args.server).unpause()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_suspend(self, args):
-        """Suspend a server."""
-        self._find_server(args.server).suspend()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_resume(self, args):
-        """Resume a server."""
-        self._find_server(args.server).resume()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_rescue(self, args):
-        """Rescue a server."""
-        self._find_server(args.server).rescue()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_unrescue(self, args):
-        """Unrescue a server."""
-        self._find_server(args.server).unrescue()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_diagnostics(self, args):
-        """Retrieve server diagnostics."""
-        print_dict(self.cs.servers.diagnostics(args.server)[1])
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_actions(self, args):
-        """Retrieve server actions."""
-        print_list(
-            self.cs.servers.actions(args.server),
-            ["Created_At", "Action", "Error"])
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_resize_confirm(self, args):
-        """Confirm a previous resize."""
-        self._find_server(args.server).confirm_resize()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_resize_revert(self, args):
-        """Revert a previous resize (and return to the previous VM)."""
-        self._find_server(args.server).revert_resize()
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_root_password(self, args):
-        """
-        Change the root password for a server.
-        """
-        server = self._find_server(args.server)
-        p1 = getpass.getpass('New password: ')
-        p2 = getpass.getpass('Again: ')
-        if p1 != p2:
-            raise CommandError("Passwords do not match.")
-        server.change_password(p1)
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_show(self, args):
-        """Show details about the given server."""
-        s = self._find_server(args.server)
-
-        info = s._info.copy()
-        addresses = info.pop('addresses')
-        for network_name in addresses.keys():
-            ips = map(lambda x: x["addr"], addresses[network_name])
-            info['%s ip' % network_name] = ', '.join(ips)
-
-        flavor = info.get('flavor', {})
-        flavor_id = flavor.get('id')
-        if flavor_id:
-            info['flavor'] = self._find_flavor(flavor_id).name
-        image = info.get('image', {})
-        image_id = image.get('id')
-        if image_id:
-            info['image'] = self._find_image(image_id).name
-
-        print_dict(info)
-
-    @arg('server', metavar='<server>', help='Name or ID of server.')
-    def do_delete(self, args):
-        """Immediately shut down and delete a server."""
-        self._find_server(args.server).delete()
-
-    def _find_server(self, server):
-        """Get a server by name or ID."""
-        return self._find_resource(self.cs.servers, server)
-
-    def _find_image(self, image):
-        """Get an image by name or ID."""
-        return self._find_resource(self.cs.images, image)
-
-    def _find_flavor(self, flavor):
-        """Get a flavor by name, ID, or RAM size."""
+    if keyfile:
         try:
-            return self._find_resource(self.cs.flavors, flavor)
-        except exceptions.NotFound:
-            return self.cs.flavors.find(ram=flavor)
+            files['/root/.ssh/authorized_keys2'] = open(keyfile)
+        except IOError, e:
+            raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e))
 
-    def _find_resource(self, manager, name_or_id):
-        """Helper for the _find_* methods."""
-        try:
-            if isinstance(name_or_id, int) or name_or_id.isdigit():
-                return manager.get(int(name_or_id))
-
-            try:
-                uuid.UUID(name_or_id)
-                return manager.get(name_or_id)
-            except ValueError:
-                return manager.find(name=name_or_id)
-        except exceptions.NotFound:
-            raise CommandError("No %s with a name or ID of '%s' exists." %
-                         (manager.resource_class.__name__.lower(), name_or_id))
+    return (args.name, image, flavor, metadata, files)
 
 
-# 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 do_flavor_list(cs, args):
+    """Print a list of available 'flavors' (sizes of servers)."""
+    flavors = cs.flavors.list()
+    _translate_flavor_keys(flavors)
+    utils.print_list(flavors, [
+        'ID',
+        'Name',
+        'Memory_MB',
+        'Swap',
+        'Local_GB',
+        'VCPUs',
+        'RXTX_Quota',
+        'RXTX_Cap'])
 
+def do_image_list(cs, args):
+    """Print a list of available images to boot from."""
+    utils.print_list(cs.images.list(), ['ID', 'Name', 'Status'])
 
-# Helpers
-def print_list(objs, fields, formatters={}):
-    pt = prettytable.PrettyTable([f for f in fields], caching=False)
-    pt.aligns = ['l' for f in fields]
+@utils.arg('image', metavar='<image>', help='Name or ID of image.')
+def do_image_delete(cs, args):
+    """
+    Delete an image.
 
-    for o in objs:
-        row = []
-        for field in fields:
-            if field in formatters:
-                row.append(formatters[field](o))
-            else:
-                field_name = field.lower().replace(' ', '_')
-                data = getattr(o, field_name, '')
-                row.append(data)
-        pt.add_row(row)
+    It should go without saying, but you can only delete images you
+    created.
+    """
+    image = _find_image(cs, args.image)
+    image.delete()
 
-    pt.printt(sortby=fields[0])
+@utils.arg('--fixed_ip',
+    dest='fixed_ip',
+    metavar='<fixed_ip>',
+    default=None,
+    help='Only match against fixed IP.')
+@utils.arg('--reservation_id',
+    dest='reservation_id',
+    metavar='<reservation_id>',
+    default=None,
+    help='Only return instances that match reservation_id.')
+@utils.arg('--recurse_zones',
+    dest='recurse_zones',
+    metavar='<0|1>',
+    nargs='?',
+    type=int,
+    const=1,
+    default=0,
+    help='Recurse through all zones if set.')
+@utils.arg('--ip',
+    dest='ip',
+    metavar='<ip_regexp>',
+    default=None,
+    help='Search with regular expression match by IP address')
+@utils.arg('--ip6',
+    dest='ip6',
+    metavar='<ip6_regexp>',
+    default=None,
+    help='Search with regular expression match by IPv6 address')
+@utils.arg('--server_name',
+    dest='server_name',
+    metavar='<name_regexp>',
+    default=None,
+    help='Search with regular expression match by server name')
+@utils.arg('--name',
+    dest='display_name',
+    metavar='<name_regexp>',
+    default=None,
+    help='Search with regular expression match by display name')
+@utils.arg('--instance_name',
+    dest='name',
+    metavar='<name_regexp>',
+    default=None,
+    help='Search with regular expression match by instance name')
+def do_list(cs, args):
+    """List active servers."""
+    recurse_zones = args.recurse_zones
+    search_opts = {
+            'reservation_id': args.reservation_id,
+            'fixed_ip': args.fixed_ip,
+            'recurse_zones': recurse_zones,
+            'ip': args.ip,
+            'ip6': args.ip6,
+            'name': args.name,
+            'server_name': args.server_name,
+            'display_name': args.display_name}
+    if recurse_zones:
+        to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP']
+    else:
+        to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP']
+    utils.print_list(cs.servers.list(search_opts=search_opts),
+            to_print)
 
+@utils.arg('--hard',
+    dest='reboot_type',
+    action='store_const',
+    const=servers.REBOOT_HARD,
+    default=servers.REBOOT_SOFT,
+    help='Perform a hard reboot (instead of a soft one).')
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_reboot(cs, args):
+    """Reboot a server."""
+    _find_server(cs, args.server).reboot(args.reboot_type)
 
-def print_dict(d):
-    pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
-    pt.aligns = ['l', 'l']
-    [pt.add_row(list(r)) for r in d.iteritems()]
-    pt.printt(sortby='Property')
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('image', metavar='<image>', help="Name or ID of new image.")
+def do_rebuild(cs, args):
+    """Shutdown, re-image, and re-boot a server."""
+    server = _find_server(cs, args.server)
+    image = _find_image(cs, args.image)
+    server.rebuild(image)
 
+@utils.arg('server', metavar='<server>', help='Name (old name) or ID of server.')
+@utils.arg('name', metavar='<name>', help='New name for the server.')
+def do_rename(cs, args):
+    """Rename a server."""
+    _find_server(cs, args.server).update(name=args.name)
 
-def main():
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('flavor', metavar='<flavor>', help="Name or ID of new flavor.")
+def do_resize(cs, args):
+    """Resize a server."""
+    server = _find_server(cs, args.server)
+    flavor = _find_flavor(cs, args.flavor)
+    server.resize(flavor)
+
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_resize_confirm(cs, args):
+    """Confirm a previous resize."""
+    _find_server(cs, args.server).confirm_resize()
+
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_resize_revert(cs, args):
+    """Revert a previous resize (and return to the previous VM)."""
+    _find_server(cs, args.server).revert_resize()
+
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_root_password(cs, args):
+    """
+    Change the root password for a server.
+    """
+    server = _find_server(cs, args.server)
+    p1 = getpass.getpass('New password: ')
+    p2 = getpass.getpass('Again: ')
+    if p1 != p2:
+        raise exceptions.CommandError("Passwords do not match.")
+    server.change_password(p1)
+
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+@utils.arg('name', metavar='<name>', help='Name of snapshot.')
+def do_create_image(cs, args):
+    """Create a new image by taking a snapshot of a running server."""
+    server = _find_server(cs, args.server)
+    cs.servers.create_image(server, args.name)
+
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_show(cs, args):
+    """Show details about the given server."""
+    s = _find_server(cs, args.server)
+
+    info = s._info.copy()
+    addresses = info.pop('addresses', [])
+    for addrtype in addresses:
+        ips = map(lambda x: x['addr'], addresses[addrtype])
+        info['%s ip' % addrtype] = ', '.join(ips)
+
+    flavor = info.get('flavor', {})
+    flavor_id = flavor.get('id', '')
+    info['flavor'] = _find_flavor(cs, flavor_id).name
+
+    image = info.get('image', {})
+    image_id = image.get('id', '')
+    info['image'] = _find_image(cs, image_id).name
+
+    utils.print_dict(info)
+
+@utils.arg('server', metavar='<server>', help='Name or ID of server.')
+def do_delete(cs, args):
+    """Immediately shut down and delete a server."""
+    _find_server(cs, args.server).delete()
+
+def _find_server(cs, server):
+    """Get a server by name or ID."""
+    return _find_resource(cs.servers, server)
+
+def _find_image(cs, image):
+    """Get an image by name or ID."""
+    return _find_resource(cs.images, image)
+
+def _find_flavor(cs, flavor):
+    """Get a flavor by name, ID, or RAM size."""
     try:
-        OpenStackShell().main(sys.argv[1:])
+        return _find_resource(cs.flavors, flavor)
+    except exceptions.NotFound:
+        return cs.flavors.find(ram=flavor)
+
+def _find_resource(manager, name_or_id):
+    """Helper for the _find_* methods."""
+    try:
+        if isinstance(name_or_id, int) or name_or_id.isdigit():
+            return manager.get(int(name_or_id))
+
+        try:
+            uuid.UUID(name_or_id)
+            return manager.get(name_or_id)
+        except ValueError:
+            return manager.find(name=name_or_id)
+    except exceptions.NotFound:
+        raise exceptions.CommandError("No %s with a name or ID of '%s' exists." %
+                     (manager.resource_class.__name__.lower(), name_or_id))
 
-    except Exception, e:
-        if httplib2.debuglevel == 1:
-            raise  # dump stack.
-        else:
-            print >> sys.stderr, e
-        sys.exit(1)
diff --git a/tests/fakes.py b/tests/fakes.py
new file mode 100644
index 000000000..b63cc8087
--- /dev/null
+++ b/tests/fakes.py
@@ -0,0 +1,66 @@
+"""
+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.
+"""
+
+import novaclient.client
+
+
+def assert_has_keys(dict, required=[], optional=[]):
+    keys = dict.keys()
+    for k in required:
+        assert k in keys
+    allowed_keys = set(required) | set(optional)
+    extra_keys = set(keys).difference(set(required + optional))
+    if extra_keys:
+        raise AssertionError("found unexpected keys: %s" % list(extra_keys))
+
+
+class FakeClient(object):
+
+    def assert_called(self, method, url, body=None):
+        """
+        Assert than an API method was just called.
+        """
+        expected = (method, url)
+        called = self.client.callstack[-1][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[-1][2] == body
+
+        self.client.callstack = []
+
+    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:
+            called = entry[0:2]
+            if expected == entry[0:2]:
+                found = True
+                break
+
+        assert found, 'Expected %s %s; got %s' % \
+                              (expected, self.client.callstack)
+        if body is not None:
+            assert entry[2] == body
+
+        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..1dda3086f
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,60 @@
+
+import mock
+
+from novaclient import base
+from novaclient import exceptions
+from novaclient.v1_0 import flavors
+from tests.v1_0 import fakes
+from tests import utils
+
+
+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_resource_lazy_getattr(self):
+        f = flavors.Flavor(cs.flavors, {'id': 1})
+        self.assertEqual(f.name, '256 MB Server')
+        cs.assert_called('GET', '/flavors/1')
+
+        # Missing stuff still fails after a second get
+        self.assertRaises(AttributeError, getattr, f, 'blahblah')
+        cs.assert_called('GET', '/flavors/1')
+
+    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 = flavors.Flavor(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.flavors.findall(vegetable='carrot')
+
+        # However, find() should raise an error
+        self.assertRaises(exceptions.NotFound,
+                          cs.flavors.find,
+                          vegetable='carrot')
diff --git a/tests/test_http.py b/tests/test_http.py
new file mode 100644
index 000000000..5e8e6ef77
--- /dev/null
+++ b/tests/test_http.py
@@ -0,0 +1,58 @@
+
+import httplib2
+import mock
+
+from novaclient import client
+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", "apikey",
+                           "project_id", "auth_test")
+    cl.management_url = "http://example.com"
+    cl.auth_token = "token"
+    return cl
+
+
+class ClientTest(utils.TestCase):
+
+    def test_get(self):
+        cl = get_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,
+            }
+            mock_request.assert_called_with("http://example.com/hi?fresh=1234",
+                                            "GET", headers=headers)
+            # Automatic JSON parsing
+            self.assertEqual(body, {"hi": "there"})
+
+        test_get_call()
+
+
+    def test_post(self):
+        cl = get_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",
+                "User-Agent": cl.USER_AGENT
+            }
+            mock_request.assert_called_with("http://example.com/hi", "POST",
+                                            headers=headers, body='[1, 2, 3]')
+
+        test_post_call()
diff --git a/tests/test_shell.py b/tests/test_shell.py
new file mode 100644
index 000000000..869f2d822
--- /dev/null
+++ b/tests/test_shell.py
@@ -0,0 +1,39 @@
+
+import os
+import mock
+import httplib2
+
+from novaclient.shell import OpenStackComputeShell
+from novaclient import exceptions
+from tests import utils
+
+
+class ShellTest(utils.TestCase):
+
+    # Patch os.environ to avoid required auth info.
+    def setUp(self):
+        global _old_env
+        fake_env = {
+            'NOVA_USERNAME': 'username',
+            'NOVA_API_KEY': 'password',
+            'NOVA_PROJECT_ID': 'project_id'
+        }
+        _old_env, os.environ = os.environ, fake_env.copy()
+
+        # Make a fake shell object, a helping wrapper to call it, and a quick way
+        # of asserting that certain API calls were made.
+        global shell, _shell, assert_called, assert_called_anytime
+        _shell = OpenStackComputeShell()
+        shell = lambda cmd: _shell.main(cmd.split())
+
+    def tearDown(self):
+        global _old_env
+        os.environ = _old_env
+
+    def test_help_unknown_command(self):
+        self.assertRaises(exceptions.CommandError, shell, 'help foofoo')
+
+    def test_debug(self):
+        httplib2.debuglevel = 0
+        shell('--debug help')
+        assert httplib2.debuglevel == 1
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 000000000..4f1ca3f1e
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,5 @@
+import unittest
+
+
+class TestCase(unittest.TestCase):
+    pass
diff --git a/tests/v1_0/fakes.py b/tests/v1_0/fakes.py
index 2ee96e0ae..c47affc5e 100644
--- a/tests/v1_0/fakes.py
+++ b/tests/v1_0/fakes.py
@@ -1,82 +1,22 @@
-"""
-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 fail. I've indicated in comments the places where actual
-behavior differs from the spec.
-"""
-
-from __future__ import absolute_import
-
-import urlparse
-import urllib
-
 import httplib2
+import urllib
+import urlparse
 
-from novaclient.v1_0 import Client
-from novaclient.v1_0.client import HTTPClient
-
-from .utils import fail, assert_in, assert_not_in, assert_has_keys
+from novaclient import client as base_client
+from novaclient.v1_0 import client
+from tests import fakes
 
 
-def assert_equal(value_one, value_two):
-    assert value_one == value_two
+class FakeClient(fakes.FakeClient, client.Client):
+
+    def __init__(self, *args, **kwargs):
+        client.Client.__init__(self, 'username', 'apikey',
+                               'project_id', 'auth_url')
+        self.client = FakeHTTPClient(**kwargs)
 
 
-class FakeClient(Client):
-    def __init__(self, username=None, password=None, project_id=None,
-                 auth_url=None):
-        super(FakeClient, self).__init__('username', 'apikey',
-                                         'project_id', 'auth_url')
-        self.client = FakeHTTPClient()
-
-    def assert_called(self, method, url, body=None):
-        """
-        Assert than an API method was just called.
-        """
-        expected = (method, url)
-        called = self.client.callstack[-1][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_equal(self.client.callstack[-1][2], body)
-
-        self.client.callstack = []
-
-    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:
-            called = entry[0:2]
-            if expected == entry[0:2]:
-                found = True
-                break
-
-        assert found, 'Expected %s; got %s' % \
-                              (expected, self.client.callstack)
-        if body is not None:
-            assert_equal(entry[2], body)
-
-        self.client.callstack = []
-
-    def authenticate(self):
-        pass
-
-
-class FakeHTTPClient(HTTPClient):
-    def __init__(self):
+class FakeHTTPClient(base_client.HTTPClient):
+    def __init__(self, **kwargs):
         self.username = 'username'
         self.apikey = 'apikey'
         self.auth_url = 'auth_url'
@@ -85,15 +25,15 @@ class FakeHTTPClient(HTTPClient):
     def _cs_request(self, url, method, **kwargs):
         # Check that certain things are called correctly
         if method in ['GET', 'DELETE']:
-            assert_not_in('body', kwargs)
+            assert 'body' not in kwargs
         elif method in ['PUT', 'POST']:
-            assert_in('body', kwargs)
+            assert 'body' in kwargs
 
         # Call the method
         munged_url = url.strip('/').replace('/', '_').replace('.', '_')
         callback = "%s_%s" % (method.lower(), munged_url)
         if not hasattr(self, callback):
-            fail('Called unknown API method: %s %s' % (method, url))
+            raise AssertionError('Called unknown API method: %s %s' % (method, url))
 
         # Note the call
         self.callstack.append((method, url, kwargs.get('body', None)))
@@ -210,14 +150,14 @@ class FakeHTTPClient(HTTPClient):
         ]})
 
     def post_servers(self, body, **kw):
-        assert_equal(body.keys(), ['server'])
-        assert_has_keys(body['server'],
-                        required=['name', 'imageId', 'flavorId'],
-                        optional=['sharedIpGroupId', 'metadata',
-                                'personality', 'min_count', 'max_count'])
+        assert body.keys() == ['server']
+        fakes.assert_has_keys(body['server'],
+                             required=['name', 'imageId', 'flavorId'],
+                             optional=['sharedIpGroupId', 'metadata',
+                            'personality', 'min_count', 'max_count'])
         if 'personality' in body['server']:
             for pfile in body['server']['personality']:
-                assert_has_keys(pfile, required=['path', 'contents'])
+                fakes.assert_has_keys(pfile, required=['path', 'contents'])
         return (202, self.get_servers_1234()[1])
 
     def get_servers_1234(self, **kw):
@@ -229,8 +169,8 @@ class FakeHTTPClient(HTTPClient):
         return (200, r)
 
     def put_servers_1234(self, body, **kw):
-        assert_equal(body.keys(), ['server'])
-        assert_has_keys(body['server'], optional=['name', 'adminPass'])
+        assert body.keys() == ['server']
+        fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
         return (204, None)
 
     def delete_servers_1234(self, **kw):
@@ -253,8 +193,8 @@ class FakeHTTPClient(HTTPClient):
                       self.get_servers_1234_ips()[1]['addresses']['private']})
 
     def put_servers_1234_ips_public_1_2_3_4(self, body, **kw):
-        assert_equal(body.keys(), ['shareIp'])
-        assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
+        assert body.keys() == ['shareIp']
+        fakes.assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
                                          'configureServer'])
         return (202, None)
 
@@ -266,29 +206,32 @@ class FakeHTTPClient(HTTPClient):
     #
 
     def post_servers_1234_action(self, body, **kw):
-        assert_equal(len(body.keys()), 1)
+        assert len(body.keys()) == 1
         action = body.keys()[0]
         if action == 'reboot':
-            assert_equal(body[action].keys(), ['type'])
-            assert_in(body[action]['type'], ['HARD', 'SOFT'])
+            assert body[action].keys() == ['type']
+            assert body[action]['type'] in ['HARD', 'SOFT']
         elif action == 'rebuild':
-            assert_equal(body[action].keys(), ['imageId'])
+            assert body[action].keys() == ['imageId']
         elif action == 'resize':
-            assert_equal(body[action].keys(), ['flavorId'])
+            assert body[action].keys() == ['flavorId']
+        elif action == 'createBackup':
+            assert set(body[action].keys()) ==  \
+                   set(['name', 'rotation', 'backup_type'])
         elif action == 'confirmResize':
-            assert_equal(body[action], None)
+            assert body[action] is None
             # This one method returns a different response code
             return (204, None)
         elif action == 'revertResize':
-            assert_equal(body[action], None)
+            assert body[action] is None
         elif action == 'migrate':
-            assert_equal(body[action], None)
+            assert body[action] is None
         elif action == 'addFixedIp':
-            assert_equal(body[action].keys(), ['networkId'])
+            assert body[action].keys() == ['networkId']
         elif action == 'removeFixedIp':
-            assert_equal(body[action].keys(), ['address'])
+            assert body[action].keys() == ['address']
         else:
-            fail("Unexpected server action: %s" % action)
+            raise AssertionError("Unexpected server action: %s" % action)
         return (202, None)
 
     #
@@ -349,8 +292,8 @@ class FakeHTTPClient(HTTPClient):
         return (200, {'image': self.get_images_detail()[1]['images'][1]})
 
     def post_images(self, body, **kw):
-        assert_equal(body.keys(), ['image'])
-        assert_has_keys(body['image'], required=['serverId', 'name', 'image_type', 'backup_type', 'rotation'])
+        assert body.keys() == ['image']
+        fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
         return (202, self.get_images_1()[1])
 
     def delete_images_1(self, **kw):
@@ -367,8 +310,8 @@ class FakeHTTPClient(HTTPClient):
         }})
 
     def post_servers_1234_backup_schedule(self, body, **kw):
-        assert_equal(body.keys(), ['backupSchedule'])
-        assert_has_keys(body['backupSchedule'], required=['enabled'],
+        assert body.keys() == ['backupSchedule']
+        fakes.assert_has_keys(body['backupSchedule'], required=['enabled'],
                                                 optional=['weekly', 'daily'])
         return (204, None)
 
@@ -395,8 +338,8 @@ class FakeHTTPClient(HTTPClient):
                    self.get_shared_ip_groups_detail()[1]['sharedIpGroups'][0]})
 
     def post_shared_ip_groups(self, body, **kw):
-        assert_equal(body.keys(), ['sharedIpGroup'])
-        assert_has_keys(body['sharedIpGroup'], required=['name'],
+        assert body.keys() == ['sharedIpGroup']
+        fakes.assert_has_keys(body['sharedIpGroup'], required=['name'],
                                                optional=['server'])
         return (201, {'sharedIpGroup': {
             'id': 10101,
@@ -417,7 +360,6 @@ class FakeHTTPClient(HTTPClient):
             {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice'},
         ]})
 
-
     def get_zones_detail(self, **kw):
         return (200, {'zones': [
             {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
@@ -435,16 +377,16 @@ class FakeHTTPClient(HTTPClient):
         return (200, r)
 
     def post_zones(self, body, **kw):
-        assert_equal(body.keys(), ['zone'])
-        assert_has_keys(body['zone'],
+        assert body.keys() == ['zone']
+        fakes.assert_has_keys(body['zone'],
                         required=['api_url', 'username', 'password'],
                         optional=['weight_offset', 'weight_scale'])
 
         return (202, self.get_zones_1()[1])
 
     def put_zones_1(self, body, **kw):
-        assert_equal(body.keys(), ['zone'])
-        assert_has_keys(body['zone'], optional=['api_url', 'username',
+        assert body.keys() == ['zone']
+        fakes.assert_has_keys(body['zone'], optional=['api_url', 'username',
                                                 'password',
                                                 'weight_offset',
                                                 'weight_scale'])
@@ -457,12 +399,14 @@ class FakeHTTPClient(HTTPClient):
     # Accounts
     #
     def post_accounts_test_account_create_instance(self, body, **kw):
-        assert_equal(body.keys(), ['server'])
-        assert_has_keys(body['server'],
+        assert body.keys() == ['server']
+        fakes.assert_has_keys(body['server'],
                         required=['name', 'imageId', 'flavorId'],
                         optional=['sharedIpGroupId', 'metadata',
                                 'personality', 'min_count', 'max_count'])
         if 'personality' in body['server']:
             for pfile in body['server']['personality']:
-                assert_has_keys(pfile, required=['path', 'contents'])
+                fakes.assert_has_keys(pfile, required=['path', 'contents'])
         return (202, self.get_servers_1234()[1])
+
+
diff --git a/tests/v1_0/test_accounts.py b/tests/v1_0/test_accounts.py
index 93eeed92f..72d77b17e 100644
--- a/tests/v1_0/test_accounts.py
+++ b/tests/v1_0/test_accounts.py
@@ -1,21 +1,25 @@
-from __future__ import absolute_import
 
 import StringIO
 
-from .fakes import FakeClient
+from tests.v1_0 import fakes
+from tests import utils
 
-os = FakeClient()
 
-def test_instance_creation_for_account():
-    s = os.accounts.create_instance_for(
-        account_id='test_account',
-        name="My server",
-        image=1,
-        flavor=1,
-        meta={'foo': 'bar'},
-        ipgroup=1,
-        files={
-            '/etc/passwd': 'some data',                 # a file
-            '/tmp/foo.txt': StringIO.StringIO('data')   # a stream
-        })
-    os.assert_called('POST', '/accounts/test_account/create_instance')
+cs = fakes.FakeClient()
+
+
+class AccountsTest(utils.TestCase):
+
+    def test_instance_creation_for_account(self):
+        s = cs.accounts.create_instance_for(
+            account_id='test_account',
+            name="My server",
+            image=1,
+            flavor=1,
+            meta={'foo': 'bar'},
+            ipgroup=1,
+            files={
+                '/etc/passwd': 'some data',                 # a file
+                '/tmp/foo.txt': StringIO.StringIO('data')   # a stream
+            })
+        cs.assert_called('POST', '/accounts/test_account/create_instance')
diff --git a/tests/v1_0/test_auth.py b/tests/v1_0/test_auth.py
index d27907cb4..f66336f2b 100644
--- a/tests/v1_0/test_auth.py
+++ b/tests/v1_0/test_auth.py
@@ -1,71 +1,74 @@
-import mock
+
 import httplib2
-from nose.tools import assert_raises, assert_equal
+import mock
 
-import novaclient.v1_0
-from novaclient.v1_0 import exceptions
+from novaclient.v1_0 import client
+from novaclient import exceptions
+from tests import utils
 
 
-def test_authenticate_success():
-    cs = novaclient.v1_0.Client("username", "apikey", "project_id")
-    auth_response = httplib2.Response({
-        'status': 204,
-        'x-server-management-url':
-                    'https://servers.api.rackspacecloud.com/v1.0/443470',
-        'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
-    })
-    mock_request = mock.Mock(return_value=(auth_response, None))
+class AuthenticationTests(utils.TestCase):
 
-    @mock.patch.object(httplib2.Http, "request", mock_request)
-    def test_auth_call():
-        cs.client.authenticate()
-        mock_request.assert_called_with(cs.client.auth_url, 'GET',
+    def test_authenticate_success(self):
+        cs = client.Client("username", "apikey", "project_id")
+        management_url = 'https://servers.api.rackspacecloud.com/v1.0/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={
                 'X-Auth-User': 'username',
                 'X-Auth-Key': 'apikey',
                 'X-Auth-Project-Id': 'project_id',
                 'User-Agent': cs.client.USER_AGENT
-            })
-        assert_equal(cs.client.management_url,
-                     auth_response['x-server-management-url'])
-        assert_equal(cs.client.auth_token, auth_response['x-auth-token'])
+            }
+            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()
+        test_auth_call()
 
+    def test_authenticate_failure(self):
+        cs = client.Client("username", "apikey", "project_id")
+        auth_response = httplib2.Response({'status': 401})
+        mock_request = mock.Mock(return_value=(auth_response, None))
 
-def test_authenticate_failure():
-    cs = novaclient.v1_0.Client("username", "apikey", "project_id")
-    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)
 
-    @mock.patch.object(httplib2.Http, "request", mock_request)
-    def test_auth_call():
-        assert_raises(exceptions.Unauthorized, cs.client.authenticate)
+        test_auth_call()
 
-    test_auth_call()
+    def test_auth_automatic(self):
+        cs = client.Client("username", "apikey", "project_id")
+        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()
 
-def test_auth_automatic():
-    client = novaclient.v1_0.Client("username", "apikey", "project_id").client
-    client.management_url = ''
-    mock_request = mock.Mock(return_value=(None, None))
+        test_auth_call()
 
-    @mock.patch.object(client, 'request', mock_request)
-    @mock.patch.object(client, 'authenticate')
-    def test_auth_call(m):
-        client.get('/')
-        m.assert_called()
-        mock_request.assert_called()
+    def test_auth_manual(self):
+        cs = client.Client("username", "apikey", "project_id")
 
-    test_auth_call()
+        @mock.patch.object(cs.client, 'authenticate')
+        def test_auth_call(m):
+            cs.authenticate()
+            m.assert_called()
 
-
-def test_auth_manual():
-    cs = novaclient.v1_0.Client("username", "apikey", "project_id")
-
-    @mock.patch.object(cs.client, 'authenticate')
-    def test_auth_call(m):
-        cs.authenticate()
-        m.assert_called()
-
-    test_auth_call()
+        test_auth_call()
diff --git a/tests/v1_0/test_backup_schedules.py b/tests/v1_0/test_backup_schedules.py
index 831f81067..ebf5dad40 100644
--- a/tests/v1_0/test_backup_schedules.py
+++ b/tests/v1_0/test_backup_schedules.py
@@ -1,60 +1,60 @@
-from __future__ import absolute_import
 
 from novaclient.v1_0 import backup_schedules
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
-
-os = FakeClient()
+from tests.v1_0 import fakes
+from tests import utils
 
 
-def test_get_backup_schedule():
-    s = os.servers.get(1234)
-
-    # access via manager
-    b = os.backup_schedules.get(server=s)
-    assert_isinstance(b, backup_schedules.BackupSchedule)
-    os.assert_called('GET', '/servers/1234/backup_schedule')
-
-    b = os.backup_schedules.get(server=1234)
-    assert_isinstance(b, backup_schedules.BackupSchedule)
-    os.assert_called('GET', '/servers/1234/backup_schedule')
-
-    # access via instance
-    assert_isinstance(s.backup_schedule, backup_schedules.BackupSchedule)
-    os.assert_called('GET', '/servers/1234/backup_schedule')
-
-    # Just for coverage's sake
-    b = s.backup_schedule.get()
-    os.assert_called('GET', '/servers/1234/backup_schedule')
+cs = fakes.FakeClient()
 
 
-def test_create_update_backup_schedule():
-    s = os.servers.get(1234)
+class BackupSchedulesTest(utils.TestCase):
 
-    # create/update via manager
-    os.backup_schedules.update(
-        server=s,
-        enabled=True,
-        weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY,
-        daily=backup_schedules.BACKUP_DAILY_H_1000_1200
-    )
-    os.assert_called('POST', '/servers/1234/backup_schedule')
+    def test_get_backup_schedule(self):
+        s = cs.servers.get(1234)
 
-    # and via instance
-    s.backup_schedule.update(enabled=False)
-    os.assert_called('POST', '/servers/1234/backup_schedule')
+        # access via manager
+        b = cs.backup_schedules.get(server=s)
+        self.assertTrue(isinstance(b, backup_schedules.BackupSchedule))
+        cs.assert_called('GET', '/servers/1234/backup_schedule')
 
+        b = cs.backup_schedules.get(server=1234)
+        self.assertTrue(isinstance(b, backup_schedules.BackupSchedule))
+        cs.assert_called('GET', '/servers/1234/backup_schedule')
 
-def test_delete_backup_schedule():
-    s = os.servers.get(1234)
+        # access via instance
+        self.assertTrue(isinstance(s.backup_schedule,
+                                   backup_schedules.BackupSchedule))
+        cs.assert_called('GET', '/servers/1234/backup_schedule')
 
-    # delete via manager
-    os.backup_schedules.delete(s)
-    os.assert_called('DELETE', '/servers/1234/backup_schedule')
-    os.backup_schedules.delete(1234)
-    os.assert_called('DELETE', '/servers/1234/backup_schedule')
+        # Just for coverage's sake
+        b = s.backup_schedule.get()
+        cs.assert_called('GET', '/servers/1234/backup_schedule')
 
-    # and via instance
-    s.backup_schedule.delete()
-    os.assert_called('DELETE', '/servers/1234/backup_schedule')
+    def test_create_update_backup_schedule(self):
+        s = cs.servers.get(1234)
+
+        # create/update via manager
+        cs.backup_schedules.update(
+            server=s,
+            enabled=True,
+            weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY,
+            daily=backup_schedules.BACKUP_DAILY_H_1000_1200
+        )
+        cs.assert_called('POST', '/servers/1234/backup_schedule')
+
+        # and via instance
+        s.backup_schedule.update(enabled=False)
+        cs.assert_called('POST', '/servers/1234/backup_schedule')
+
+    def test_delete_backup_schedule(self):
+        s = cs.servers.get(1234)
+
+        # delete via manager
+        cs.backup_schedules.delete(s)
+        cs.assert_called('DELETE', '/servers/1234/backup_schedule')
+        cs.backup_schedules.delete(1234)
+        cs.assert_called('DELETE', '/servers/1234/backup_schedule')
+
+        # and via instance
+        s.backup_schedule.delete()
+        cs.assert_called('DELETE', '/servers/1234/backup_schedule')
diff --git a/tests/v1_0/test_base.py b/tests/v1_0/test_base.py
deleted file mode 100644
index db440bbe6..000000000
--- a/tests/v1_0/test_base.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from __future__ import absolute_import
-
-import mock
-from nose.tools import assert_equal, assert_not_equal, assert_raises
-
-from novaclient.v1_0 import flavors
-from novaclient.v1_0 import exceptions
-from novaclient.v1_0 import base
-
-from .fakes import FakeClient
-
-os = FakeClient()
-
-
-def test_resource_repr():
-    r = base.Resource(None, dict(foo="bar", baz="spam"))
-    assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
-
-
-def test_getid():
-    assert_equal(base.getid(4), 4)
-
-    class O(object):
-        id = 4
-    assert_equal(base.getid(O), 4)
-
-
-def test_resource_lazy_getattr():
-    f = flavors.Flavor(os.flavors, {'id': 1})
-    assert_equal(f.name, '256 MB Server')
-    os.assert_called('GET', '/flavors/1')
-
-    # Missing stuff still fails after a second get
-    assert_raises(AttributeError, getattr, f, 'blahblah')
-    os.assert_called('GET', '/flavors/1')
-
-
-def test_eq():
-    # 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'})
-    assert_equal(r1, r2)
-
-    # Two resoruces of different types: never equal
-    r1 = base.Resource(None, {'id': 1})
-    r2 = flavors.Flavor(None, {'id': 1})
-    assert_not_equal(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})
-    assert_equal(r1, r2)
-
-
-def test_findall_invalid_attribute():
-    # Make sure findall with an invalid attribute doesn't cause errors.
-    # The following should not raise an exception.
-    os.flavors.findall(vegetable='carrot')
-
-    # However, find() should raise an error
-    assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot')
diff --git a/tests/v1_0/test_client.py b/tests/v1_0/test_client.py
deleted file mode 100644
index e7a63b7d7..000000000
--- a/tests/v1_0/test_client.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import mock
-import httplib2
-
-from novaclient.v1_0 import client
-from nose.tools import assert_equal
-
-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", "apikey", "project_id", "auth_test")
-    cl.management_url = "http://example.com"
-    cl.auth_token = "token"
-    return cl
-
-
-def test_get():
-    cl = get_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")
-        mock_request.assert_called_with("http://example.com/hi?fresh=1234",
-            "GET",
-            headers={"X-Auth-Token": "token",
-                     "X-Auth-Project-Id": "project_id",
-                     "User-Agent": cl.USER_AGENT})
-        # Automatic JSON parsing
-        assert_equal(body, {"hi": "there"})
-
-    test_get_call()
-
-
-def test_post():
-    cl = get_client()
-
-    @mock.patch.object(httplib2.Http, "request", mock_request)
-    def test_post_call():
-        cl.post("/hi", body=[1, 2, 3])
-        mock_request.assert_called_with("http://example.com/hi", "POST",
-            headers={
-                "X-Auth-Token": "token",
-                "X-Auth-Project-Id": "project_id",
-                "Content-Type": "application/json",
-                "User-Agent": cl.USER_AGENT},
-            body='[1, 2, 3]'
-        )
-
-    test_post_call()
diff --git a/tests/v1_0/test_flavors.py b/tests/v1_0/test_flavors.py
index 3c9f644c3..30cf84c47 100644
--- a/tests/v1_0/test_flavors.py
+++ b/tests/v1_0/test_flavors.py
@@ -1,42 +1,38 @@
-from __future__ import absolute_import
-
-from nose.tools import assert_raises, assert_equal
 
+from novaclient import exceptions
 from novaclient.v1_0 import flavors
-from novaclient.v1_0 import exceptions
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
-
-os = FakeClient()
+from tests.v1_0 import fakes
+from tests import utils
 
 
-def test_list_flavors():
-    fl = os.flavors.list()
-    os.assert_called('GET', '/flavors/detail')
-    [assert_isinstance(f, flavors.Flavor) for f in fl]
+cs = fakes.FakeClient()
 
 
-def test_list_flavors_undetailed():
-    fl = os.flavors.list(detailed=False)
-    os.assert_called('GET', '/flavors')
-    [assert_isinstance(f, flavors.Flavor) for f in fl]
+class FlavorsTest(utils.TestCase):
 
+    def test_list_flavors(self):
+        fl = cs.flavors.list()
+        cs.assert_called('GET', '/flavors/detail')
+        [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
 
-def test_get_flavor_details():
-    f = os.flavors.get(1)
-    os.assert_called('GET', '/flavors/1')
-    assert_isinstance(f, flavors.Flavor)
-    assert_equal(f.ram, 256)
-    assert_equal(f.disk, 10)
+    def test_list_flavors_undetailed(self):
+        fl = cs.flavors.list(detailed=False)
+        cs.assert_called('GET', '/flavors')
+        [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
 
+    def test_get_flavor_details(self):
+        f = cs.flavors.get(1)
+        cs.assert_called('GET', '/flavors/1')
+        self.assertTrue(isinstance(f, flavors.Flavor))
+        self.assertEqual(f.ram, 256)
+        self.assertEqual(f.disk, 10)
 
-def test_find():
-    f = os.flavors.find(ram=256)
-    os.assert_called('GET', '/flavors/detail')
-    assert_equal(f.name, '256 MB Server')
+    def test_find(self):
+        f = cs.flavors.find(ram=256)
+        cs.assert_called('GET', '/flavors/detail')
+        self.assertEqual(f.name, '256 MB Server')
 
-    f = os.flavors.find(disk=20)
-    assert_equal(f.name, '512 MB Server')
+        f = cs.flavors.find(disk=20)
+        self.assertEqual(f.name, '512 MB Server')
 
-    assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)
+        self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345)
diff --git a/tests/v1_0/test_images.py b/tests/v1_0/test_images.py
index 9e9bb4732..41c1399f7 100644
--- a/tests/v1_0/test_images.py
+++ b/tests/v1_0/test_images.py
@@ -1,51 +1,45 @@
-from __future__ import absolute_import
-
-from nose.tools import assert_equal
 
 from novaclient.v1_0 import images
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
-
-os = FakeClient()
+from tests.v1_0 import fakes
+from tests import utils
 
 
-def test_list_images():
-    il = os.images.list()
-    os.assert_called('GET', '/images/detail')
-    [assert_isinstance(i, images.Image) for i in il]
+cs = fakes.FakeClient()
 
 
-def test_list_images_undetailed():
-    il = os.images.list(detailed=False)
-    os.assert_called('GET', '/images')
-    [assert_isinstance(i, images.Image) for i in il]
+class ImagesTest(utils.TestCase):
 
+    def test_list_images(self):
+        il = cs.images.list()
+        cs.assert_called('GET', '/images/detail')
+        [self.assertTrue(isinstance(i, images.Image)) for i in il]
 
-def test_get_image_details():
-    i = os.images.get(1)
-    os.assert_called('GET', '/images/1')
-    assert_isinstance(i, images.Image)
-    assert_equal(i.id, 1)
-    assert_equal(i.name, 'CentOS 5.2')
+    def test_list_images_undetailed(self):
+        il = cs.images.list(detailed=False)
+        cs.assert_called('GET', '/images')
+        [self.assertTrue(isinstance(i, images.Image)) for i in il]
 
+    def test_get_image_details(self):
+        i = cs.images.get(1)
+        cs.assert_called('GET', '/images/1')
+        self.assertTrue(isinstance(i, images.Image))
+        self.assertEqual(i.id, 1)
+        self.assertEqual(i.name, 'CentOS 5.2')
 
-def test_create_image():
-    i = os.images.create(server=1234, name="Just in case")
-    os.assert_called('POST', '/images')
-    assert_isinstance(i, images.Image)
+    def test_create_image(self):
+        i = cs.images.create(server=1234, name="Just in case")
+        cs.assert_called('POST', '/images')
+        self.assertTrue(isinstance(i, images.Image))
 
+    def test_delete_image(self):
+        cs.images.delete(1)
+        cs.assert_called('DELETE', '/images/1')
 
-def test_delete_image():
-    os.images.delete(1)
-    os.assert_called('DELETE', '/images/1')
+    def test_find(self):
+        i = cs.images.find(name="CentOS 5.2")
+        self.assertEqual(i.id, 1)
+        cs.assert_called('GET', '/images/detail')
 
-
-def test_find():
-    i = os.images.find(name="CentOS 5.2")
-    assert_equal(i.id, 1)
-    os.assert_called('GET', '/images/detail')
-
-    iml = os.images.findall(status='SAVING')
-    assert_equal(len(iml), 1)
-    assert_equal(iml[0].name, 'My Server Backup')
+        iml = cs.images.findall(status='SAVING')
+        self.assertEqual(len(iml), 1)
+        self.assertEqual(iml[0].name, 'My Server Backup')
diff --git a/tests/v1_0/test_ipgroups.py b/tests/v1_0/test_ipgroups.py
index 3c89d4c68..3ab308b44 100644
--- a/tests/v1_0/test_ipgroups.py
+++ b/tests/v1_0/test_ipgroups.py
@@ -1,52 +1,48 @@
-from __future__ import absolute_import
-
-from nose.tools import assert_equal
 
 from novaclient.v1_0 import ipgroups
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
-
-os = FakeClient()
+from tests.v1_0 import fakes
+from tests import utils
 
 
-def test_list_ipgroups():
-    ipl = os.ipgroups.list()
-    os.assert_called('GET', '/shared_ip_groups/detail')
-    [assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl]
+cs = fakes.FakeClient()
 
 
-def test_list_ipgroups_undetailed():
-    ipl = os.ipgroups.list(detailed=False)
-    os.assert_called('GET', '/shared_ip_groups')
-    [assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl]
+class IPGroupTest(utils.TestCase):
 
+    def test_list_ipgroups(self):
+        ipl = cs.ipgroups.list()
+        cs.assert_called('GET', '/shared_ip_groups/detail')
+        [self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \
+            for ipg in ipl]
 
-def test_get_ipgroup():
-    ipg = os.ipgroups.get(1)
-    os.assert_called('GET', '/shared_ip_groups/1')
-    assert_isinstance(ipg, ipgroups.IPGroup)
+    def test_list_ipgroups_undetailed(self):
+        ipl = cs.ipgroups.list(detailed=False)
+        cs.assert_called('GET', '/shared_ip_groups')
+        [self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \
+            for ipg in ipl]
 
+    def test_get_ipgroup(self):
+        ipg = cs.ipgroups.get(1)
+        cs.assert_called('GET', '/shared_ip_groups/1')
+        self.assertTrue(isinstance(ipg, ipgroups.IPGroup))
 
-def test_create_ipgroup():
-    ipg = os.ipgroups.create("My group", 1234)
-    os.assert_called('POST', '/shared_ip_groups')
-    assert_isinstance(ipg, ipgroups.IPGroup)
+    def test_create_ipgroup(self):
+        ipg = cs.ipgroups.create("My group", 1234)
+        cs.assert_called('POST', '/shared_ip_groups')
+        self.assertTrue(isinstance(ipg, ipgroups.IPGroup))
 
+    def test_delete_ipgroup(self):
+        ipg = cs.ipgroups.get(1)
+        ipg.delete()
+        cs.assert_called('DELETE', '/shared_ip_groups/1')
+        cs.ipgroups.delete(ipg)
+        cs.assert_called('DELETE', '/shared_ip_groups/1')
+        cs.ipgroups.delete(1)
+        cs.assert_called('DELETE', '/shared_ip_groups/1')
 
-def test_delete_ipgroup():
-    ipg = os.ipgroups.get(1)
-    ipg.delete()
-    os.assert_called('DELETE', '/shared_ip_groups/1')
-    os.ipgroups.delete(ipg)
-    os.assert_called('DELETE', '/shared_ip_groups/1')
-    os.ipgroups.delete(1)
-    os.assert_called('DELETE', '/shared_ip_groups/1')
-
-
-def test_find():
-    ipg = os.ipgroups.find(name='group1')
-    os.assert_called('GET', '/shared_ip_groups/detail')
-    assert_equal(ipg.name, 'group1')
-    ipgl = os.ipgroups.findall(id=1)
-    assert_equal(ipgl, [ipgroups.IPGroup(None, {'id': 1})])
+    def test_find(self):
+        ipg = cs.ipgroups.find(name='group1')
+        cs.assert_called('GET', '/shared_ip_groups/detail')
+        self.assertEqual(ipg.name, 'group1')
+        ipgl = cs.ipgroups.findall(id=1)
+        self.assertEqual(ipgl, [ipgroups.IPGroup(None, {'id': 1})])
diff --git a/tests/v1_0/test_servers.py b/tests/v1_0/test_servers.py
index e32b3d61b..fcb3e9068 100644
--- a/tests/v1_0/test_servers.py
+++ b/tests/v1_0/test_servers.py
@@ -1,180 +1,169 @@
-from __future__ import absolute_import
 
 import StringIO
 
-from nose.tools import assert_equal
-
 from novaclient.v1_0 import servers
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
+from tests.v1_0 import fakes
+from tests import utils
 
 
-os = FakeClient()
+cs = fakes.FakeClient()
 
 
-def test_list_servers():
-    sl = os.servers.list()
-    os.assert_called('GET', '/servers/detail')
-    [assert_isinstance(s, servers.Server) for s in sl]
+class ServersTest(utils.TestCase):
 
+    def test_list_servers(self):
+        sl = cs.servers.list()
+        cs.assert_called('GET', '/servers/detail')
+        [self.assertTrue(isinstance(s, servers.Server)) for s in sl]
 
-def test_list_servers_undetailed():
-    sl = os.servers.list(detailed=False)
-    os.assert_called('GET', '/servers')
-    [assert_isinstance(s, servers.Server) for s in sl]
+    def test_list_servers_undetailed(self):
+        sl = cs.servers.list(detailed=False)
+        cs.assert_called('GET', '/servers')
+        [self.assertTrue(isinstance(s, servers.Server)) for s in sl]
 
+    def test_get_server_details(self):
+        s = cs.servers.get(1234)
+        cs.assert_called('GET', '/servers/1234')
+        self.assertTrue(isinstance(s, servers.Server))
+        self.assertEqual(s.id, 1234)
+        self.assertEqual(s.status, 'BUILD')
 
-def test_get_server_details():
-    s = os.servers.get(1234)
-    os.assert_called('GET', '/servers/1234')
-    assert_isinstance(s, servers.Server)
-    assert_equal(s.id, 1234)
-    assert_equal(s.status, 'BUILD')
+    def test_create_server(self):
+        s = cs.servers.create(
+            name="My server",
+            image=1,
+            flavor=1,
+            meta={'foo': 'bar'},
+            ipgroup=1,
+            files={
+                '/etc/passwd': 'some data',                 # a file
+                '/tmp/foo.txt': StringIO.StringIO('data')   # a stream
+            }
+        )
+        cs.assert_called('POST', '/servers')
+        self.assertTrue(isinstance(s, servers.Server))
 
+    def test_update_server(self):
+        s = cs.servers.get(1234)
 
-def test_create_server():
-    s = os.servers.create(
-        name="My server",
-        image=1,
-        flavor=1,
-        meta={'foo': 'bar'},
-        ipgroup=1,
-        files={
-            '/etc/passwd': 'some data',                 # a file
-            '/tmp/foo.txt': StringIO.StringIO('data')   # a stream
-        }
-    )
-    os.assert_called('POST', '/servers')
-    assert_isinstance(s, servers.Server)
+        # Update via instance
+        s.update(name='hi')
+        cs.assert_called('PUT', '/servers/1234')
+        s.update(name='hi', password='there')
+        cs.assert_called('PUT', '/servers/1234')
 
+        # Silly, but not an error
+        s.update()
 
-def test_update_server():
-    s = os.servers.get(1234)
+        # Update via manager
+        cs.servers.update(s, name='hi')
+        cs.assert_called('PUT', '/servers/1234')
+        cs.servers.update(1234, password='there')
+        cs.assert_called('PUT', '/servers/1234')
+        cs.servers.update(s, name='hi', password='there')
+        cs.assert_called('PUT', '/servers/1234')
 
-    # Update via instance
-    s.update(name='hi')
-    os.assert_called('PUT', '/servers/1234')
-    s.update(name='hi', password='there')
-    os.assert_called('PUT', '/servers/1234')
+    def test_delete_server(self):
+        s = cs.servers.get(1234)
+        s.delete()
+        cs.assert_called('DELETE', '/servers/1234')
+        cs.servers.delete(1234)
+        cs.assert_called('DELETE', '/servers/1234')
+        cs.servers.delete(s)
+        cs.assert_called('DELETE', '/servers/1234')
 
-    # Silly, but not an error
-    s.update()
+    def test_find(self):
+        s = cs.servers.find(name='sample-server')
+        cs.assert_called('GET', '/servers/detail')
+        self.assertEqual(s.name, 'sample-server')
 
-    # Update via manager
-    os.servers.update(s, name='hi')
-    os.assert_called('PUT', '/servers/1234')
-    os.servers.update(1234, password='there')
-    os.assert_called('PUT', '/servers/1234')
-    os.servers.update(s, name='hi', password='there')
-    os.assert_called('PUT', '/servers/1234')
+        # Find with multiple results arbitraility returns the first item
+        s = cs.servers.find(flavorId=1)
+        sl = cs.servers.findall(flavorId=1)
+        self.assertEqual(sl[0], s)
+        self.assertEqual([s.id for s in sl], [1234, 5678])
 
+    def test_share_ip(self):
+        s = cs.servers.get(1234)
 
-def test_delete_server():
-    s = os.servers.get(1234)
-    s.delete()
-    os.assert_called('DELETE', '/servers/1234')
-    os.servers.delete(1234)
-    os.assert_called('DELETE', '/servers/1234')
-    os.servers.delete(s)
-    os.assert_called('DELETE', '/servers/1234')
+        # Share via instance
+        s.share_ip(ipgroup=1, address='1.2.3.4')
+        cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
 
+        # Share via manager
+        cs.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False)
+        cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
 
-def test_find():
-    s = os.servers.find(name='sample-server')
-    os.assert_called('GET', '/servers/detail')
-    assert_equal(s.name, 'sample-server')
+    def test_unshare_ip(self):
+        s = cs.servers.get(1234)
 
-    # Find with multiple results arbitraility returns the first item
-    s = os.servers.find(flavorId=1)
-    sl = os.servers.findall(flavorId=1)
-    assert_equal(sl[0], s)
-    assert_equal([s.id for s in sl], [1234, 5678])
+        # Unshare via instance
+        s.unshare_ip('1.2.3.4')
+        cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
 
+        # Unshare via manager
+        cs.servers.unshare_ip(s, '1.2.3.4')
+        cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
 
-def test_share_ip():
-    s = os.servers.get(1234)
+    def test_reboot_server(self):
+        s = cs.servers.get(1234)
+        s.reboot()
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.reboot(s, type='HARD')
+        cs.assert_called('POST', '/servers/1234/action')
 
-    # Share via instance
-    s.share_ip(ipgroup=1, address='1.2.3.4')
-    os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
+    def test_rebuild_server(self):
+        s = cs.servers.get(1234)
+        s.rebuild(image=1)
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.rebuild(s, image=1)
+        cs.assert_called('POST', '/servers/1234/action')
 
-    # Share via manager
-    os.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False)
-    os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4')
+    def test_resize_server(self):
+        s = cs.servers.get(1234)
+        s.resize(flavor=1)
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.resize(s, flavor=1)
+        cs.assert_called('POST', '/servers/1234/action')
 
+    def test_confirm_resized_server(self):
+        s = cs.servers.get(1234)
+        s.confirm_resize()
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.confirm_resize(s)
+        cs.assert_called('POST', '/servers/1234/action')
 
-def test_unshare_ip():
-    s = os.servers.get(1234)
+    def test_revert_resized_server(self):
+        s = cs.servers.get(1234)
+        s.revert_resize()
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.revert_resize(s)
+        cs.assert_called('POST', '/servers/1234/action')
 
-    # Unshare via instance
-    s.unshare_ip('1.2.3.4')
-    os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
+    def test_backup_server(self):
+        s = cs.servers.get(1234)
+        s.backup("ImageName", "daily", 10)
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.backup(s, "ImageName", "daily", 10)
+        cs.assert_called('POST', '/servers/1234/action')
 
-    # Unshare via manager
-    os.servers.unshare_ip(s, '1.2.3.4')
-    os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
+    def test_migrate_server(self):
+        s = cs.servers.get(1234)
+        s.migrate()
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.migrate(s)
+        cs.assert_called('POST', '/servers/1234/action')
 
+    def test_add_fixed_ip(self):
+        s = cs.servers.get(1234)
+        s.add_fixed_ip(1)
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.add_fixed_ip(s, 1)
+        cs.assert_called('POST', '/servers/1234/action')
 
-def test_reboot_server():
-    s = os.servers.get(1234)
-    s.reboot()
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.reboot(s, type='HARD')
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_rebuild_server():
-    s = os.servers.get(1234)
-    s.rebuild(image=1)
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.rebuild(s, image=1)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_resize_server():
-    s = os.servers.get(1234)
-    s.resize(flavor=1)
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.resize(s, flavor=1)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_confirm_resized_server():
-    s = os.servers.get(1234)
-    s.confirm_resize()
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.confirm_resize(s)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_revert_resized_server():
-    s = os.servers.get(1234)
-    s.revert_resize()
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.revert_resize(s)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_migrate_server():
-    s = os.servers.get(1234)
-    s.migrate()
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.migrate(s)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_add_fixed_ip():
-    s = os.servers.get(1234)
-    s.add_fixed_ip(1)
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.add_fixed_ip(s, 1)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_remove_fixed_ip():
-    s = os.servers.get(1234)
-    s.remove_fixed_ip('10.0.0.1')
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.remove_fixed_ip(s, '10.0.0.1')
-    os.assert_called('POST', '/servers/1234/action')
+    def test_remove_fixed_ip(self):
+        s = cs.servers.get(1234)
+        s.remove_fixed_ip('10.0.0.1')
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.remove_fixed_ip(s, '10.0.0.1')
+        cs.assert_called('POST', '/servers/1234/action')
diff --git a/tests/v1_0/test_shell.py b/tests/v1_0/test_shell.py
index bc607292e..ebde8c8ac 100644
--- a/tests/v1_0/test_shell.py
+++ b/tests/v1_0/test_shell.py
@@ -1,374 +1,319 @@
-from __future__ import absolute_import
 
 import os
 import mock
-import httplib2
 
-from nose.tools import assert_raises, assert_equal
-
-from novaclient.v1_0.shell import OpenStackShell, CommandError
-
-from .fakes import FakeClient
-from .utils import assert_in
+from novaclient.shell import OpenStackComputeShell
+from novaclient import exceptions
+from tests.v1_0 import fakes
+from tests import utils
 
 
-# Patch os.environ to avoid required auth info.
-def setup():
-    global _old_env
-    fake_env = {
-        'NOVA_USERNAME': 'username',
-        'NOVA_API_KEY': 'password',
-        'NOVA_PROJECT_ID': 'project_id'
-    }
-    _old_env, os.environ = os.environ, fake_env.copy()
+class ShellTest(utils.TestCase):
 
-    # Make a fake shell object, a helping wrapper to call it, and a quick way
-    # of asserting that certain API calls were made.
-    global shell, _shell, assert_called, assert_called_anytime
-    _shell = OpenStackShell()
-    _shell._api_class = FakeClient
-    assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
-    assert_called_anytime = lambda m, u, b=None: \
-                                _shell.cs.assert_called_anytime(m, u, b)
-    shell = lambda cmd: _shell.main(cmd.split())
-
-
-def teardown():
-    global _old_env
-    os.environ = _old_env
-
-
-def test_backup_schedule():
-    shell('backup-schedule 1234')
-    assert_called('GET', '/servers/1234/backup_schedule')
-
-    shell('backup-schedule sample-server --weekly monday')
-    assert_called(
-        'POST', '/servers/1234/backup_schedule',
-        {'backupSchedule': {'enabled': True, 'daily': 'DISABLED',
-                            'weekly': 'MONDAY'}}
-    )
-
-    shell('backup-schedule sample-server '
-          '--weekly disabled --daily h_0000_0200')
-    assert_called(
-        'POST', '/servers/1234/backup_schedule',
-        {'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200',
-                            'weekly': 'DISABLED'}}
-    )
-
-    shell('backup-schedule sample-server --disable')
-    assert_called(
-        'POST', '/servers/1234/backup_schedule',
-        {'backupSchedule': {'enabled': False, 'daily': 'DISABLED',
-                            'weekly': 'DISABLED'}}
-    )
-
-
-def test_backup_schedule_delete():
-    shell('backup-schedule-delete 1234')
-    assert_called('DELETE', '/servers/1234/backup_schedule')
-
-
-def test_boot():
-    shell('boot --image 1 some-server')
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
-         'min_count': 1, 'max_count': 1}}
-    )
-
-    shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
-                    'min_count': 1, 'max_count': 1,
-                    'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
-    )
-
-
-def test_boot_files():
-    testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
-    expected_file_data = open(testfile).read().encode('base64')
-
-    shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
-                                                         (testfile, testfile))
-
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
-                    'min_count': 1, 'max_count': 1,
-                    'personality': [
-                        {'path': '/tmp/bar', 'contents': expected_file_data},
-                        {'path': '/tmp/foo', 'contents': expected_file_data}
-                    ]}
+    # Patch os.environ to avoid required auth info.
+    def setUp(self):
+        global _old_env
+        fake_env = {
+            'NOVA_USERNAME': 'username',
+            'NOVA_API_KEY': 'password',
+            'NOVA_PROJECT_ID': 'project_id'
         }
-    )
+        _old_env, os.environ = os.environ, fake_env.copy()
 
+        # Make a fake shell object, a helping wrapper to call it, and a quick way
+        # of asserting that certain API calls were made.
+        global shell, _shell, assert_called, assert_called_anytime
+        _shell = OpenStackComputeShell()
+        _shell._api_class = fakes.FakeClient
+        assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
+        assert_called_anytime = lambda m, u, b=None: \
+                                    _shell.cs.assert_called_anytime(m, u, b)
 
-def test_boot_invalid_file():
-    invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
-    assert_raises(CommandError, shell, 'boot some-server --image 1 '
-                                       '--file /foo=%s' % invalid_file)
+        def shell(cmd):
+            command = ['--version=1.0',]
+            command.extend(cmd.split())
+            _shell.main(command)
 
+    def tearDown(self):
+        global _old_env
+        os.environ = _old_env
 
-def test_boot_key_auto():
-    mock_exists = mock.Mock(return_value=True)
-    mock_open = mock.Mock()
-    mock_open.return_value = mock.Mock()
-    mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
+    def test_backup_schedule(self):
+        shell('backup-schedule 1234')
+        assert_called('GET', '/servers/1234/backup_schedule')
 
-    @mock.patch('os.path.exists', mock_exists)
-    @mock.patch('__builtin__.open', mock_open)
-    def test_shell_call():
-        shell('boot some-server --image 1 --key')
+        shell('backup-schedule sample-server --weekly monday')
+        assert_called(
+            'POST', '/servers/1234/backup_schedule',
+            {'backupSchedule': {'enabled': True, 'daily': 'DISABLED',
+                                'weekly': 'MONDAY'}}
+        )
+
+        shell('backup-schedule sample-server '
+              '--weekly disabled --daily h_0000_0200')
+        assert_called(
+            'POST', '/servers/1234/backup_schedule',
+            {'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200',
+                                'weekly': 'DISABLED'}}
+        )
+
+        shell('backup-schedule sample-server --disable')
+        assert_called(
+            'POST', '/servers/1234/backup_schedule',
+            {'backupSchedule': {'enabled': False, 'daily': 'DISABLED',
+                                'weekly': 'DISABLED'}}
+        )
+
+    def test_backup_schedule_delete(self):
+        shell('backup-schedule-delete 1234')
+        assert_called('DELETE', '/servers/1234/backup_schedule')
+
+    def test_boot(self):
+        shell('boot --image 1 some-server')
+        assert_called(
+            'POST', '/servers',
+            {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
+             'min_count': 1, 'max_count': 1}}
+        )
+
+        shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
         assert_called(
             'POST', '/servers',
             {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
                         'min_count': 1, 'max_count': 1,
-                        'personality': [{
-                            'path': '/root/.ssh/authorized_keys2',
-                            'contents': ('SSHKEY').encode('base64')},
+                        'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
+        )
+
+    def test_boot_files(self):
+        testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
+        expected_file_data = open(testfile).read().encode('base64')
+
+        shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
+                                                             (testfile, testfile))
+
+        assert_called(
+            'POST', '/servers',
+            {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
+                        'min_count': 1, 'max_count': 1,
+                        'personality': [
+                            {'path': '/tmp/bar', 'contents': expected_file_data},
+                            {'path': '/tmp/foo', 'contents': expected_file_data}
                         ]}
             }
         )
 
-    test_shell_call()
-
-
-def test_boot_key_auto_no_keys():
-    mock_exists = mock.Mock(return_value=False)
-
-    @mock.patch('os.path.exists', mock_exists)
-    def test_shell_call():
-        assert_raises(CommandError, shell, 'boot some-server --image 1 --key')
-
-    test_shell_call()
-
-
-def test_boot_key_file():
-    testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
-    expected_file_data = open(testfile).read().encode('base64')
-    shell('boot some-server --image 1 --key %s' % testfile)
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
-                    'min_count': 1, 'max_count': 1,
-                    'personality': [
-                        {'path': '/root/.ssh/authorized_keys2', 'contents':
-                         expected_file_data},
-                    ]}
-        }
-    )
-
-
-def test_boot_invalid_keyfile():
-    invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
-    assert_raises(CommandError, shell, 'boot some-server '
-                                       '--image 1 --key %s' % invalid_file)
-
-
-def test_boot_ipgroup():
-    shell('boot --image 1 --ipgroup 1 some-server')
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
-                    'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
-    )
-
-
-def test_boot_ipgroup_name():
-    shell('boot --image 1 --ipgroup group1 some-server')
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
-                    'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
-    )
-
-
-def test_flavor_list():
-    shell('flavor-list')
-    assert_called_anytime('GET', '/flavors/detail')
-
-
-def test_image_list():
-    shell('image-list')
-    assert_called('GET', '/images/detail')
-
-
-def test_snapshot_create():
-    shell('image-create sample-server mysnapshot')
-    assert_called(
-        'POST', '/images',
-        {'image': {'name': 'mysnapshot', 'serverId': 1234, 'image_type': 'snapshot', 'backup_type': None, 'rotation': None}}
-    )
-
-
-def test_backup_create():
-    shell('image-create sample-server mybackup --image-type backup --backup-type daily --rotation 1')
-    assert_called(
-        'POST', '/images',
-        {'image': {'name': 'mybackup', 'serverId': 1234, 'image_type': 'backup', 'backup_type': 'daily', 'rotation': 1}}
-    )
-
-
-def test_image_delete():
-    shell('image-delete 1')
-    assert_called('DELETE', '/images/1')
-
-
-def test_ip_share():
-    shell('ip-share sample-server 1 1.2.3.4')
-    assert_called(
-        'PUT', '/servers/1234/ips/public/1.2.3.4',
-        {'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}}
-    )
-
-
-def test_ip_unshare():
-    shell('ip-unshare sample-server 1.2.3.4')
-    assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
-
-
-def test_ipgroup_list():
-    shell('ipgroup-list')
-    assert_in(('GET', '/shared_ip_groups/detail', None),
-              _shell.cs.client.callstack)
-    assert_called('GET', '/servers/5678')
-
-
-def test_ipgroup_show():
-    shell('ipgroup-show 1')
-    assert_called('GET', '/shared_ip_groups/1')
-    shell('ipgroup-show group2')
-    # does a search, not a direct GET
-    assert_called('GET', '/shared_ip_groups/detail')
-
-
-def test_ipgroup_create():
-    shell('ipgroup-create a-group')
-    assert_called(
-        'POST', '/shared_ip_groups',
-        {'sharedIpGroup': {'name': 'a-group'}}
-    )
-    shell('ipgroup-create a-group sample-server')
-    assert_called(
-        'POST', '/shared_ip_groups',
-        {'sharedIpGroup': {'name': 'a-group', 'server': 1234}}
-    )
-
-
-def test_ipgroup_delete():
-    shell('ipgroup-delete group1')
-    assert_called('DELETE', '/shared_ip_groups/1')
-
-
-def test_list():
-    shell('list')
-    assert_called('GET', '/servers/detail')
-
-
-def test_reboot():
-    shell('reboot sample-server')
-    assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
-    shell('reboot sample-server --hard')
-    assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
-
-
-def test_rebuild():
-    shell('rebuild sample-server 1')
-    assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}})
-
-
-def test_rename():
-    shell('rename sample-server newname')
-    assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
-
-
-def test_resize():
-    shell('resize sample-server 1')
-    assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}})
-
-
-def test_resize_confirm():
-    shell('resize-confirm sample-server')
-    assert_called('POST', '/servers/1234/action', {'confirmResize': None})
-
-
-def test_resize_revert():
-    shell('resize-revert sample-server')
-    assert_called('POST', '/servers/1234/action', {'revertResize': None})
-
-
-@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
-def test_root_password():
-    shell('root-password sample-server')
-    assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}})
-
-
-def test_show():
-    shell('show 1234')
-    # XXX need a way to test multiple calls
-    # assert_called('GET', '/servers/1234')
-    assert_called('GET', '/images/2')
-
-
-def test_delete():
-    shell('delete 1234')
-    assert_called('DELETE', '/servers/1234')
-    shell('delete sample-server')
-    assert_called('DELETE', '/servers/1234')
-
-
-def test_zone():
-    shell('zone 1')
-    assert_called('GET', '/zones/1')
-
-    shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx')
-    assert_called(
-        'PUT', '/zones/1',
-        {'zone': {'api_url': 'http://zzz', 'username': 'frank',
-                  'password': 'xxx'}}
-    )
-
-def test_zone_add():
-    shell('zone-add http://zzz frank xxx 0.0 1.0')
-    assert_called(
-        'POST', '/zones',
-        {'zone': {'api_url': 'http://zzz', 'username': 'frank',
-                  'password': 'xxx',
-                  'weight_offset': '0.0', 'weight_scale': '1.0'}}
-    )
-
-def test_zone_delete():
-    shell('zone-delete 1')
-    assert_called('DELETE', '/zones/1')
-
-
-def test_zone_list():
-    shell('zone-list')
-    assert_in(('GET', '/zones/detail', None),
-              _shell.cs.client.callstack)
-
-
-def test_help():
-    @mock.patch.object(_shell.parser, 'print_help')
-    def test_help(m):
-        shell('help')
-        m.assert_called()
-
-    @mock.patch.object(_shell.subcommands['delete'], 'print_help')
-    def test_help_delete(m):
-        shell('help delete')
-        m.assert_called()
-
-    test_help()
-    test_help_delete()
-
-    assert_raises(CommandError, shell, 'help foofoo')
-
-
-def test_debug():
-    httplib2.debuglevel = 0
-    shell('--debug list')
-    assert httplib2.debuglevel == 1
+    def test_boot_invalid_file(self):
+        invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
+        self.assertRaises(exceptions.CommandError, shell, 'boot some-server --image 1 '
+                                               '--file /foo=%s' % invalid_file)
+
+    def test_boot_key_auto(self):
+        mock_exists = mock.Mock(return_value=True)
+        mock_open = mock.Mock()
+        mock_open.return_value = mock.Mock()
+        mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
+
+        @mock.patch('os.path.exists', mock_exists)
+        @mock.patch('__builtin__.open', mock_open)
+        def test_shell_call():
+            shell('boot some-server --image 1 --key')
+            assert_called(
+                'POST', '/servers',
+                {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
+                            'min_count': 1, 'max_count': 1,
+                            'personality': [{
+                                'path': '/root/.ssh/authorized_keys2',
+                                'contents': ('SSHKEY').encode('base64')},
+                            ]}
+                }
+            )
+
+        test_shell_call()
+
+    def test_boot_key_auto_no_keys(self):
+        mock_exists = mock.Mock(return_value=False)
+
+        @mock.patch('os.path.exists', mock_exists)
+        def test_shell_call():
+            self.assertRaises(exceptions.CommandError, shell,
+                              'boot some-server --image 1 --key')
+
+        test_shell_call()
+
+    def test_boot_key_file(self):
+        testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
+        expected_file_data = open(testfile).read().encode('base64')
+        shell('boot some-server --image 1 --key %s' % testfile)
+        assert_called(
+            'POST', '/servers',
+            {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
+                        'min_count': 1, 'max_count': 1,
+                        'personality': [
+                            {'path': '/root/.ssh/authorized_keys2', 'contents':
+                             expected_file_data},
+                        ]}
+            }
+        )
+
+    def test_boot_invalid_keyfile(self):
+        invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
+        self.assertRaises(exceptions.CommandError, shell, 'boot some-server '
+                                               '--image 1 --key %s' % invalid_file)
+
+    def test_boot_ipgroup(self):
+        shell('boot --image 1 --ipgroup 1 some-server')
+        assert_called(
+            'POST', '/servers',
+            {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
+                        'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
+        )
+
+    def test_boot_ipgroup_name(self):
+        shell('boot --image 1 --ipgroup group1 some-server')
+        assert_called(
+            'POST', '/servers',
+            {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
+                        'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}}
+        )
+
+    def test_flavor_list(self):
+        shell('flavor-list')
+        assert_called_anytime('GET', '/flavors/detail')
+
+    def test_image_list(self):
+        shell('image-list')
+        assert_called('GET', '/images/detail')
+
+    def test_snapshot_create(self):
+        shell('image-create sample-server mysnapshot')
+        assert_called(
+            'POST', '/images',
+            {'image': {'name': 'mysnapshot', 'serverId': 1234}}
+        )
+
+    def test_image_delete(self):
+        shell('image-delete 1')
+        assert_called('DELETE', '/images/1')
+
+    def test_ip_share(self):
+        shell('ip-share sample-server 1 1.2.3.4')
+        assert_called(
+            'PUT', '/servers/1234/ips/public/1.2.3.4',
+            {'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}}
+        )
+
+    def test_ip_unshare(self):
+        shell('ip-unshare sample-server 1.2.3.4')
+        assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
+
+    def test_ipgroup_list(self):
+        shell('ipgroup-list')
+        assert ('GET', '/shared_ip_groups/detail', None) in \
+                  _shell.cs.client.callstack
+        assert_called('GET', '/servers/5678')
+
+    def test_ipgroup_show(self):
+        shell('ipgroup-show 1')
+        assert_called('GET', '/shared_ip_groups/1')
+        shell('ipgroup-show group2')
+        # does a search, not a direct GET
+        assert_called('GET', '/shared_ip_groups/detail')
+
+    def test_ipgroup_create(self):
+        shell('ipgroup-create a-group')
+        assert_called(
+            'POST', '/shared_ip_groups',
+            {'sharedIpGroup': {'name': 'a-group'}}
+        )
+        shell('ipgroup-create a-group sample-server')
+        assert_called(
+            'POST', '/shared_ip_groups',
+            {'sharedIpGroup': {'name': 'a-group', 'server': 1234}}
+        )
+
+    def test_ipgroup_delete(self):
+        shell('ipgroup-delete group1')
+        assert_called('DELETE', '/shared_ip_groups/1')
+
+    def test_list(self):
+        shell('list')
+        assert_called('GET', '/servers/detail')
+
+    def test_reboot(self):
+        shell('reboot sample-server')
+        assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
+        shell('reboot sample-server --hard')
+        assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
+
+    def test_rebuild(self):
+        shell('rebuild sample-server 1')
+        assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}})
+
+    def test_rename(self):
+        shell('rename sample-server newname')
+        assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
+
+    def test_resize(self):
+        shell('resize sample-server 1')
+        assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}})
+
+    def test_resize_confirm(self):
+        shell('resize-confirm sample-server')
+        assert_called('POST', '/servers/1234/action', {'confirmResize': None})
+
+    def test_resize_revert(self):
+        shell('resize-revert sample-server')
+        assert_called('POST', '/servers/1234/action', {'revertResize': None})
+
+    def test_backup(self):
+        shell('backup sample-server mybackup daily 1')
+        assert_called(
+            'POST', '/servers/1234/action',
+            {'createBackup': {'name': 'mybackup', 'backup_type': 'daily',
+                              'rotation': 1}}
+        )
+
+    @mock.patch('getpass.getpass', mock.Mock(return_value='p'))
+    def test_root_password(self):
+        shell('root-password sample-server')
+        assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}})
+
+    def test_show(self):
+        shell('show 1234')
+        # XXX need a way to test multiple calls
+        # assert_called('GET', '/servers/1234')
+        assert_called('GET', '/images/2')
+
+    def test_delete(self):
+        shell('delete 1234')
+        assert_called('DELETE', '/servers/1234')
+        shell('delete sample-server')
+        assert_called('DELETE', '/servers/1234')
+
+    def test_zone(self):
+        shell('zone 1')
+        assert_called('GET', '/zones/1')
+
+        shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx')
+        assert_called(
+            'PUT', '/zones/1',
+            {'zone': {'api_url': 'http://zzz', 'username': 'frank',
+                      'password': 'xxx'}}
+        )
+
+    def test_zone_add(self):
+        shell('zone-add http://zzz frank xxx 0.0 1.0')
+        assert_called(
+            'POST', '/zones',
+            {'zone': {'api_url': 'http://zzz', 'username': 'frank',
+                      'password': 'xxx',
+                      'weight_offset': '0.0', 'weight_scale': '1.0'}}
+        )
+
+    def test_zone_delete(self):
+        shell('zone-delete 1')
+        assert_called('DELETE', '/zones/1')
+
+    def test_zone_list(self):
+        shell('zone-list')
+        assert ('GET', '/zones/detail', None) in _shell.cs.client.callstack
diff --git a/tests/v1_0/test_zones.py b/tests/v1_0/test_zones.py
index 2e6aa8487..c6b96a3f1 100644
--- a/tests/v1_0/test_zones.py
+++ b/tests/v1_0/test_zones.py
@@ -1,83 +1,76 @@
-from __future__ import absolute_import
 
 import StringIO
 
-from nose.tools import assert_equal
-
 from novaclient.v1_0 import zones
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
-
-os = FakeClient()
+from tests.v1_0 import fakes
+from tests import utils
 
 
-def test_list_zones():
-    sl = os.zones.list()
-    os.assert_called('GET', '/zones/detail')
-    [assert_isinstance(s, zones.Zone) for s in sl]
+os = fakes.FakeClient()
 
 
-def test_list_zones_undetailed():
-    sl = os.zones.list(detailed=False)
-    os.assert_called('GET', '/zones')
-    [assert_isinstance(s, zones.Zone) for s in sl]
+class ZonesTest(utils.TestCase):
 
+    def test_list_zones(self):
+        sl = os.zones.list()
+        os.assert_called('GET', '/zones/detail')
+        [self.assertTrue(isinstance(s, zones.Zone)) for s in sl]
 
-def test_get_zone_details():
-    s = os.zones.get(1)
-    os.assert_called('GET', '/zones/1')
-    assert_isinstance(s, zones.Zone)
-    assert_equal(s.id, 1)
-    assert_equal(s.api_url, 'http://foo.com')
+    def test_list_zones_undetailed(self):
+        sl = os.zones.list(detailed=False)
+        os.assert_called('GET', '/zones')
+        [self.assertTrue(isinstance(s, zones.Zone)) for s in sl]
 
+    def test_get_zone_details(self):
+        s = os.zones.get(1)
+        os.assert_called('GET', '/zones/1')
+        self.assertTrue(isinstance(s, zones.Zone))
+        self.assertEqual(s.id, 1)
+        self.assertEqual(s.api_url, 'http://foo.com')
 
-def test_create_zone():
-    s = os.zones.create(api_url="http://foo.com", username='bob',
-                        password='xxx')
-    os.assert_called('POST', '/zones')
-    assert_isinstance(s, zones.Zone)
+    def test_create_zone(self):
+        s = os.zones.create(api_url="http://foo.com", username='bob',
+                            password='xxx')
+        os.assert_called('POST', '/zones')
+        self.assertTrue(isinstance(s, zones.Zone))
 
+    def test_update_zone(self):
+        s = os.zones.get(1)
 
-def test_update_zone():
-    s = os.zones.get(1)
+        # Update via instance
+        s.update(api_url='http://blah.com')
+        os.assert_called('PUT', '/zones/1')
+        s.update(api_url='http://blah.com', username='alice', password='xxx')
+        os.assert_called('PUT', '/zones/1')
 
-    # Update via instance
-    s.update(api_url='http://blah.com')
-    os.assert_called('PUT', '/zones/1')
-    s.update(api_url='http://blah.com', username='alice', password='xxx')
-    os.assert_called('PUT', '/zones/1')
+        # Silly, but not an error
+        s.update()
 
-    # Silly, but not an error
-    s.update()
+        # Update via manager
+        os.zones.update(s, api_url='http://blah.com')
+        os.assert_called('PUT', '/zones/1')
+        os.zones.update(1, api_url='http://blah.com')
+        os.assert_called('PUT', '/zones/1')
+        os.zones.update(s, api_url='http://blah.com', username='fred',
+                           password='zip')
+        os.assert_called('PUT', '/zones/1')
 
-    # Update via manager
-    os.zones.update(s, api_url='http://blah.com')
-    os.assert_called('PUT', '/zones/1')
-    os.zones.update(1, api_url= 'http://blah.com')
-    os.assert_called('PUT', '/zones/1')
-    os.zones.update(s, api_url='http://blah.com', username='fred',
-                       password='zip')
-    os.assert_called('PUT', '/zones/1')
+    def test_delete_zone(self):
+        s = os.zones.get(1)
+        s.delete()
+        os.assert_called('DELETE', '/zones/1')
+        os.zones.delete(1)
+        os.assert_called('DELETE', '/zones/1')
+        os.zones.delete(s)
+        os.assert_called('DELETE', '/zones/1')
 
+    def test_find_zone(self):
+        s = os.zones.find(password='qwerty')
+        os.assert_called('GET', '/zones/detail')
+        self.assertEqual(s.username, 'bob')
 
-def test_delete_zone():
-    s = os.zones.get(1)
-    s.delete()
-    os.assert_called('DELETE', '/zones/1')
-    os.zones.delete(1)
-    os.assert_called('DELETE', '/zones/1')
-    os.zones.delete(s)
-    os.assert_called('DELETE', '/zones/1')
-
-
-def test_find_zone():
-    s = os.zones.find(password='qwerty')
-    os.assert_called('GET', '/zones/detail')
-    assert_equal(s.username, 'bob')
-
-    # Find with multiple results returns the first item
-    s = os.zones.find(api_url='http://foo.com')
-    sl = os.zones.findall(api_url='http://foo.com')
-    assert_equal(sl[0], s)
-    assert_equal([s.id for s in sl], [1, 2])
+        # Find with multiple results returns the first item
+        s = os.zones.find(api_url='http://foo.com')
+        sl = os.zones.findall(api_url='http://foo.com')
+        self.assertEqual(sl[0], s)
+        self.assertEqual([s.id for s in sl], [1, 2])
diff --git a/tests/v1_0/testfile.txt b/tests/v1_0/testfile.txt
index 90763c69f..e4e860f38 100644
--- a/tests/v1_0/testfile.txt
+++ b/tests/v1_0/testfile.txt
@@ -1 +1 @@
-OH HAI!
\ No newline at end of file
+BLAH
diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py
index fc7793bf4..c13fe695f 100644
--- a/tests/v1_1/fakes.py
+++ b/tests/v1_1/fakes.py
@@ -1,86 +1,23 @@
-"""
-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 fail. I've indicated in comments the places where actual
-behavior differs from the spec.
-"""
-
-from __future__ import absolute_import
-
-import urlparse
-import urllib
-
 import httplib2
+import urllib
+import urlparse
 
-from novaclient.v1_1 import Client
-from novaclient.v1_1.client import HTTPClient
-
-from .utils import fail, assert_in, assert_not_in, assert_has_keys
+from novaclient import client as base_client
+from novaclient.v1_1 import client
+from tests import fakes
 
 
-def assert_equal(value_one, value_two):
-    try:
-        assert value_one == value_two
-    except AssertionError:
-        print "%(value_one)s does not equal %(value_two)s" % locals()
-        raise
+class FakeClient(fakes.FakeClient, client.Client):
+
+    def __init__(self, *args, **kwargs):
+        client.Client.__init__(self, 'username', 'apikey',
+                               'project_id', 'auth_url')
+        self.client = FakeHTTPClient(**kwargs)
 
 
-class FakeClient(Client):
-    def __init__(self, username=None, password=None, project_id=None,
-                 auth_url=None):
-        super(FakeClient, self).__init__('username', 'apikey',
-                                         'project_id', 'auth_url')
-        self.client = FakeHTTPClient()
+class FakeHTTPClient(base_client.HTTPClient):
 
-    def assert_called(self, method, url, body=None):
-        """
-        Assert than an API method was just called.
-        """
-        expected = (method, url)
-        called = self.client.callstack[-1][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_equal(self.client.callstack[-1][2], body)
-
-        self.client.callstack = []
-
-    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:
-            called = entry[0:2]
-            if expected == entry[0:2]:
-                found = True
-                break
-
-        assert found, 'Expected %s; got %s' % \
-                              (expected, self.client.callstack)
-        if body is not None:
-            assert_equal(entry[2], body)
-
-        self.client.callstack = []
-
-    def authenticate(self):
-        pass
-
-
-class FakeHTTPClient(HTTPClient):
-    def __init__(self):
+    def __init__(self, **kwargs):
         self.username = 'username'
         self.apikey = 'apikey'
         self.auth_url = 'auth_url'
@@ -89,15 +26,15 @@ class FakeHTTPClient(HTTPClient):
     def _cs_request(self, url, method, **kwargs):
         # Check that certain things are called correctly
         if method in ['GET', 'DELETE']:
-            assert_not_in('body', kwargs)
+            assert 'body' not in kwargs
         elif method in ['PUT', 'POST']:
-            assert_in('body', kwargs)
+            assert 'body' in kwargs
 
         # Call the method
         munged_url = url.strip('/').replace('/', '_').replace('.', '_')
         callback = "%s_%s" % (method.lower(), munged_url)
         if not hasattr(self, callback):
-            fail('Called unknown API method: %s %s' % (method, url))
+            raise AssertionError('Called unknown API method: %s %s' % (method, url))
 
         # Note the call
         self.callstack.append((method, url, kwargs.get('body', None)))
@@ -184,25 +121,27 @@ class FakeHTTPClient(HTTPClient):
                 "name": "sample-server",
                 "image": {
                     "id": 2,
+                    "name": "sample image",
                 },
                 "flavor": {
                     "id": 1,
+                    "name": "256 MB Server",
                 },
                 "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
                 "status": "BUILD",
                 "progress": 60,
                 "addresses": {
                     "public": [{
-                        "addr": "1.2.3.4",
                         "version": 4,
+                        "addr": "1.2.3.4",
                     },
                     {
-                        "addr": "5.6.7.8",
                         "version": 4,
+                        "addr": "5.6.7.8",
                     }],
                     "private": [{
-                        "addr": "10.11.12.13",
                         "version": 4,
+                        "addr": "10.11.12.13",
                     }],
                 },
                 "metadata": {
@@ -215,24 +154,26 @@ class FakeHTTPClient(HTTPClient):
                 "name": "sample-server2",
                 "image": {
                     "id": 2,
+                    "name": "sample image",
                 },
                 "flavor": {
                     "id": 1,
+                    "name": "256 MB Server",
                 },
                 "hostId": "9e107d9d372bb6826bd81d3542a419d6",
                 "status": "ACTIVE",
                 "addresses": {
                     "public": [{
-                        "addr": "1.2.3.5",
                         "version": 4,
+                        "addr": "4.5.6.7",
                     },
                     {
-                        "addr": "5.6.7.9",
                         "version": 4,
+                        "addr": "5.6.9.8",
                     }],
                     "private": [{
-                        "addr": "10.13.12.13",
                         "version": 4,
+                        "addr": "10.13.12.13",
                     }],
                 },
                 "metadata": {
@@ -242,13 +183,13 @@ class FakeHTTPClient(HTTPClient):
         ]})
 
     def post_servers(self, body, **kw):
-        assert_equal(body.keys(), ['server'])
-        assert_has_keys(body['server'],
+        assert body.keys() == ['server']
+        fakes.assert_has_keys(body['server'],
                         required=['name', 'imageRef', 'flavorRef'],
                         optional=['metadata', 'personality'])
         if 'personality' in body['server']:
             for pfile in body['server']['personality']:
-                assert_has_keys(pfile, required=['path', 'contents'])
+                fakes.assert_has_keys(pfile, required=['path', 'contents'])
         return (202, self.get_servers_1234()[1])
 
     def get_servers_1234(self, **kw):
@@ -260,8 +201,8 @@ class FakeHTTPClient(HTTPClient):
         return (200, r)
 
     def put_servers_1234(self, body, **kw):
-        assert_equal(body.keys(), ['server'])
-        assert_has_keys(body['server'], optional=['name', 'adminPass'])
+        assert body.keys() == ['server']
+        fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
         return (204, None)
 
     def delete_servers_1234(self, **kw):
@@ -283,12 +224,6 @@ class FakeHTTPClient(HTTPClient):
         return (200, {'private':
                       self.get_servers_1234_ips()[1]['addresses']['private']})
 
-    def put_servers_1234_ips_public_1_2_3_4(self, body, **kw):
-        assert_equal(body.keys(), ['shareIp'])
-        assert_has_keys(body['shareIp'], required=['sharedIpGroupId',
-                                         'configureServer'])
-        return (202, None)
-
     def delete_servers_1234_ips_public_1_2_3_4(self, **kw):
         return (202, None)
 
@@ -297,27 +232,27 @@ class FakeHTTPClient(HTTPClient):
     #
 
     def post_servers_1234_action(self, body, **kw):
-        assert_equal(len(body.keys()), 1)
+        assert len(body.keys()) == 1
         action = body.keys()[0]
         if action == 'reboot':
-            assert_equal(body[action].keys(), ['type'])
-            assert_in(body[action]['type'], ['HARD', 'SOFT'])
+            assert body[action].keys() == ['type']
+            assert body[action]['type'] in ['HARD', 'SOFT']
         elif action == 'rebuild':
-            assert_equal(body[action].keys(), ['imageRef'])
+            assert body[action].keys() == ['imageRef']
         elif action == 'resize':
-            assert_equal(body[action].keys(), ['flavorRef'])
+            assert body[action].keys() == ['flavorRef']
         elif action == 'confirmResize':
-            assert_equal(body[action], None)
+            assert body[action] is None
             # This one method returns a different response code
             return (204, None)
         elif action == 'revertResize':
-            assert_equal(body[action], None)
-        elif action == 'changePassword':
-            assert_equal(body[action].keys(), ["adminPass"])
+            assert body[action] is None
         elif action == 'createImage':
-            assert_equal(body[action].keys(), ["name", "metadata"])
+            assert set(body[action].keys()) == set(['name', 'metadata'])
+        elif action == 'changePassword':
+            assert body[action].keys() == ['adminPass']
         else:
-            fail("Unexpected server action: %s" % action)
+            raise AssertionError("Unexpected server action: %s" % action)
         return (202, None)
 
     #
@@ -378,119 +313,9 @@ class FakeHTTPClient(HTTPClient):
         return (200, {'image': self.get_images_detail()[1]['images'][1]})
 
     def post_images(self, body, **kw):
-        assert_equal(body.keys(), ['image'])
-        assert_has_keys(body['image'], required=['serverId', 'name', 'image_type', 'backup_type', 'rotation'])
+        assert body.keys() == ['image']
+        fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
         return (202, self.get_images_1()[1])
 
     def delete_images_1(self, **kw):
         return (204, None)
-
-    #
-    # Backup schedules
-    #
-    def get_servers_1234_backup_schedule(self, **kw):
-        return (200, {"backupSchedule": {
-            "enabled": True,
-            "weekly": "THURSDAY",
-            "daily": "H_0400_0600"
-        }})
-
-    def post_servers_1234_backup_schedule(self, body, **kw):
-        assert_equal(body.keys(), ['backupSchedule'])
-        assert_has_keys(body['backupSchedule'], required=['enabled'],
-                                                optional=['weekly', 'daily'])
-        return (204, None)
-
-    def delete_servers_1234_backup_schedule(self, **kw):
-        return (204, None)
-
-    #
-    # Shared IP groups
-    #
-    def get_shared_ip_groups(self, **kw):
-        return (200, {'sharedIpGroups': [
-            {'id': 1, 'name': 'group1'},
-            {'id': 2, 'name': 'group2'},
-        ]})
-
-    def get_shared_ip_groups_detail(self, **kw):
-        return (200, {'sharedIpGroups': [
-            {'id': 1, 'name': 'group1', 'servers': [1234]},
-            {'id': 2, 'name': 'group2', 'servers': [5678]},
-        ]})
-
-    def get_shared_ip_groups_1(self, **kw):
-        return (200, {'sharedIpGroup':
-                   self.get_shared_ip_groups_detail()[1]['sharedIpGroups'][0]})
-
-    def post_shared_ip_groups(self, body, **kw):
-        assert_equal(body.keys(), ['sharedIpGroup'])
-        assert_has_keys(body['sharedIpGroup'], required=['name'],
-                                               optional=['server'])
-        return (201, {'sharedIpGroup': {
-            'id': 10101,
-            'name': body['sharedIpGroup']['name'],
-            'servers': 'server' in body['sharedIpGroup'] and \
-                       [body['sharedIpGroup']['server']] or None
-        }})
-
-    def delete_shared_ip_groups_1(self, **kw):
-        return (204, None)
-
-    #
-    # Zones
-    #
-    def get_zones(self, **kw):
-        return (200, {'zones': [
-            {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob'},
-            {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice'},
-        ]})
-
-
-    def get_zones_detail(self, **kw):
-        return (200, {'zones': [
-            {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob',
-                                                   'password': 'qwerty'},
-            {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice',
-                                                   'password': 'password'}
-        ]})
-
-    def get_zones_1(self, **kw):
-        r = {'zone': self.get_zones_detail()[1]['zones'][0]}
-        return (200, r)
-
-    def get_zones_2(self, **kw):
-        r = {'zone': self.get_zones_detail()[1]['zones'][1]}
-        return (200, r)
-
-    def post_zones(self, body, **kw):
-        assert_equal(body.keys(), ['zone'])
-        assert_has_keys(body['zone'],
-                        required=['api_url', 'username', 'password'],
-                        optional=['weight_offset', 'weight_scale'])
-
-        return (202, self.get_zones_1()[1])
-
-    def put_zones_1(self, body, **kw):
-        assert_equal(body.keys(), ['zone'])
-        assert_has_keys(body['zone'], optional=['api_url', 'username',
-                                                'password',
-                                                'weight_offset',
-                                                'weight_scale'])
-        return (204, None)
-
-    def delete_zones_1(self, **kw):
-        return (202, None)
-
-    #
-    # Accounts
-    #
-    def post_accounts_test_account_create_instance(self, body, **kw):
-        assert_equal(body.keys(), ['server'])
-        assert_has_keys(body['server'],
-                        required=['name', 'imageRef', 'flavorRef'],
-                        optional=['metadata', 'personality'])
-        if 'personality' in body['server']:
-            for pfile in body['server']['personality']:
-                assert_has_keys(pfile, required=['path', 'contents'])
-        return (202, self.get_servers_1234()[1])
diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py
new file mode 100644
index 000000000..5bb4e781f
--- /dev/null
+++ b/tests/v1_1/test_auth.py
@@ -0,0 +1,74 @@
+
+import httplib2
+import mock
+
+from novaclient.v1_1 import client
+from novaclient import exceptions
+from tests import utils
+
+
+class AuthenticationTests(utils.TestCase):
+
+    def test_authenticate_success(self):
+        cs = client.Client("username", "apikey", "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={
+                'X-Auth-User': 'username',
+                'X-Auth-Key': 'apikey',
+                '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", "apikey", "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", "apikey", "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", "apikey", "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_1/test_base.py b/tests/v1_1/test_base.py
deleted file mode 100644
index 9db08eb80..000000000
--- a/tests/v1_1/test_base.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from __future__ import absolute_import
-
-import mock
-from nose.tools import assert_equal, assert_not_equal, assert_raises
-
-from novaclient.v1_1 import flavors
-from novaclient.v1_1 import exceptions
-from novaclient.v1_1 import base
-
-from .fakes import FakeClient
-
-os = FakeClient()
-
-
-def test_resource_repr():
-    r = base.Resource(None, dict(foo="bar", baz="spam"))
-    assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
-
-
-def test_getid():
-    assert_equal(base.getid(4), 4)
-
-    class O(object):
-        id = 4
-    assert_equal(base.getid(O), 4)
-
-
-def test_resource_lazy_getattr():
-    f = flavors.Flavor(os.flavors, {'id': 1})
-    assert_equal(f.name, '256 MB Server')
-    os.assert_called('GET', '/flavors/1')
-
-    # Missing stuff still fails after a second get
-    assert_raises(AttributeError, getattr, f, 'blahblah')
-    os.assert_called('GET', '/flavors/1')
-
-
-def test_eq():
-    # 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'})
-    assert_equal(r1, r2)
-
-    # Two resoruces of different types: never equal
-    r1 = base.Resource(None, {'id': 1})
-    r2 = flavors.Flavor(None, {'id': 1})
-    assert_not_equal(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})
-    assert_equal(r1, r2)
-
-
-def test_findall_invalid_attribute():
-    # Make sure findall with an invalid attribute doesn't cause errors.
-    # The following should not raise an exception.
-    os.flavors.findall(vegetable='carrot')
-
-    # However, find() should raise an error
-    assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot')
diff --git a/tests/v1_1/test_client.py b/tests/v1_1/test_client.py
deleted file mode 100644
index 7cf90e368..000000000
--- a/tests/v1_1/test_client.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import mock
-import httplib2
-
-from novaclient.v1_1 import client
-from nose.tools import assert_equal
-
-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", "apikey", "project_id", "auth_test")
-    cl.management_url = "http://example.com"
-    cl.auth_token = "token"
-    return cl
-
-
-def test_get():
-    cl = get_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")
-        mock_request.assert_called_with("http://example.com/hi?fresh=1234",
-            "GET",
-            headers={"X-Auth-Token": "token",
-                     "X-Auth-Project-Id": "project_id",
-                     "User-Agent": cl.USER_AGENT})
-        # Automatic JSON parsing
-        assert_equal(body, {"hi": "there"})
-
-    test_get_call()
-
-
-def test_post():
-    cl = get_client()
-
-    @mock.patch.object(httplib2.Http, "request", mock_request)
-    def test_post_call():
-        cl.post("/hi", body=[1, 2, 3])
-        mock_request.assert_called_with("http://example.com/hi", "POST",
-            headers={
-                "X-Auth-Token": "token",
-                "X-Auth-Project-Id": "project_id",
-                "Content-Type": "application/json",
-                "User-Agent": cl.USER_AGENT},
-            body='[1, 2, 3]'
-        )
-
-    test_post_call()
diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py
index 51ca7a1bf..bc30cda1b 100644
--- a/tests/v1_1/test_flavors.py
+++ b/tests/v1_1/test_flavors.py
@@ -1,42 +1,38 @@
-from __future__ import absolute_import
-
-from nose.tools import assert_raises, assert_equal
 
+from novaclient import exceptions
 from novaclient.v1_1 import flavors
-from novaclient.v1_1 import exceptions
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
-
-os = FakeClient()
+from tests.v1_1 import fakes
+from tests import utils
 
 
-def test_list_flavors():
-    fl = os.flavors.list()
-    os.assert_called('GET', '/flavors/detail')
-    [assert_isinstance(f, flavors.Flavor) for f in fl]
+cs = fakes.FakeClient()
 
 
-def test_list_flavors_undetailed():
-    fl = os.flavors.list(detailed=False)
-    os.assert_called('GET', '/flavors')
-    [assert_isinstance(f, flavors.Flavor) for f in fl]
+class FlavorsTest(utils.TestCase):
 
+    def test_list_flavors(self):
+        fl = cs.flavors.list()
+        cs.assert_called('GET', '/flavors/detail')
+        [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
 
-def test_get_flavor_details():
-    f = os.flavors.get(1)
-    os.assert_called('GET', '/flavors/1')
-    assert_isinstance(f, flavors.Flavor)
-    assert_equal(f.ram, 256)
-    assert_equal(f.disk, 10)
+    def test_list_flavors_undetailed(self):
+        fl = cs.flavors.list(detailed=False)
+        cs.assert_called('GET', '/flavors')
+        [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl]
 
+    def test_get_flavor_details(self):
+        f = cs.flavors.get(1)
+        cs.assert_called('GET', '/flavors/1')
+        self.assertTrue(isinstance(f, flavors.Flavor))
+        self.assertEqual(f.ram, 256)
+        self.assertEqual(f.disk, 10)
 
-def test_find():
-    f = os.flavors.find(ram=256)
-    os.assert_called('GET', '/flavors/detail')
-    assert_equal(f.name, '256 MB Server')
+    def test_find(self):
+        f = cs.flavors.find(ram=256)
+        cs.assert_called('GET', '/flavors/detail')
+        self.assertEqual(f.name, '256 MB Server')
 
-    f = os.flavors.find(disk=20)
-    assert_equal(f.name, '512 MB Server')
+        f = cs.flavors.find(disk=20)
+        self.assertEqual(f.name, '512 MB Server')
 
-    assert_raises(exceptions.NotFound, os.flavors.find, disk=12345)
+        self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345)
diff --git a/tests/v1_1/test_images.py b/tests/v1_1/test_images.py
index 03b63b7cb..c2c78ed65 100644
--- a/tests/v1_1/test_images.py
+++ b/tests/v1_1/test_images.py
@@ -1,51 +1,40 @@
-from __future__ import absolute_import
-
-from nose.tools import assert_equal
 
 from novaclient.v1_1 import images
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
-
-os = FakeClient()
+from tests.v1_1 import fakes
+from tests import utils
 
 
-def test_list_images():
-    il = os.images.list()
-    os.assert_called('GET', '/images/detail')
-    [assert_isinstance(i, images.Image) for i in il]
+cs = fakes.FakeClient()
 
 
-def test_list_images_undetailed():
-    il = os.images.list(detailed=False)
-    os.assert_called('GET', '/images')
-    [assert_isinstance(i, images.Image) for i in il]
+class ImagesTest(utils.TestCase):
 
+    def test_list_images(self):
+        il = cs.images.list()
+        cs.assert_called('GET', '/images/detail')
+        [self.assertTrue(isinstance(i, images.Image)) for i in il]
 
-def test_get_image_details():
-    i = os.images.get(1)
-    os.assert_called('GET', '/images/1')
-    assert_isinstance(i, images.Image)
-    assert_equal(i.id, 1)
-    assert_equal(i.name, 'CentOS 5.2')
+    def test_list_images_undetailed(self):
+        il = cs.images.list(detailed=False)
+        cs.assert_called('GET', '/images')
+        [self.assertTrue(isinstance(i, images.Image)) for i in il]
 
+    def test_get_image_details(self):
+        i = cs.images.get(1)
+        cs.assert_called('GET', '/images/1')
+        self.assertTrue(isinstance(i, images.Image))
+        self.assertEqual(i.id, 1)
+        self.assertEqual(i.name, 'CentOS 5.2')
 
-def test_create_image():
-    i = os.images.create(server=1234, name="Just in case")
-    os.assert_called('POST', '/images')
-    assert_isinstance(i, images.Image)
+    def test_delete_image(self):
+        cs.images.delete(1)
+        cs.assert_called('DELETE', '/images/1')
 
+    def test_find(self):
+        i = cs.images.find(name="CentOS 5.2")
+        self.assertEqual(i.id, 1)
+        cs.assert_called('GET', '/images/detail')
 
-def test_delete_image():
-    os.images.delete(1)
-    os.assert_called('DELETE', '/images/1')
-
-
-def test_find():
-    i = os.images.find(name="CentOS 5.2")
-    assert_equal(i.id, 1)
-    os.assert_called('GET', '/images/detail')
-
-    iml = os.images.findall(status='SAVING')
-    assert_equal(len(iml), 1)
-    assert_equal(iml[0].name, 'My Server Backup')
+        iml = cs.images.findall(status='SAVING')
+        self.assertEqual(len(iml), 1)
+        self.assertEqual(iml[0].name, 'My Server Backup')
diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py
index 1ffb7b959..5a7ff73ac 100644
--- a/tests/v1_1/test_servers.py
+++ b/tests/v1_1/test_servers.py
@@ -1,125 +1,114 @@
-from __future__ import absolute_import
 
 import StringIO
 
-from nose.tools import assert_equal
-
 from novaclient.v1_1 import servers
-
-from .fakes import FakeClient
-from .utils import assert_isinstance
+from tests.v1_1 import fakes
+from tests import utils
 
 
-os = FakeClient()
+cs = fakes.FakeClient()
 
 
-def test_list_servers():
-    sl = os.servers.list()
-    os.assert_called('GET', '/servers/detail')
-    [assert_isinstance(s, servers.Server) for s in sl]
+class ServersTest(utils.TestCase):
 
+    def test_list_servers(self):
+        sl = cs.servers.list()
+        cs.assert_called('GET', '/servers/detail')
+        [self.assertTrue(isinstance(s, servers.Server)) for s in sl]
 
-def test_list_servers_undetailed():
-    sl = os.servers.list(detailed=False)
-    os.assert_called('GET', '/servers')
-    [assert_isinstance(s, servers.Server) for s in sl]
+    def test_list_servers_undetailed(self):
+        sl = cs.servers.list(detailed=False)
+        cs.assert_called('GET', '/servers')
+        [self.assertTrue(isinstance(s, servers.Server)) for s in sl]
 
+    def test_get_server_details(self):
+        s = cs.servers.get(1234)
+        cs.assert_called('GET', '/servers/1234')
+        self.assertTrue(isinstance(s, servers.Server))
+        self.assertEqual(s.id, 1234)
+        self.assertEqual(s.status, 'BUILD')
 
-def test_get_server_details():
-    s = os.servers.get(1234)
-    os.assert_called('GET', '/servers/1234')
-    assert_isinstance(s, servers.Server)
-    assert_equal(s.id, 1234)
-    assert_equal(s.status, 'BUILD')
+    def test_create_server(self):
+        s = cs.servers.create(
+            name="My server",
+            image=1,
+            flavor=1,
+            meta={'foo': 'bar'},
+            files={
+                '/etc/passwd': 'some data',                 # a file
+                '/tmp/foo.txt': StringIO.StringIO('data')   # a stream
+            }
+        )
+        cs.assert_called('POST', '/servers')
+        self.assertTrue(isinstance(s, servers.Server))
 
+    def test_update_server(self):
+        s = cs.servers.get(1234)
 
-def test_create_server():
-    s = os.servers.create(
-        name="My server",
-        image=1,
-        flavor=1,
-        meta={'foo': 'bar'},
-        files={
-            '/etc/passwd': 'some data',                 # a file
-            '/tmp/foo.txt': StringIO.StringIO('data')   # a stream
-        }
-    )
-    os.assert_called('POST', '/servers')
-    assert_isinstance(s, servers.Server)
+        # Update via instance
+        s.update(name='hi')
+        cs.assert_called('PUT', '/servers/1234')
+        s.update(name='hi')
+        cs.assert_called('PUT', '/servers/1234')
 
+        # Silly, but not an error
+        s.update()
 
-def test_update_server():
-    s = os.servers.get(1234)
+        # Update via manager
+        cs.servers.update(s, name='hi')
+        cs.assert_called('PUT', '/servers/1234')
 
-    # Update via instance
-    s.update(name='hi')
-    os.assert_called('PUT', '/servers/1234')
+    def test_delete_server(self):
+        s = cs.servers.get(1234)
+        s.delete()
+        cs.assert_called('DELETE', '/servers/1234')
+        cs.servers.delete(1234)
+        cs.assert_called('DELETE', '/servers/1234')
+        cs.servers.delete(s)
+        cs.assert_called('DELETE', '/servers/1234')
 
-    # Silly, but not an error
-    s.update()
+    def test_find(self):
+        s = cs.servers.find(name='sample-server')
+        cs.assert_called('GET', '/servers/detail')
+        self.assertEqual(s.name, 'sample-server')
 
-    # Update via manager
-    os.servers.update(s, name='hi')
-    os.assert_called('PUT', '/servers/1234')
+        # Find with multiple results arbitraility returns the first item
+        s = cs.servers.find(flavor={"id": 1, "name": "256 MB Server"})
+        sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"})
+        self.assertEqual(sl[0], s)
+        self.assertEqual([s.id for s in sl], [1234, 5678])
 
+    def test_reboot_server(self):
+        s = cs.servers.get(1234)
+        s.reboot()
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.reboot(s, type='HARD')
+        cs.assert_called('POST', '/servers/1234/action')
 
-def test_delete_server():
-    s = os.servers.get(1234)
-    s.delete()
-    os.assert_called('DELETE', '/servers/1234')
-    os.servers.delete(1234)
-    os.assert_called('DELETE', '/servers/1234')
-    os.servers.delete(s)
-    os.assert_called('DELETE', '/servers/1234')
+    def test_rebuild_server(self):
+        s = cs.servers.get(1234)
+        s.rebuild(image=1)
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.rebuild(s, image=1)
+        cs.assert_called('POST', '/servers/1234/action')
 
+    def test_resize_server(self):
+        s = cs.servers.get(1234)
+        s.resize(flavor=1)
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.resize(s, flavor=1)
+        cs.assert_called('POST', '/servers/1234/action')
 
-def test_find():
-    s = os.servers.find(name='sample-server')
-    os.assert_called('GET', '/servers/detail')
-    assert_equal(s.name, 'sample-server')
+    def test_confirm_resized_server(self):
+        s = cs.servers.get(1234)
+        s.confirm_resize()
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.confirm_resize(s)
+        cs.assert_called('POST', '/servers/1234/action')
 
-    # Find with multiple results arbitraility returns the first item
-    s = os.servers.find(flavor_id=1)
-    sl = os.servers.findall(flavor_id=1)
-    assert_equal(sl[0], s)
-    assert_equal([s.id for s in sl], [1234, 5678])
-
-
-def test_reboot_server():
-    s = os.servers.get(1234)
-    s.reboot()
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.reboot(s, type='HARD')
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_rebuild_server():
-    s = os.servers.get(1234)
-    s.rebuild(image=1)
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.rebuild(s, image=1)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_resize_server():
-    s = os.servers.get(1234)
-    s.resize(flavor=1)
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.resize(s, flavor=1)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_confirm_resized_server():
-    s = os.servers.get(1234)
-    s.confirm_resize()
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.confirm_resize(s)
-    os.assert_called('POST', '/servers/1234/action')
-
-
-def test_revert_resized_server():
-    s = os.servers.get(1234)
-    s.revert_resize()
-    os.assert_called('POST', '/servers/1234/action')
-    os.servers.revert_resize(s)
-    os.assert_called('POST', '/servers/1234/action')
+    def test_revert_resized_server(self):
+        s = cs.servers.get(1234)
+        s.revert_resize()
+        cs.assert_called('POST', '/servers/1234/action')
+        cs.servers.revert_resize(s)
+        cs.assert_called('POST', '/servers/1234/action')
diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py
index baf117b93..958f1020b 100644
--- a/tests/v1_1/test_shell.py
+++ b/tests/v1_1/test_shell.py
@@ -1,234 +1,210 @@
-from __future__ import absolute_import
 
 import os
 import mock
-import httplib2
 
-from nose.tools import assert_raises, assert_equal
-
-from novaclient.v1_1.shell import OpenStackShell, CommandError
-
-from .fakes import FakeClient
-from .utils import assert_in
+from novaclient.shell import OpenStackComputeShell
+from novaclient import exceptions
+from tests.v1_1 import fakes
+from tests import utils
 
 
-# Patch os.environ to avoid required auth info.
-def setup():
-    global _old_env
-    fake_env = {
-        'NOVA_USERNAME': 'username',
-        'NOVA_API_KEY': 'password',
-        'NOVA_PROJECT_ID': 'project_id'
-    }
-    _old_env, os.environ = os.environ, fake_env.copy()
+class ShellTest(utils.TestCase):
 
-    # Make a fake shell object, a helping wrapper to call it, and a quick way
-    # of asserting that certain API calls were made.
-    global shell, _shell, assert_called, assert_called_anytime
-    _shell = OpenStackShell()
-    _shell._api_class = FakeClient
-    assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
-    assert_called_anytime = lambda m, u, b=None: \
-                                _shell.cs.assert_called_anytime(m, u, b)
-    shell = lambda cmd: _shell.main(cmd.split())
-
-
-def teardown():
-    global _old_env
-    os.environ = _old_env
-
-
-def test_boot():
-    shell('boot --image 1 some-server')
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1'}}
-    )
-
-    shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
-                    'metadata': {'foo': 'bar', 'spam': 'eggs'}}}
-    )
-
-
-def test_boot_files():
-    testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
-    expected_file_data = open(testfile).read().encode('base64')
-
-    shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
-                                                         (testfile, testfile))
-
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
-                    'personality': [
-                        {'path': '/tmp/bar', 'contents': expected_file_data},
-                        {'path': '/tmp/foo', 'contents': expected_file_data}
-                    ]}
+    # Patch os.environ to avoid required auth info.
+    def setUp(self):
+        global _old_env
+        fake_env = {
+            'NOVA_USERNAME': 'username',
+            'NOVA_API_KEY': 'password',
+            'NOVA_PROJECT_ID': 'project_id'
         }
-    )
+        _old_env, os.environ = os.environ, fake_env.copy()
 
+        # Make a fake shell object, a helping wrapper to call it, and a quick way
+        # of asserting that certain API calls were made.
+        global shell, _shell, assert_called, assert_called_anytime
+        _shell = OpenStackComputeShell()
+        _shell._api_class = fakes.FakeClient
+        assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b)
+        assert_called_anytime = lambda m, u, b=None: \
+                                    _shell.cs.assert_called_anytime(m, u, b)
+        shell = lambda cmd: _shell.main(cmd.split())
 
-def test_boot_invalid_file():
-    invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
-    assert_raises(CommandError, shell, 'boot some-server --image 1 '
-                                       '--file /foo=%s' % invalid_file)
+    def tearDown(self):
+        global _old_env
+        os.environ = _old_env
 
-
-def test_boot_key_auto():
-    mock_exists = mock.Mock(return_value=True)
-    mock_open = mock.Mock()
-    mock_open.return_value = mock.Mock()
-    mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
-
-    @mock.patch('os.path.exists', mock_exists)
-    @mock.patch('__builtin__.open', mock_open)
-    def test_shell_call():
-        shell('boot some-server --image 1 --key')
+    def test_boot(self):
+        shell('boot --image 1 some-server')
         assert_called(
             'POST', '/servers',
-            {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
-                        'personality': [{
-                            'path': '/root/.ssh/authorized_keys2',
-                            'contents': ('SSHKEY').encode('base64')},
-                        ]}
+            {'server': {
+                'flavorRef': 1,
+                'name': 'some-server',
+                'imageRef': '1',
+                'personality': [],
+                'metadata': {},
+            }}
+        )
+
+        shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
+        assert_called(
+            'POST', '/servers',
+            {'server': {
+                'flavorRef': 1,
+                'name': 'some-server',
+                'imageRef': '1',
+                'metadata': {'foo': 'bar', 'spam': 'eggs'},
+                'personality': [],
+            }}
+        )
+
+    def test_boot_files(self):
+        testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
+        expected_file_data = open(testfile).read().encode('base64')
+
+        cmd = 'boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s'
+        shell(cmd % (testfile, testfile))
+
+        assert_called(
+            'POST', '/servers',
+            {'server': {
+                'flavorRef': 1,
+                'name': 'some-server',
+                'imageRef': '1',
+                'metadata': {},
+                'personality': [
+                   {'path': '/tmp/bar', 'contents': expected_file_data},
+                   {'path': '/tmp/foo', 'contents': expected_file_data}
+                ]}
             }
         )
 
-    test_shell_call()
+    def test_boot_invalid_file(self):
+        invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
+        cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file
+        self.assertRaises(exceptions.CommandError, shell, cmd)
 
+    def test_boot_key_auto(self):
+        mock_exists = mock.Mock(return_value=True)
+        mock_open = mock.Mock()
+        mock_open.return_value = mock.Mock()
+        mock_open.return_value.read = mock.Mock(return_value='SSHKEY')
 
-def test_boot_key_auto_no_keys():
-    mock_exists = mock.Mock(return_value=False)
-
-    @mock.patch('os.path.exists', mock_exists)
-    def test_shell_call():
-        assert_raises(CommandError, shell, 'boot some-server --image 1 --key')
-
-    test_shell_call()
-
-
-def test_boot_key_file():
-    testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
-    expected_file_data = open(testfile).read().encode('base64')
-    shell('boot some-server --image 1 --key %s' % testfile)
-    assert_called(
-        'POST', '/servers',
-        {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1',
-                    'personality': [
-                        {'path': '/root/.ssh/authorized_keys2', 'contents':
-                         expected_file_data},
+        @mock.patch('os.path.exists', mock_exists)
+        @mock.patch('__builtin__.open', mock_open)
+        def test_shell_call():
+            shell('boot some-server --image 1 --key')
+            assert_called(
+                'POST', '/servers',
+                {'server': {
+                    'flavorRef': 1,
+                    'name': 'some-server',
+                    'imageRef': '1',
+                    'metadata': {},
+                    'personality': [{
+                        'path': '/root/.ssh/authorized_keys2',
+                        'contents': ('SSHKEY').encode('base64')},
                     ]}
-        }
-    )
+                }
+            )
 
+        test_shell_call()
 
-def test_boot_invalid_keyfile():
-    invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
-    assert_raises(CommandError, shell, 'boot some-server '
-                                       '--image 1 --key %s' % invalid_file)
+    def test_boot_key_auto_no_keys(self):
+        mock_exists = mock.Mock(return_value=False)
 
+        @mock.patch('os.path.exists', mock_exists)
+        def test_shell_call():
+            self.assertRaises(exceptions.CommandError, shell,
+                              'boot some-server --image 1 --key')
 
-def test_flavor_list():
-    shell('flavor-list')
-    assert_called_anytime('GET', '/flavors/detail')
+        test_shell_call()
 
+    def test_boot_key_file(self):
+        testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
+        expected_file_data = open(testfile).read().encode('base64')
+        shell('boot some-server --image 1 --key %s' % testfile)
+        assert_called(
+            'POST', '/servers',
+            {'server': {
+                'flavorRef': 1,
+                 'name': 'some-server', 'imageRef': '1',
+                 'metadata': {},
+                 'personality': [
+                     {'path': '/root/.ssh/authorized_keys2',
+                      'contents':expected_file_data},
+                  ]}
+            }
+        )
 
-def test_image_list():
-    shell('image-list')
-    assert_called('GET', '/images/detail')
+    def test_boot_invalid_keyfile(self):
+        invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
+        self.assertRaises(exceptions.CommandError, shell, 'boot some-server '
+                                               '--image 1 --key %s' % invalid_file)
 
+    def test_flavor_list(self):
+        shell('flavor-list')
+        assert_called_anytime('GET', '/flavors/detail')
 
-def test_create_image():
-    shell('create-image sample-server mysnapshot')
-    assert_called(
-        'POST', '/servers/1234/action',
-        {'createImage': {'name': 'mysnapshot', "metadata": {}}}
-    )
+    def test_image_list(self):
+        shell('image-list')
+        assert_called('GET', '/images/detail')
 
+    def test_create_image(self):
+        shell('create-image sample-server mysnapshot')
+        assert_called(
+            'POST', '/servers/1234/action',
+            {'createImage': {'name': 'mysnapshot', 'metadata': {}}}
+        )
 
-def test_image_delete():
-    shell('image-delete 1')
-    assert_called('DELETE', '/images/1')
+    def test_image_delete(self):
+        shell('image-delete 1')
+        assert_called('DELETE', '/images/1')
 
+    def test_list(self):
+        shell('list')
+        assert_called('GET', '/servers/detail')
 
-def test_list():
-    shell('list')
-    assert_called('GET', '/servers/detail')
+    def test_reboot(self):
+        shell('reboot sample-server')
+        assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
+        shell('reboot sample-server --hard')
+        assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
 
+    def test_rebuild(self):
+        shell('rebuild sample-server 1')
+        assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}})
 
-def test_reboot():
-    shell('reboot sample-server')
-    assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
-    shell('reboot sample-server --hard')
-    assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
+    def test_rename(self):
+        shell('rename sample-server newname')
+        assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
 
+    def test_resize(self):
+        shell('resize sample-server 1')
+        assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}})
 
-def test_rebuild():
-    shell('rebuild sample-server 1')
-    assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}})
+    def test_resize_confirm(self):
+        shell('resize-confirm sample-server')
+        assert_called('POST', '/servers/1234/action', {'confirmResize': None})
 
+    def test_resize_revert(self):
+        shell('resize-revert sample-server')
+        assert_called('POST', '/servers/1234/action', {'revertResize': None})
 
-def test_rename():
-    shell('rename sample-server newname')
-    assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
+    @mock.patch('getpass.getpass', mock.Mock(return_value='p'))
+    def test_root_password(self):
+        shell('root-password sample-server')
+        assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}})
 
+    def test_show(self):
+        shell('show 1234')
+        # XXX need a way to test multiple calls
+        # assert_called('GET', '/servers/1234')
+        assert_called('GET', '/images/2')
 
-def test_resize():
-    shell('resize sample-server 1')
-    assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}})
-
-
-def test_resize_confirm():
-    shell('resize-confirm sample-server')
-    assert_called('POST', '/servers/1234/action', {'confirmResize': None})
-
-
-def test_resize_revert():
-    shell('resize-revert sample-server')
-    assert_called('POST', '/servers/1234/action', {'revertResize': None})
-
-
-@mock.patch('getpass.getpass', mock.Mock(return_value='p'))
-def test_root_password():
-    shell('root-password sample-server')
-    assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}})
-
-
-def test_show():
-    shell('show 1234')
-    # XXX need a way to test multiple calls
-    # assert_called('GET', '/servers/1234')
-    assert_called('GET', '/images/2')
-
-
-def test_delete():
-    shell('delete 1234')
-    assert_called('DELETE', '/servers/1234')
-    shell('delete sample-server')
-    assert_called('DELETE', '/servers/1234')
-
-
-def test_help():
-    @mock.patch.object(_shell.parser, 'print_help')
-    def test_help(m):
-        shell('help')
-        m.assert_called()
-
-    @mock.patch.object(_shell.subcommands['delete'], 'print_help')
-    def test_help_delete(m):
-        shell('help delete')
-        m.assert_called()
-
-    test_help()
-    test_help_delete()
-
-    assert_raises(CommandError, shell, 'help foofoo')
-
-
-def test_debug():
-    httplib2.debuglevel = 0
-    shell('--debug list')
-    assert httplib2.debuglevel == 1
+    def test_delete(self):
+        shell('delete 1234')
+        assert_called('DELETE', '/servers/1234')
+        shell('delete sample-server')
+        assert_called('DELETE', '/servers/1234')
diff --git a/tests/v1_1/testfile.txt b/tests/v1_1/testfile.txt
index 90763c69f..e4e860f38 100644
--- a/tests/v1_1/testfile.txt
+++ b/tests/v1_1/testfile.txt
@@ -1 +1 @@
-OH HAI!
\ No newline at end of file
+BLAH