diff --git a/.gitignore b/.gitignore
index b32d8aa7a..355cfbd22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
+.coverage
+*,cover
+cover
 *.pyc
-.idea
\ No newline at end of file
+.idea
diff --git a/novaclient/__init__.py b/novaclient/__init__.py
index a0807c710..e69de29bb 100644
--- a/novaclient/__init__.py
+++ b/novaclient/__init__.py
@@ -1,87 +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.
-
-"""
-novaclient module.
-"""
-
-__version__ = '2.5'
-
-from novaclient.accounts import Account, AccountManager
-from novaclient.backup_schedules import (
-        BackupSchedule, BackupScheduleManager,
-        BACKUP_WEEKLY_DISABLED, BACKUP_WEEKLY_SUNDAY, BACKUP_WEEKLY_MONDAY,
-        BACKUP_WEEKLY_TUESDAY, BACKUP_WEEKLY_WEDNESDAY,
-        BACKUP_WEEKLY_THURSDAY, BACKUP_WEEKLY_FRIDAY, BACKUP_WEEKLY_SATURDAY,
-        BACKUP_DAILY_DISABLED, BACKUP_DAILY_H_0000_0200,
-        BACKUP_DAILY_H_0200_0400, BACKUP_DAILY_H_0400_0600,
-        BACKUP_DAILY_H_0600_0800, BACKUP_DAILY_H_0800_1000,
-        BACKUP_DAILY_H_1000_1200, BACKUP_DAILY_H_1200_1400,
-        BACKUP_DAILY_H_1400_1600, BACKUP_DAILY_H_1600_1800,
-        BACKUP_DAILY_H_1800_2000, BACKUP_DAILY_H_2000_2200,
-        BACKUP_DAILY_H_2200_0000)
-from novaclient.client import OpenStackClient
-from novaclient.exceptions import (OpenStackException, BadRequest,
-        Unauthorized, Forbidden, NotFound, OverLimit)
-from novaclient.flavors import FlavorManager, Flavor
-from novaclient.images import ImageManager, Image
-from novaclient.ipgroups import IPGroupManager, IPGroup
-from novaclient.servers import (ServerManager, Server, REBOOT_HARD,
-                                 REBOOT_SOFT)
-from novaclient.zones import Zone, ZoneManager
-
-
-class OpenStack(object):
-    """
-    Top-level object to access the OpenStack Nova API.
-
-    Create an instance with your creds::
-
-        >>> os = OpenStack(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='https://auth.api.rackspacecloud.com/v1.0', timeout=None):
-        self.backup_schedules = BackupScheduleManager(self)
-        self.client = OpenStackClient(username, apikey, projectid, auth_url,
-                timeout=timeout)
-        self.flavors = FlavorManager(self)
-        self.images = ImageManager(self)
-        self.ipgroups = IPGroupManager(self)
-        self.servers = ServerManager(self)
-        self.zones = ZoneManager(self)
-        self.accounts = AccountManager(self)
-
-    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/base.py b/novaclient/base.py
index ee24ea76c..7928f8d5c 100644
--- a/novaclient/base.py
+++ b/novaclient/base.py
@@ -19,7 +19,8 @@
 Base utilities to build API operation managers and objects on top of.
 """
 
-from novaclient.exceptions import NotFound
+from novaclient import exceptions
+
 
 # Python 2.4 compat
 try:
@@ -101,8 +102,8 @@ class ManagerWithFind(Manager):
         try:
             return rl[0]
         except IndexError:
-            raise NotFound(404, "No %s matching %s." %
-                    (self.resource_class.__name__, kwargs))
+            msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+            raise exceptions.NotFound(404, msg)
 
     def findall(self, **kwargs):
         """
diff --git a/novaclient/client.py b/novaclient/client.py
index 2d2f4846b..fc1c98401 100644
--- a/novaclient/client.py
+++ b/novaclient/client.py
@@ -19,18 +19,19 @@ if not hasattr(urlparse, 'parse_qsl'):
     import cgi
     urlparse.parse_qsl = cgi.parse_qsl
 
-import novaclient
+
 from novaclient import exceptions
 
+
 _logger = logging.getLogger(__name__)
 
 
-class OpenStackClient(httplib2.Http):
+class HTTPClient(httplib2.Http):
 
-    USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
+    USER_AGENT = 'python-novaclient'
 
     def __init__(self, user, apikey, projectid, auth_url, timeout=None):
-        super(OpenStackClient, self).__init__(timeout=timeout)
+        super(HTTPClient, self).__init__(timeout=timeout)
         self.user = user
         self.apikey = apikey
         self.projectid = projectid
@@ -68,7 +69,7 @@ class OpenStackClient(httplib2.Http):
             kwargs['headers']['Content-Type'] = 'application/json'
             kwargs['body'] = json.dumps(kwargs['body'])
 
-        resp, body = super(OpenStackClient, self).request(*args, **kwargs)
+        resp, body = super(HTTPClient, self).request(*args, **kwargs)
 
         self.http_log(args, kwargs, resp, body)
 
@@ -144,7 +145,7 @@ class OpenStackClient(httplib2.Http):
         """
         Munge GET URLs to always return uncached content.
 
-        The OpenStack Nova API caches data *very* agressively and doesn't
+        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.
diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py
index 64efb4b75..ae456decd 100644
--- a/novaclient/exceptions.py
+++ b/novaclient/exceptions.py
@@ -4,7 +4,11 @@ Exception definitions.
 """
 
 
-class OpenStackException(Exception):
+class CommandError(Exception):
+    pass
+
+
+class ClientException(Exception):
     """
     The base exception class for all exceptions this library raises.
     """
@@ -17,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.
     """
@@ -25,7 +29,7 @@ class BadRequest(OpenStackException):
     message = "Bad request"
 
 
-class Unauthorized(OpenStackException):
+class Unauthorized(ClientException):
     """
     HTTP 401 - Unauthorized: bad credentials.
     """
@@ -33,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.
@@ -42,7 +46,7 @@ class Forbidden(OpenStackException):
     message = "Forbidden"
 
 
-class NotFound(OpenStackException):
+class NotFound(ClientException):
     """
     HTTP 404 - Not found
     """
@@ -50,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.
     """
@@ -59,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.
     """
@@ -70,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,
@@ -79,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::
@@ -88,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
index f9f5252e9..9642645d3 100644
--- a/novaclient/shell.py
+++ b/novaclient/shell.py
@@ -20,56 +20,25 @@ Command-line interface to the OpenStack Nova API.
 """
 
 import argparse
-import novaclient
-import getpass
 import httplib2
 import os
 import prettytable
 import sys
-import textwrap
-import uuid
 
-# Choices for flags.
-DAY_CHOICES = [getattr(novaclient, i).lower()
-               for i in dir(novaclient)
-               if i.startswith('BACKUP_WEEKLY_')]
-HOUR_CHOICES = [getattr(novaclient, i).lower()
-                for i in dir(novaclient)
-                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
-
-
-class CommandError(Exception):
-    pass
+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 OpenStackShell(object):
+class OpenStackComputeShell(object):
 
-    # Hook for the test suite to inject a fake server.
-    _api_class = novaclient.OpenStack
-
-    def __init__(self):
-        self.parser = argparse.ArgumentParser(
+    def get_base_parser(self):
+        parser = argparse.ArgumentParser(
             prog='nova',
             description=__doc__.strip(),
             epilog='See "nova help COMMAND" '\
@@ -79,44 +48,62 @@ class OpenStackShell(object):
         )
 
         # Global arguments
-        self.parser.add_argument('-h', '--help',
+        parser.add_argument('-h', '--help',
             action='help',
             help=argparse.SUPPRESS,
         )
 
-        self.parser.add_argument('--debug',
+        parser.add_argument('--debug',
             default=False,
             action='store_true',
             help=argparse.SUPPRESS)
 
-        self.parser.add_argument('--username',
+        parser.add_argument('--username',
             default=env('NOVA_USERNAME'),
             help='Defaults to env[NOVA_USERNAME].')
 
-        self.parser.add_argument('--apikey',
+        parser.add_argument('--apikey',
             default=env('NOVA_API_KEY'),
             help='Defaults to env[NOVA_API_KEY].')
 
-        self.parser.add_argument('--projectid',
+        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,
+        parser.add_argument('--url',
+            default=env('NOVA_URL'),
             help='Defaults to env[NOVA_URL].')
 
-        # Subcommands
-        subparsers = self.parser.add_subparsers(metavar='<subcommand>')
-        self.subcommands = {}
+        parser.add_argument('--version',
+            default=env('NOVA_VERSION'),
+            help='Accepts 1.0 or 1.1, defaults to env[NOVA_VERSION].')
 
-        # Everything that's do_* is a subcommand.
-        for attr in (a for a in dir(self) if a.startswith('do_')):
+        return parser
+
+    def get_subcommand_parser(self, version):
+        parser = self.get_base_parser()
+
+        self.subcommands = {}
+        subparsers = parser.add_subparsers(metavar='<subcommand>')
+
+        try:
+            actions_module = {
+                '1.0': shell_v1_0,
+                '1.1': shell_v1_1,
+            }[version]
+        except KeyError:
+            actions_module = shell_v1_0
+
+        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(self, attr)
+            callback = getattr(actions_module, attr)
             desc = callback.__doc__ or ''
             help = desc.strip().split('\n')[0]
             arguments = getattr(callback, 'arguments', [])
@@ -137,18 +124,26 @@ class OpenStackShell(object):
             subparser.set_defaults(func=callback)
 
     def main(self, argv):
-        # Parse args and call whatever callback was selected
-        args = self.parser.parse_args(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
 
-        # Deal with global arguments
-        if args.debug:
-            httplib2.debuglevel = 1
-
         user, apikey, projectid, url = args.username, args.apikey, \
                                        args.projectid, args.url
 
@@ -156,21 +151,31 @@ class OpenStackShell(object):
         # for username or apikey but for compatibility it is not.
 
         if not user:
-            raise CommandError("You must provide a username, either via "
+            raise exceptions.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 "
+            raise exceptions.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)
+        self.cs = self.get_api_class(options.version)(user, apikey, projectid, url)
+
         try:
             self.cs.authenticate()
-        except novaclient.Unauthorized:
-            raise CommandError("Invalid OpenStack Nova credentials.")
+        except exceptions.Unauthorized:
+            raise exceptions.CommandError("Invalid OpenStack Nova credentials.")
 
-        args.func(args)
+        args.func(self.cs, args)
 
-    @arg('command', metavar='<subcommand>', nargs='?',
+    def get_api_class(self, version):
+        try:
+            return {
+                "1.0": shell_v1_0.CLIENT_CLASS,
+                "1.1": shell_v1_1.CLIENT_CLASS,
+            }[version]
+        except KeyError:
+            return shell_v1_0.CLIENT_CLASS
+
+    @utils.arg('command', metavar='<subcommand>', nargs='?',
                     help='Display help for <subcommand>')
     def do_help(self, args):
         """
@@ -180,682 +185,11 @@ class OpenStackShell(object):
             if args.command in self.subcommands:
                 self.subcommands[args.command].print_help()
             else:
-                raise CommandError("'%s' is not a valid subcommand." %
+                raise exceptions.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(novaclient, 'BACKUP_DAILY_%s' %
-                                                    args.daily.upper())
-        if args.weekly:
-            backup['weekly'] = getattr(novaclient, 'BACKUP_WEEKLY_%s' %
-                                                     args.weekly.upper())
-        if args.enabled is not None:
-            backup['enabled'] = args.enabled
-        if backup:
-            server.backup_schedule.update(**backup)
-        else:
-            print_dict(server.backup_schedule._info)
-
-    @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()
-
-    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")
-
-        flavor = args.flavor or self.cs.flavors.find(ram=256)
-        image = args.image or self.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 = self._find_ipgroup(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 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, 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,
-                                        ipgroup=ipgroup,
-                                        meta=metadata,
-                                        files=files,
-                                        min_count=min_count,
-                                        max_count=max_count)
-        print_dict(server._info)
-
-    @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)
-
-        server = self.cs.accounts.create_instance_for(args.account, args.name,
-                    image, flavor,
-                    ipgroup=ipgroup,
-                    meta=metadata,
-                    files=files)
-        print_dict(server._info)
-
-    @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)
-
-        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
-
-    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])
-
-    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 snapshot.')
-    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)
-        print_dict(image._info)
-
-    @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('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)
-
-    @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)
-
-    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)
-
-        print_list(self.cs.ipgroups.list(),
-                   fields=['ID', 'Name', 'Server List'],
-                   formatters={'Server List': pretty_server_list})
-
-    @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)
-
-    @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)
-
-    @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()
-
-    @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)
-
-    @arg('--hard',
-        dest='reboot_type',
-        action='store_const',
-        const=novaclient.REBOOT_HARD,
-        default=novaclient.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.')
-    @arg('name', metavar='<name>', help='Name of snapshot.')
-    @arg('backup_type', metavar='<daily|weekly>', help='type of backup')
-    @arg('rotation', type=int, metavar='<rotation>',
-         help="Number of backups to retain. Used for backup image_type.")
-    def do_backup(self, args):
-        """Resize a server."""
-        server = self._find_server(args.server)
-        server.backup(args.name, args.backup_type, args.rotation)
-
-    @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.update(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 addrtype in addresses:
-            info['%s ip' % addrtype] = ', '.join(addresses[addrtype])
-
-        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
-
-        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()
-
-    # --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)
-
-        # 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)
-
-    def do_zone_info(self, args):
-        """Get this zones name and capabilities."""
-        zone = self.cs.zones.info()
-        print_dict(zone._info)
-
-    @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)
-
-    @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)
-
-    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'])
-
-    @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)
-
-    @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_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 novaclient.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 novaclient.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):
@@ -865,35 +199,9 @@ class OpenStackHelpFormatter(argparse.HelpFormatter):
         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():
     try:
-        OpenStackShell().main(sys.argv[1:])
+        OpenStackComputeShell().main(sys.argv[1:])
 
     except Exception, e:
         if httplib2.debuglevel == 1:
diff --git a/novaclient/utils.py b/novaclient/utils.py
new file mode 100644
index 000000000..08fb70caf
--- /dev/null
+++ b/novaclient/utils.py
@@ -0,0 +1,41 @@
+
+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
new file mode 100644
index 000000000..8fb731925
--- /dev/null
+++ b/novaclient/v1_0/__init__.py
@@ -0,0 +1 @@
+from novaclient.v1_0.client import Client
diff --git a/novaclient/accounts.py b/novaclient/v1_0/accounts.py
similarity index 83%
rename from novaclient/accounts.py
rename to novaclient/v1_0/accounts.py
index 28a9dedcd..966afa54b 100644
--- a/novaclient/accounts.py
+++ b/novaclient/v1_0/accounts.py
@@ -1,11 +1,14 @@
+
 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/backup_schedules.py b/novaclient/v1_0/backup_schedules.py
similarity index 99%
rename from novaclient/backup_schedules.py
rename to novaclient/v1_0/backup_schedules.py
index 662e31489..2d8aea824 100644
--- a/novaclient/backup_schedules.py
+++ b/novaclient/v1_0/backup_schedules.py
@@ -5,6 +5,7 @@ Backup Schedule interface.
 
 from novaclient import base
 
+
 BACKUP_WEEKLY_DISABLED = 'DISABLED'
 BACKUP_WEEKLY_SUNDAY = 'SUNDAY'
 BACKUP_WEEKLY_MONDAY = 'MONDAY'
diff --git a/novaclient/v1_0/base.py b/novaclient/v1_0/base.py
new file mode 100644
index 000000000..3ff7ac2d6
--- /dev/null
+++ b/novaclient/v1_0/base.py
@@ -0,0 +1,100 @@
+# 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 base
+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)
+
+
+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,
+              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": base.getid(image),
+            "flavorId": base.getid(flavor),
+        }}
+        if ipgroup:
+            body["server"]["sharedIpGroupId"] = base.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)
diff --git a/novaclient/v1_0/client.py b/novaclient/v1_0/client.py
new file mode 100644
index 000000000..f7e203fae
--- /dev/null
+++ b/novaclient/v1_0/client.py
@@ -0,0 +1,60 @@
+
+
+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
+
+
+
+class Client(object):
+    """
+    Top-level object to access the OpenStack Compute API.
+
+    Create an instance with your creds::
+
+        >>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
+
+    Then call methods on its managers::
+
+        >>> client.servers.list()
+        ...
+        >>> client.flavors.list()
+        ...
+
+    """
+
+    def __init__(self, username, api_key, project_id, auth_url=None,
+                 timeout=None):
+
+        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)
+
+        _auth_url = auth_url or 'https://auth.api.rackspacecloud.com/v1.0'
+
+        self.client = client.HTTPClient(username,
+                                        api_key,
+                                        project_id,
+                                        _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:`exceptions.Unauthorized` if the
+        credentials are wrong.
+        """
+        self.client.authenticate()
diff --git a/novaclient/flavors.py b/novaclient/v1_0/flavors.py
similarity index 99%
rename from novaclient/flavors.py
rename to novaclient/v1_0/flavors.py
index bfede1e13..f1b495804 100644
--- a/novaclient/flavors.py
+++ b/novaclient/v1_0/flavors.py
@@ -3,7 +3,6 @@
 Flavor interface.
 """
 
-
 from novaclient import base
 
 
diff --git a/novaclient/images.py b/novaclient/v1_0/images.py
similarity index 100%
rename from novaclient/images.py
rename to novaclient/v1_0/images.py
diff --git a/novaclient/ipgroups.py b/novaclient/v1_0/ipgroups.py
similarity index 100%
rename from novaclient/ipgroups.py
rename to novaclient/v1_0/ipgroups.py
diff --git a/novaclient/servers.py b/novaclient/v1_0/servers.py
similarity index 99%
rename from novaclient/servers.py
rename to novaclient/v1_0/servers.py
index c9166228d..83cbebab1 100644
--- a/novaclient/servers.py
+++ b/novaclient/v1_0/servers.py
@@ -20,7 +20,10 @@ Server interface.
 """
 
 import urllib
+
 from novaclient import base
+from novaclient.v1_0 import base as local_base
+
 
 REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
 
@@ -210,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):
diff --git a/novaclient/v1_0/shell.py b/novaclient/v1_0/shell.py
new file mode 100644
index 000000000..5731b2c88
--- /dev/null
+++ b/novaclient/v1_0/shell.py
@@ -0,0 +1,714 @@
+# Copyright 2010 Jacob Kaplan-Moss
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import getpass
+import os
+import uuid
+
+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 servers
+
+
+CLIENT_CLASS = client.Client
+
+# Choices for flags.
+DAY_CHOICES = [getattr(backup_schedules, i).lower()
+               for i in dir(backup_schedules)
+               if i.startswith('BACKUP_WEEKLY_')]
+HOUR_CHOICES = [getattr(backup_schedules, i).lower()
+                for i in dir(backup_schedules)
+                if i.startswith('BACKUP_DAILY_')]
+
+
+# Sentinal for boot --key
+AUTO_KEY = object()
+
+
+@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)
+
+    # 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' %
+                                                     args.weekly.upper())
+    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:
+            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
+
+    if keyfile:
+        try:
+            files['/root/.ssh/authorized_keys2'] = open(keyfile)
+        except IOError, e:
+            raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e))
+
+    return (args.name, image, flavor, ipgroup, metadata, files,
+            reservation_id, min_count, max_count)
+
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'nova flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'nova images'). "\
+          "Defaults to Ubuntu 10.04 LTS.")
+@utils.arg('--ipgroup',
+     default=None,
+     metavar='<group>',
+     help="IP group name or ID (see 'nova 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)
+
+    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)
+
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'nova flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'nova images'). "\
+          "Defaults to Ubuntu 10.04 LTS.")
+@utils.arg('--ipgroup',
+     default=None,
+     metavar='<group>',
+     help="IP group name or ID (see 'nova 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)
+
+    server = cs.accounts.create_instance_for(args.account, args.name,
+                image, flavor,
+                ipgroup=ipgroup,
+                meta=metadata,
+                files=files)
+    utils.print_dict(server._info)
+
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'nova flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'nova images'). "\
+          "Defaults to Ubuntu 10.04 LTS.")
+@utils.arg('--ipgroup',
+     default=None,
+     metavar='<group>',
+     help="IP group name or ID (see 'nova 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)
+
+    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 "Reservation ID=", reservation_id
+
+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])
+
+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'])
+
+@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)
+
+@utils.arg('image', metavar='<image>', help='Name or ID of image.')
+def do_image_delete(cs, args):
+    """
+    Delete an image.
+
+    It should go without saying, but you can only delete images you
+    created.
+    """
+    image = _find_image(cs, args.image)
+    image.delete()
+
+@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)
+
+@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)
+
+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)
+
+    utils.print_list(cs.ipgroups.list(),
+               fields=['ID', 'Name', 'Server List'],
+               formatters={'Server List': pretty_server_list})
+
+@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)
+
+@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)
+
+@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()
+
+@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)
+
+@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)
+
+@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.')
+@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)
+
+@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()
+
+@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()
+
+@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()
+
+@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()
+
+@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()
+
+@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()
+
+@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()
+
+@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])
+
+@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"])
+
+@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.update(password=p1)
+
+@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:
+        info['%s ip' % addrtype] = ', '.join(addresses[addrtype])
+
+    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
+
+    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()
+
+# --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)
+
+    # 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)
+
+def do_zone_info(cs, args):
+    """Get this zones name and capabilities."""
+    zone = cs.zones.info()
+    utils.print_dict(zone._info)
+
+@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)
+
+@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_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'])
+
+@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)
+
+@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 _find_server(cs, server):
+    """Get a server by name or ID."""
+    return _find_resource(cs.servers, server)
+
+def _find_ipgroup(cs, group):
+    """Get an IP group by name or ID."""
+    return _find_resource(cs.ipgroups, group)
+
+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:
+        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))
+
diff --git a/novaclient/zones.py b/novaclient/v1_0/zones.py
similarity index 98%
rename from novaclient/zones.py
rename to novaclient/v1_0/zones.py
index 28d0d6676..01d128f69 100644
--- a/novaclient/zones.py
+++ b/novaclient/v1_0/zones.py
@@ -18,6 +18,7 @@ Zone interface.
 """
 
 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
new file mode 100644
index 000000000..145edc77e
--- /dev/null
+++ b/novaclient/v1_1/__init__.py
@@ -0,0 +1 @@
+from novaclient.v1_1.client import Client
diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py
new file mode 100644
index 000000000..9762f73eb
--- /dev/null
+++ b/novaclient/v1_1/base.py
@@ -0,0 +1,231 @@
+# 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:
+            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):
+    """Like a `ManagerWithFind`, but has the ability to boot servers."""
+    def _boot(self, resource_url, response_key, name, image, flavor,
+              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 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,
+            "imageRef": getid(image),
+            "flavorRef": getid(flavor),
+        }}
+        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/v1_1/client.py b/novaclient/v1_1/client.py
new file mode 100644
index 000000000..fbbc62c0e
--- /dev/null
+++ b/novaclient/v1_1/client.py
@@ -0,0 +1,50 @@
+
+
+from novaclient import client
+from novaclient.v1_1 import flavors
+from novaclient.v1_1 import images
+from novaclient.v1_1 import servers
+from novaclient.v1_1 import zones
+
+
+
+class Client(object):
+    """
+    Top-level object to access the OpenStack Compute API.
+
+    Create an instance with your creds::
+
+        >>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
+
+    Then call methods on its managers::
+
+        >>> client.servers.list()
+        ...
+        >>> client.flavors.list()
+        ...
+
+    """
+
+    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)
+        self.zones = zones.ZoneManager(self)
+
+        self.client = client.HTTPClient(username,
+                                        api_key,
+                                        project_id,
+                                        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:`exceptions.Unauthorized` if the
+        credentials are wrong.
+        """
+        self.client.authenticate()
diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py
new file mode 100644
index 000000000..6eb1e2c4b
--- /dev/null
+++ b/novaclient/v1_1/flavors.py
@@ -0,0 +1,41 @@
+# Copyright 2010 Jacob Kaplan-Moss
+"""
+Flavor interface.
+"""
+
+from novaclient import base
+
+
+class Flavor(base.Resource):
+    """
+    A flavor is an available hardware configuration for a server.
+    """
+    def __repr__(self):
+        return "<Flavor: %s>" % self.name
+
+
+class FlavorManager(base.ManagerWithFind):
+    """
+    Manage :class:`Flavor` resources.
+    """
+    resource_class = Flavor
+
+    def list(self, detailed=True):
+        """
+        Get a list of all flavors.
+
+        :rtype: list of :class:`Flavor`.
+        """
+        if detailed is True:
+            return self._list("/flavors/detail", "flavors")
+        else:
+            return self._list("/flavors", "flavors")
+
+    def get(self, flavor):
+        """
+        Get a specific flavor.
+
+        :param flavor: The ID of the :class:`Flavor` to get.
+        :rtype: :class:`Flavor`
+        """
+        return self._get("/flavors/%s" % base.getid(flavor), "flavor")
diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py
new file mode 100644
index 000000000..e25c237e7
--- /dev/null
+++ b/novaclient/v1_1/images.py
@@ -0,0 +1,58 @@
+# Copyright 2010 Jacob Kaplan-Moss
+"""
+Image interface.
+"""
+
+from novaclient import base
+
+
+class Image(base.Resource):
+    """
+    An image is a collection of files used to create or rebuild a server.
+    """
+    def __repr__(self):
+        return "<Image: %s>" % self.name
+
+    def delete(self):
+        """
+        Delete this image.
+        """
+        return self.manager.delete(self)
+
+
+class ImageManager(base.ManagerWithFind):
+    """
+    Manage :class:`Image` resources.
+    """
+    resource_class = Image
+
+    def get(self, image):
+        """
+        Get an image.
+
+        :param image: The ID of the image to get.
+        :rtype: :class:`Image`
+        """
+        return self._get("/images/%s" % base.getid(image), "image")
+
+    def list(self, detailed=True):
+        """
+        Get a list of all images.
+
+        :rtype: list of :class:`Image`
+        """
+        if detailed is True:
+            return self._list("/images/detail", "images")
+        else:
+            return self._list("/images", "images")
+
+    def delete(self, image):
+        """
+        Delete an image.
+
+        It should go without saying that you can't delete an image
+        that you didn't create.
+
+        :param image: The :class:`Image` (or its ID) to delete.
+        """
+        self._delete("/images/%s" % base.getid(image))
diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py
new file mode 100644
index 000000000..5db925c88
--- /dev/null
+++ b/novaclient/v1_1/servers.py
@@ -0,0 +1,509 @@
+# 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.
+
+"""
+Server interface.
+"""
+
+import urllib
+
+from novaclient import base
+from novaclient.v1_1 import base as local_base
+
+
+REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
+
+
+class Server(base.Resource):
+    def __repr__(self):
+        return "<Server: %s>" % self.name
+
+    def delete(self):
+        """
+        Delete (i.e. shut down and delete the image) this server.
+        """
+        self.manager.delete(self)
+
+    def update(self, name=None):
+        """
+        Update the name or the password for this server.
+
+        :param name: Update the server's name.
+        :param password: Update the root password.
+        """
+        self.manager.update(self, name=name)
+
+    def add_fixed_ip(self, network_id):
+        """
+        Add an IP address on a network.
+
+        :param network_id: The ID of the network the IP should be on.
+        """
+        self.manager.add_fixed_ip(self, network_id)
+
+    def pause(self):
+        """
+        Pause -- Pause the running server.
+        """
+        self.manager.pause(self)
+
+    def unpause(self):
+        """
+        Unpause -- Unpause the paused server.
+        """
+        self.manager.unpause(self)
+
+    def suspend(self):
+        """
+        Suspend -- Suspend the running server.
+        """
+        self.manager.suspend(self)
+
+    def resume(self):
+        """
+        Resume -- Resume the suspended server.
+        """
+        self.manager.resume(self)
+
+    def rescue(self):
+        """
+        Rescue -- Rescue the problematic server.
+        """
+        self.manager.rescue(self)
+
+    def unrescue(self):
+        """
+        Unrescue -- Unrescue the rescued server.
+        """
+        self.manager.unrescue(self)
+
+    def diagnostics(self):
+        """Diagnostics -- Retrieve server diagnostics."""
+        self.manager.diagnostics(self)
+
+    def actions(self):
+        """Actions -- Retrieve server actions."""
+        self.manager.actions(self)
+
+    def migrate(self):
+        """
+        Migrate a server to a new host in the same zone.
+        """
+        self.manager.migrate(self)
+
+    def remove_fixed_ip(self, address):
+        """
+        Remove an IP address.
+
+        :param address: The IP address to remove.
+        """
+        self.manager.remove_fixed_ip(self, address)
+
+    def change_password(self, password):
+        """
+        Update the password for a server.
+        """
+        self.manager.change_password(self, password)
+
+    def reboot(self, type=REBOOT_SOFT):
+        """
+        Reboot the server.
+
+        :param type: either :data:`REBOOT_SOFT` for a software-level reboot,
+                     or `REBOOT_HARD` for a virtual power cycle hard reboot.
+        """
+        self.manager.reboot(self, type)
+
+    def rebuild(self, image):
+        """
+        Rebuild -- shut down and then re-image -- this server.
+
+        :param image: the :class:`Image` (or its ID) to re-image with.
+        """
+        self.manager.rebuild(self, image)
+
+    def resize(self, flavor):
+        """
+        Resize the server's resources.
+
+        :param flavor: the :class:`Flavor` (or its ID) to resize to.
+
+        Until a resize event is confirmed with :meth:`confirm_resize`, the old
+        server will be kept around and you'll be able to roll back to the old
+        flavor quickly with :meth:`revert_resize`. All resizes are
+        automatically confirmed after 24 hours.
+        """
+        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.
+        """
+        self.manager.confirm_resize(self)
+
+    def revert_resize(self):
+        """
+        Revert a previous resize, switching back to the old server.
+        """
+        self.manager.revert_resize(self)
+
+    @property
+    def networks(self):
+        """
+        Generate a simplified list of addresses
+        """
+        networks = {}
+        try:
+            for network_label, address_list in self.addresses.items():
+                networks[network_label] = [a['addr'] for a in address_list]
+            return networks
+        except Exception:
+            return {}
+
+
+class ServerManager(local_base.BootingManagerWithFind):
+    resource_class = Server
+
+    def get(self, server):
+        """
+        Get a server.
+
+        :param server: ID of the :class:`Server` to get.
+        :rtype: :class:`Server`
+        """
+        return self._get("/servers/%s" % base.getid(server), "server")
+
+    def list(self, detailed=True, search_opts=None):
+        """
+        Get a list of servers.
+        Optional detailed returns details server info.
+        Optional reservation_id only returns instances with that
+        reservation_id.
+
+        :rtype: list of :class:`Server`
+        """
+        if search_opts is None:
+            search_opts = {}
+
+        qparams = {}
+
+        for opt, val in search_opts.iteritems():
+            if val:
+                qparams[opt] = val
+
+        query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
+
+        detail = ""
+        if detailed:
+            detail = "/detail"
+        return self._list("/servers%s%s" % (detail, query_string), "servers")
+
+    def add_fixed_ip(self, server, network_id):
+        """
+        Add an IP address on a network.
+
+        :param server: The :class:`Server` (or its ID) to add an IP to.
+        :param network_id: The ID of the network the IP should be on.
+        """
+        self._action('addFixedIp', server, {'networkId': network_id})
+
+    def remove_fixed_ip(self, server, address):
+        """
+        Remove an IP address.
+
+        :param server: The :class:`Server` (or its ID) to add an IP to.
+        :param address: The IP address to remove.
+        """
+        self._action('removeFixedIp', server, {'address': address})
+
+    def pause(self, server):
+        """
+        Pause the server.
+        """
+        self.api.client.post('/servers/%s/pause' % base.getid(server), body={})
+
+    def unpause(self, server):
+        """
+        Unpause the server.
+        """
+        self.api.client.post('/servers/%s/unpause' % base.getid(server),
+                             body={})
+
+    def suspend(self, server):
+        """
+        Suspend the server.
+        """
+        self.api.client.post('/servers/%s/suspend' % base.getid(server),
+                             body={})
+
+    def resume(self, server):
+        """
+        Resume the server.
+        """
+        self.api.client.post('/servers/%s/resume' % base.getid(server),
+                             body={})
+
+    def rescue(self, server):
+        """
+        Rescue the server.
+        """
+        self.api.client.post('/servers/%s/rescue' % base.getid(server),
+                             body={})
+
+    def unrescue(self, server):
+        """
+        Unrescue the server.
+        """
+        self.api.client.post('/servers/%s/unrescue' % base.getid(server),
+                             body={})
+
+    def diagnostics(self, server):
+        """Retrieve server diagnostics."""
+        return self.api.client.get("/servers/%s/diagnostics" %
+                                   base.getid(server))
+
+    def actions(self, server):
+        """Retrieve server actions."""
+        return self._list("/servers/%s/actions" % base.getid(server),
+                          "actions")
+
+    def create(self, name, image, flavor, meta=None, files=None,
+               zone_blob=None, reservation_id=None, 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 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.
+        """
+        if not min_count:
+            min_count = 1
+        if not max_count:
+            max_count = min_count
+        if min_count > max_count:
+            min_count = max_count
+        return self._boot("/servers", "server", name, image, flavor,
+                          meta=meta, files=files,
+                          zone_blob=zone_blob, reservation_id=reservation_id,
+                          min_count=min_count, max_count=max_count)
+
+    def update(self, server, name=None):
+        """
+        Update the name or the password for a server.
+
+        :param server: The :class:`Server` (or its ID) to update.
+        :param name: Update the server's name.
+        """
+        if name is None:
+            return
+
+        body = {
+            "server": {
+                "name": name,
+            },
+        }
+
+        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.
+        """
+        self._delete("/servers/%s" % base.getid(server))
+
+    def reboot(self, server, type=REBOOT_SOFT):
+        """
+        Reboot a server.
+
+        :param server: The :class:`Server` (or its ID) to share onto.
+        :param type: either :data:`REBOOT_SOFT` for a software-level reboot,
+                     or `REBOOT_HARD` for a virtual power cycle hard reboot.
+        """
+        self._action('reboot', server, {'type': type})
+
+    def rebuild(self, server, image):
+        """
+        Rebuild -- shut down and then re-image -- a server.
+
+        :param server: The :class:`Server` (or its ID) to share onto.
+        :param image: the :class:`Image` (or its ID) to re-image with.
+        """
+        self._action('rebuild', server, {'imageRef': base.getid(image)})
+
+    def migrate(self, server):
+        """
+        Migrate a server to a new host in the same zone.
+
+        :param server: The :class:`Server` (or its ID).
+        """
+        self._action('migrate', server)
+
+    def resize(self, server, flavor):
+        """
+        Resize a server's resources.
+
+        :param server: The :class:`Server` (or its ID) to share onto.
+        :param flavor: the :class:`Flavor` (or its ID) to resize to.
+
+        Until a resize event is confirmed with :meth:`confirm_resize`, the old
+        server will be kept around and you'll be able to roll back to the old
+        flavor quickly with :meth:`revert_resize`. All resizes are
+        automatically confirmed after 24 hours.
+        """
+<<<<<<< HEAD:novaclient/servers.py
+        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.
+        """
+        self.api.client.post('/servers/%s/pause' % base.getid(server), body={})
+
+    def unpause(self, server):
+        """
+        Unpause the server.
+        """
+        self.api.client.post('/servers/%s/unpause' % base.getid(server),
+                             body={})
+
+    def suspend(self, server):
+        """
+        Suspend the server.
+        """
+        self.api.client.post('/servers/%s/suspend' % base.getid(server),
+                             body={})
+
+    def resume(self, server):
+        """
+        Resume the server.
+        """
+        self.api.client.post('/servers/%s/resume' % base.getid(server),
+                             body={})
+
+    def rescue(self, server):
+        """
+        Rescue the server.
+        """
+        self.api.client.post('/servers/%s/rescue' % base.getid(server),
+                             body={})
+
+    def unrescue(self, server):
+        """
+        Unrescue the server.
+        """
+        self.api.client.post('/servers/%s/unrescue' % base.getid(server),
+                             body={})
+
+    def diagnostics(self, server):
+        """Retrieve server diagnostics."""
+        return self.api.client.get("/servers/%s/diagnostics" %
+                                   base.getid(server))
+
+    def actions(self, server):
+        """Retrieve server actions."""
+        return self._list("/servers/%s/actions" % base.getid(server),
+                          "actions")
+=======
+        self._action('resize', server, {'flavorRef': base.getid(flavor)})
+>>>>>>> blamar/v1.1-split-and-support:novaclient/v1_1/servers.py
+
+    def confirm_resize(self, server):
+        """
+        Confirm that the resize worked, thus removing the original server.
+
+        :param server: The :class:`Server` (or its ID) to share onto.
+        """
+        self._action('confirmResize', server)
+
+    def revert_resize(self, server):
+        """
+        Revert a previous resize, switching back to the old server.
+
+        :param server: The :class:`Server` (or its ID) to share onto.
+        """
+        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.
+        """
+        self.api.client.post('/servers/%s/action' % base.getid(server),
+                             body={action: info})
diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py
new file mode 100644
index 000000000..dfa26a14e
--- /dev/null
+++ b/novaclient/v1_1/shell.py
@@ -0,0 +1,555 @@
+# Copyright 2010 Jacob Kaplan-Moss
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import getpass
+import os
+import uuid
+
+from novaclient import exceptions
+from novaclient import utils
+from novaclient.v1_1 import client
+from novaclient.v1_1 import servers
+
+
+CLIENT_CLASS = client.Client
+
+
+AUTO_KEY = object()
+
+
+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)")
+
+    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:
+            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
+
+    if keyfile:
+        try:
+            files['/root/.ssh/authorized_keys2'] = open(keyfile)
+        except IOError, e:
+            raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e))
+
+    return (args.name, image, flavor, metadata, files,
+            reservation_id, min_count, max_count)
+
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'nova flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'nova 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, reservation_id, \
+                min_count, max_count = _boot(cs, args)
+
+    server = cs.servers.create(args.name, image, flavor,
+                                    meta=metadata,
+                                    files=files,
+                                    min_count=min_count,
+                                    max_count=max_count)
+    utils.print_dict(server._info)
+
+
+@utils.arg('--flavor',
+     default=None,
+     metavar='<flavor>',
+     help="Flavor ID (see 'nova flavors'). "\
+          "Defaults to 256MB RAM instance.")
+@utils.arg('--image',
+     default=None,
+     metavar='<image>',
+     help="Image ID (see 'nova 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('--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, metadata, \
+            files, reservation_id, min_count, max_count = \
+                             _boot(cs, args,
+                                        reservation_id=reservation_id,
+                                        min_count=min_count,
+                                        max_count=max_count)
+
+    reservation_id = cs.zones.boot(args.name, image, flavor,
+                                        meta=metadata,
+                                        files=files,
+                                        reservation_id=reservation_id,
+                                        min_count=min_count,
+                                        max_count=max_count)
+    print "Reservation ID=", reservation_id
+
+
+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])
+
+
+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'])
+
+@utils.arg('image', metavar='<image>', help='Name or ID of image.')
+def do_image_delete(cs, args):
+    """
+    Delete an image.
+
+    It should go without saying, but you can only delete images you
+    created.
+    """
+    image = _find_image(cs, args.image)
+    image.delete()
+
+@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:
+        id_col = 'UUID'
+    else:
+        id_col = 'ID'
+
+    columns = [id_col, 'Name', 'Status', 'Networks']
+    formatters = {'Networks': _format_servers_list_networks}
+    utils.print_list(cs.servers.list(search_opts=search_opts), columns, formatters)
+
+
+def _format_servers_list_networks(server):
+    output = []
+    for (network, addresses) in server.networks.items():
+        if len(addresses) == 0:
+            continue
+        addresses_csv = ', '.join(addresses)
+        group = "%s=%s" % (network, addresses_csv)
+        output.append(group)
+
+    return '; '.join(output)
+
+
+@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)
+
+@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)
+
+@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_migrate(cs, args):
+    """Migrate a server."""
+    _find_server(cs, args.server).migrate()
+
+@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()
+
+@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()
+
+@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()
+
+@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()
+
+@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()
+
+@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()
+
+@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])
+
+@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"])
+
+
+
+@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_image_create(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)
+
+    networks = s.networks
+
+    info = s._info.copy()
+    for network_label, address_list in networks.items():
+        info['%s network' % network_label] = ', '.join(address_list)
+
+    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
+
+    info.pop('links', None)
+    info.pop('addresses', None)
+
+    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:
+        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))
+
+# --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)
+
+    # 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)
+
+def do_zone_info(cs, args):
+    """Get this zones name and capabilities."""
+    zone = cs.zones.info()
+    utils.print_dict(zone._info)
+
+@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)
+
+@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_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'])
+
+@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)
+
+@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)
diff --git a/novaclient/v1_1/zones.py b/novaclient/v1_1/zones.py
new file mode 100644
index 000000000..df65175d4
--- /dev/null
+++ b/novaclient/v1_1/zones.py
@@ -0,0 +1,195 @@
+# 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.
+
+"""
+Zone interface.
+"""
+
+from novaclient.v1_1 import base
+
+
+class Weighting(base.Resource):
+    def __init__(self, manager, info):
+        self.name = "n/a"
+        super(Weighting, self).__init__(manager, info)
+
+    def __repr__(self):
+        return "<Weighting: %s>" % self.name
+
+    def to_dict(self):
+        """Return the original info setting, which is a dict."""
+        return self._info
+
+
+class Zone(base.Resource):
+    def __init__(self, manager, info):
+        self.name = "n/a"
+        self.is_active = "n/a"
+        self.capabilities = "n/a"
+        super(Zone, self).__init__(manager, info)
+
+    def __repr__(self):
+        return "<Zone: %s>" % self.api_url
+
+    def delete(self):
+        """
+        Delete a child zone.
+        """
+        self.manager.delete(self)
+
+    def update(self, api_url=None, username=None, password=None,
+               weight_offset=None, weight_scale=None):
+        """
+        Update the name for this child zone.
+
+        :param api_url: Update the child zone's API URL.
+        :param username: Update the child zone's username.
+        :param password: Update the child zone's password.
+        :param weight_offset: Update the child zone's weight offset.
+        :param weight_scale: Update the child zone's weight scale.
+        """
+        self.manager.update(self, api_url, username, password,
+                            weight_offset, weight_scale)
+
+
+class ZoneManager(base.BootingManagerWithFind):
+    resource_class = Zone
+
+    def info(self):
+        """
+        Get info on this zone.
+
+        :rtype: :class:`Zone`
+        """
+        return self._get("/zones/info", "zone")
+
+    def get(self, zone):
+        """
+        Get a child zone.
+
+        :param server: ID of the :class:`Zone` to get.
+        :rtype: :class:`Zone`
+        """
+        return self._get("/zones/%s" % base.getid(zone), "zone")
+
+    def list(self, detailed=True):
+        """
+        Get a list of child zones.
+        :rtype: list of :class:`Zone`
+        """
+        detail = ""
+        if detailed:
+            detail = "/detail"
+        return self._list("/zones%s" % detail, "zones")
+
+    def create(self, api_url, username, password,
+               weight_offset=0.0, weight_scale=1.0):
+        """
+        Create a new child zone.
+
+        :param api_url: The child zone's API URL.
+        :param username: The child zone's username.
+        :param password: The child zone's password.
+        :param weight_offset: The child zone's weight offset.
+        :param weight_scale: The child zone's weight scale.
+        """
+        body = {"zone": {
+            "api_url": api_url,
+            "username": username,
+            "password": password,
+            "weight_offset": weight_offset,
+            "weight_scale": weight_scale
+        }}
+
+        return self._create("/zones", body, "zone")
+
+    def boot(self, name, image, flavor, meta=None, files=None,
+               zone_blob=None, reservation_id=None, min_count=None,
+               max_count=None):
+        """
+        Create (boot) a new server while being aware of Zones.
+
+        :param name: Something to name the server.
+        :param image: The :class:`Image` to boot with.
+        :param flavor: The :class:`Flavor` to boot onto.
+        :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 min_count: minimum number of servers to create.
+        :param max_count: maximum number of servers to create.
+        """
+        if not min_count:
+            min_count = 1
+        if not max_count:
+            max_count = min_count
+        return self._boot("/zones/boot", "reservation_id", name, image, flavor,
+                          meta=meta, files=files,
+                          zone_blob=zone_blob, reservation_id=reservation_id,
+                          return_raw=True, min_count=min_count,
+                          max_count=max_count)
+
+    def select(self, *args, **kwargs):
+        """
+        Given requirements for a new instance, select hosts
+        in this zone that best match those requirements.
+        """
+        # 'specs' may be passed in as None, so change to an empty string.
+        specs = kwargs.get("specs") or ""
+        url = "/zones/select"
+        weighting_list = self._list(url, "weights", Weighting, body=specs)
+        return [wt.to_dict() for wt in weighting_list]
+
+    def delete(self, zone):
+        """
+        Delete a child zone.
+        """
+        self._delete("/zones/%s" % base.getid(zone))
+
+    def update(self, zone, api_url=None, username=None, password=None,
+               weight_offset=None, weight_scale=None):
+        """
+        Update the name or the api_url for a zone.
+
+        :param zone: The :class:`Zone` (or its ID) to update.
+        :param api_url: Update the API URL.
+        :param username: Update the username.
+        :param password: Update the password.
+        :param weight_offset: Update the child zone's weight offset.
+        :param weight_scale: Update the child zone's weight scale.
+        """
+
+        body = {"zone": {}}
+        if api_url:
+            body["zone"]["api_url"] = api_url
+        if username:
+            body["zone"]["username"] = username
+        if password:
+            body["zone"]["password"] = password
+        if weight_offset:
+            body["zone"]["weight_offset"] = weight_offset
+        if weight_scale:
+            body["zone"]["weight_scale"] = weight_scale
+        if not len(body["zone"]):
+            return
+        self._update("/zones/%s" % base.getid(zone), body)
diff --git a/setup.py b/setup.py
index 6b74843aa..9d2d579f2 100644
--- a/setup.py
+++ b/setup.py
@@ -11,16 +11,16 @@ if sys.version_info < (2, 6):
     requirements.append('simplejson')
 
 setup(
-    name="python-novaclient",
-    version="2.5.9",
-    description="Client library for OpenStack Nova API",
-    long_description=read('README.rst'),
-    url='https://github.com/rackspace/python-novaclient',
-    license='Apache',
-    author='Rackspace, based on work by Jacob Kaplan-Moss',
-    author_email='github@racklabs.com',
-    packages=find_packages(exclude=['tests']),
-    classifiers=[
+    name = "python-novaclient",
+    version = "2.6.0",
+    description = "Client library for OpenStack Nova API",
+    long_description = read('README.rst'),
+    url = 'https://github.com/rackspace/python-novaclient',
+    license = 'Apache',
+    author = 'Rackspace, based on work by Jacob Kaplan-Moss',
+    author_email = 'github@racklabs.com',
+    packages = find_packages(exclude=['tests']),
+    classifiers = [
         'Development Status :: 5 - Production/Stable',
         'Environment :: Console',
         'Intended Audience :: Developers',
@@ -29,12 +29,12 @@ setup(
         'Operating System :: OS Independent',
         'Programming Language :: Python',
     ],
-    install_requires=requirements,
+    install_requires = requirements,
 
-    tests_require=["nose", "mock"],
-    test_suite="nose.collector",
+    tests_require = ["nose", "mock"],
+    test_suite = "nose.collector",
 
-    entry_points={
+    entry_points = {
         'console_scripts': ['nova = novaclient.shell:main']
     }
 )
diff --git a/tests/fakes.py b/tests/fakes.py
new file mode 100644
index 000000000..93709b8ce
--- /dev/null
+++ b/tests/fakes.py
@@ -0,0 +1,73 @@
+"""
+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:
+        try:
+            assert k in keys
+        except AssertionError:
+            allowed_keys = set(required) | set(optional)
+            extra_keys = set(keys).difference(set(required + optional))
+            raise AssertionError("found unexpected keys: %s" % list(extra_keys))
+
+
+class FakeClient(object):
+
+    def assert_called(self, method, url, body=None):
+        """
+        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:
+            try:
+                assert entry[2] == body
+            except AssertionError:
+                print entry[2]
+                print "!="
+                print body
+                raise
+
+        self.client.callstack = []
+
+    def authenticate(self):
+        pass
diff --git a/tests/test_accounts.py b/tests/test_accounts.py
deleted file mode 100644
index 8c488efe2..000000000
--- a/tests/test_accounts.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import StringIO
-
-from nose.tools import assert_equal
-
-from fakeserver import FakeServer
-from novaclient import Account
-
-cs = FakeServer()
-
-
-def test_instance_creation_for_account():
-    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/test_auth.py b/tests/test_auth.py
deleted file mode 100644
index 206fb851d..000000000
--- a/tests/test_auth.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import mock
-import novaclient
-import httplib2
-from nose.tools import assert_raises, assert_equal
-
-
-def test_authenticate_success():
-    cs = novaclient.OpenStack("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))
-
-    @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',
-            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'])
-
-    test_auth_call()
-
-
-def test_authenticate_failure():
-    cs = novaclient.OpenStack("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():
-        assert_raises(novaclient.Unauthorized, cs.client.authenticate)
-
-    test_auth_call()
-
-
-def test_auth_automatic():
-    client = novaclient.OpenStack("username", "apikey", "project_id").client
-    client.management_url = ''
-    mock_request = mock.Mock(return_value=(None, None))
-
-    @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()
-
-    test_auth_call()
-
-
-def test_auth_manual():
-    cs = novaclient.OpenStack("username", "apikey", "project_id")
-
-    @mock.patch.object(cs.client, 'authenticate')
-    def test_auth_call(m):
-        cs.authenticate()
-        m.assert_called()
-
-    test_auth_call()
diff --git a/tests/test_backup_schedules.py b/tests/test_backup_schedules.py
deleted file mode 100644
index 60b2ac5ea..000000000
--- a/tests/test_backup_schedules.py
+++ /dev/null
@@ -1,58 +0,0 @@
-
-from novaclient.backup_schedules import *
-from fakeserver import FakeServer
-from utils import assert_isinstance
-
-cs = FakeServer()
-
-
-def test_get_backup_schedule():
-    s = cs.servers.get(1234)
-
-    # access via manager
-    b = cs.backup_schedules.get(server=s)
-    assert_isinstance(b, BackupSchedule)
-    cs.assert_called('GET', '/servers/1234/backup_schedule')
-
-    b = cs.backup_schedules.get(server=1234)
-    assert_isinstance(b, BackupSchedule)
-    cs.assert_called('GET', '/servers/1234/backup_schedule')
-
-    # access via instance
-    assert_isinstance(s.backup_schedule, BackupSchedule)
-    cs.assert_called('GET', '/servers/1234/backup_schedule')
-
-    # Just for coverage's sake
-    b = s.backup_schedule.get()
-    cs.assert_called('GET', '/servers/1234/backup_schedule')
-
-
-def test_create_update_backup_schedule():
-    s = cs.servers.get(1234)
-
-    # create/update via manager
-    cs.backup_schedules.update(
-        server=s,
-        enabled=True,
-        weekly=BACKUP_WEEKLY_THURSDAY,
-        daily=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():
-    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/test_base.py b/tests/test_base.py
index 8477987f7..1dda3086f 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -1,59 +1,60 @@
 
 import mock
-import novaclient.base
-from novaclient import Flavor
-from novaclient.exceptions import NotFound
-from novaclient.base import Resource
-from nose.tools import assert_equal, assert_not_equal, assert_raises
-from fakeserver import FakeServer
 
-cs = FakeServer()
+from novaclient import base
+from novaclient import exceptions
+from novaclient.v1_0 import flavors
+from tests.v1_0 import fakes
+from tests import utils
 
 
-def test_resource_repr():
-    r = Resource(None, dict(foo="bar", baz="spam"))
-    assert_equal(repr(r), "<Resource baz=spam, foo=bar>")
+cs = fakes.FakeClient()
 
 
-def test_getid():
-    assert_equal(novaclient.base.getid(4), 4)
+class BaseTest(utils.TestCase):
 
-    class O(object):
-        id = 4
-    assert_equal(novaclient.base.getid(O), 4)
+    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)
 
-def test_resource_lazy_getattr():
-    f = Flavor(cs.flavors, {'id': 1})
-    assert_equal(f.name, '256 MB Server')
-    cs.assert_called('GET', '/flavors/1')
+        class TmpObject(object):
+            id = 4
+        self.assertEqual(base.getid(TmpObject), 4)
 
-    # Missing stuff still fails after a second get
-    assert_raises(AttributeError, getattr, f, 'blahblah')
-    cs.assert_called('GET', '/flavors/1')
+    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():
-    # Two resources of the same type with the same id: equal
-    r1 = Resource(None, {'id': 1, 'name': 'hi'})
-    r2 = Resource(None, {'id': 1, 'name': 'hello'})
-    assert_equal(r1, r2)
+    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 = Resource(None, {'id': 1})
-    r2 = Flavor(None, {'id': 1})
-    assert_not_equal(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 = Resource(None, {'name': 'joe', 'age': 12})
-    r2 = Resource(None, {'name': 'joe', 'age': 12})
-    assert_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})
+        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')
 
-def test_findall_invalid_attribute():
-    # 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
-    assert_raises(NotFound, cs.flavors.find, vegetable='carrot')
+        # However, find() should raise an error
+        self.assertRaises(exceptions.NotFound,
+                          cs.flavors.find,
+                          vegetable='carrot')
diff --git a/tests/test_client.py b/tests/test_client.py
deleted file mode 100644
index b22de0347..000000000
--- a/tests/test_client.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import mock
-import httplib2
-from novaclient.client import OpenStackClient
-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 client():
-    cl = OpenStackClient("username", "apikey", "project_id", "auth_test")
-    cl.management_url = "http://example.com"
-    cl.auth_token = "token"
-    return cl
-
-
-def test_get():
-    cl = 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 = 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/test_flavors.py b/tests/test_flavors.py
deleted file mode 100644
index cf4c6cfb8..000000000
--- a/tests/test_flavors.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from novaclient import Flavor, NotFound
-from fakeserver import FakeServer
-from utils import assert_isinstance
-from nose.tools import assert_raises, assert_equal
-
-cs = FakeServer()
-
-
-def test_list_flavors():
-    fl = cs.flavors.list()
-    cs.assert_called('GET', '/flavors/detail')
-    [assert_isinstance(f, Flavor) for f in fl]
-
-
-def test_list_flavors_undetailed():
-    fl = cs.flavors.list(detailed=False)
-    cs.assert_called('GET', '/flavors')
-    [assert_isinstance(f, Flavor) for f in fl]
-
-
-def test_get_flavor_details():
-    f = cs.flavors.get(1)
-    cs.assert_called('GET', '/flavors/1')
-    assert_isinstance(f, Flavor)
-    assert_equal(f.ram, 256)
-    assert_equal(f.disk, 10)
-
-
-def test_find():
-    f = cs.flavors.find(ram=256)
-    cs.assert_called('GET', '/flavors/detail')
-    assert_equal(f.name, '256 MB Server')
-
-    f = cs.flavors.find(disk=20)
-    assert_equal(f.name, '512 MB Server')
-
-    assert_raises(NotFound, cs.flavors.find, disk=12345)
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_images.py b/tests/test_images.py
deleted file mode 100644
index 1cc150a3f..000000000
--- a/tests/test_images.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from novaclient import Image
-from fakeserver import FakeServer
-from utils import assert_isinstance
-from nose.tools import assert_equal
-
-cs = FakeServer()
-
-
-def test_list_images():
-    il = cs.images.list()
-    cs.assert_called('GET', '/images/detail')
-    [assert_isinstance(i, Image) for i in il]
-
-
-def test_list_images_undetailed():
-    il = cs.images.list(detailed=False)
-    cs.assert_called('GET', '/images')
-    [assert_isinstance(i, Image) for i in il]
-
-
-def test_get_image_details():
-    i = cs.images.get(1)
-    cs.assert_called('GET', '/images/1')
-    assert_isinstance(i, Image)
-    assert_equal(i.id, 1)
-    assert_equal(i.name, 'CentOS 5.2')
-
-
-def test_create_image():
-    i = cs.images.create(server=1234, name="Just in case")
-    cs.assert_called('POST', '/images')
-    assert_isinstance(i, Image)
-
-
-def test_delete_image():
-    cs.images.delete(1)
-    cs.assert_called('DELETE', '/images/1')
-
-
-def test_find():
-    i = cs.images.find(name="CentOS 5.2")
-    assert_equal(i.id, 1)
-    cs.assert_called('GET', '/images/detail')
-
-    iml = cs.images.findall(status='SAVING')
-    assert_equal(len(iml), 1)
-    assert_equal(iml[0].name, 'My Server Backup')
diff --git a/tests/test_ipgroups.py b/tests/test_ipgroups.py
deleted file mode 100644
index 98a6f151d..000000000
--- a/tests/test_ipgroups.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from novaclient import IPGroup
-from fakeserver import FakeServer
-from utils import assert_isinstance
-from nose.tools import assert_equal
-
-cs = FakeServer()
-
-
-def test_list_ipgroups():
-    ipl = cs.ipgroups.list()
-    cs.assert_called('GET', '/shared_ip_groups/detail')
-    [assert_isinstance(ipg, IPGroup) for ipg in ipl]
-
-
-def test_list_ipgroups_undetailed():
-    ipl = cs.ipgroups.list(detailed=False)
-    cs.assert_called('GET', '/shared_ip_groups')
-    [assert_isinstance(ipg, IPGroup) for ipg in ipl]
-
-
-def test_get_ipgroup():
-    ipg = cs.ipgroups.get(1)
-    cs.assert_called('GET', '/shared_ip_groups/1')
-    assert_isinstance(ipg, IPGroup)
-
-
-def test_create_ipgroup():
-    ipg = cs.ipgroups.create("My group", 1234)
-    cs.assert_called('POST', '/shared_ip_groups')
-    assert_isinstance(ipg, IPGroup)
-
-
-def test_delete_ipgroup():
-    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_find():
-    ipg = cs.ipgroups.find(name='group1')
-    cs.assert_called('GET', '/shared_ip_groups/detail')
-    assert_equal(ipg.name, 'group1')
-    ipgl = cs.ipgroups.findall(id=1)
-    assert_equal(ipgl, [IPGroup(None, {'id': 1})])
diff --git a/tests/test_servers.py b/tests/test_servers.py
deleted file mode 100644
index 29c3069aa..000000000
--- a/tests/test_servers.py
+++ /dev/null
@@ -1,182 +0,0 @@
-import StringIO
-from nose.tools import assert_equal
-from fakeserver import FakeServer
-from utils import assert_isinstance
-from novaclient import Server
-
-cs = FakeServer()
-
-
-def test_list_servers():
-    sl = cs.servers.list()
-    cs.assert_called('GET', '/servers/detail')
-    [assert_isinstance(s, Server) for s in sl]
-
-
-def test_list_servers_undetailed():
-    sl = cs.servers.list(detailed=False)
-    cs.assert_called('GET', '/servers')
-    [assert_isinstance(s, Server) for s in sl]
-
-
-def test_get_server_details():
-    s = cs.servers.get(1234)
-    cs.assert_called('GET', '/servers/1234')
-    assert_isinstance(s, Server)
-    assert_equal(s.id, 1234)
-    assert_equal(s.status, 'BUILD')
-
-
-def test_create_server():
-    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')
-    assert_isinstance(s, Server)
-
-
-def test_update_server():
-    s = cs.servers.get(1234)
-
-    # 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()
-
-    # 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')
-
-
-def test_delete_server():
-    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')
-
-
-def test_find():
-    s = cs.servers.find(name='sample-server')
-    cs.assert_called('GET', '/servers/detail')
-    assert_equal(s.name, 'sample-server')
-
-    # Find with multiple results arbitraility returns the first item
-    s = cs.servers.find(flavorId=1)
-    sl = cs.servers.findall(flavorId=1)
-    assert_equal(sl[0], s)
-    assert_equal([s.id for s in sl], [1234, 5678])
-
-
-def test_share_ip():
-    s = cs.servers.get(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_unshare_ip():
-    s = cs.servers.get(1234)
-
-    # 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_reboot_server():
-    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_rebuild_server():
-    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():
-    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():
-    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_revert_resized_server():
-    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')
-
-
-def test_backup_server():
-    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')
-
-
-def test_migrate_server():
-    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():
-    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_remove_fixed_ip():
-    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/test_shell.py b/tests/test_shell.py
index 0d8b10751..ce7d76635 100644
--- a/tests/test_shell.py
+++ b/tests/test_shell.py
@@ -1,372 +1,39 @@
+
 import os
 import mock
 import httplib2
-from nose.tools import assert_raises, assert_equal
-from novaclient.shell import OpenStackShell, CommandError
-from fakeserver import FakeServer
-from utils import assert_in
+
+from novaclient.shell import OpenStackComputeShell
+from novaclient import exceptions
+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 = FakeServer
-    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 = 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_help_unknown_command(self):
+        self.assertRaises(exceptions.CommandError, shell, 'help foofoo')
 
-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')
-        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():
-    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}}
-    )
-
-
-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})
-
-
-def test_backup():
-    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():
-    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_debug(self):
+        httplib2.debuglevel = 0
+        shell('--debug help')
+        assert httplib2.debuglevel == 1
diff --git a/tests/test_zones.py b/tests/test_zones.py
deleted file mode 100644
index 11194189a..000000000
--- a/tests/test_zones.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import StringIO
-from nose.tools import assert_equal
-from fakeserver import FakeServer
-from utils import assert_isinstance
-from novaclient import Zone
-
-os = FakeServer()
-
-
-def test_list_zones():
-    sl = os.zones.list()
-    os.assert_called('GET', '/zones/detail')
-    [assert_isinstance(s, Zone) for s in sl]
-
-
-def test_list_zones_undetailed():
-    sl = os.zones.list(detailed=False)
-    os.assert_called('GET', '/zones')
-    [assert_isinstance(s, Zone) for s in sl]
-
-
-def test_get_zone_details():
-    s = os.zones.get(1)
-    os.assert_called('GET', '/zones/1')
-    assert_isinstance(s, Zone)
-    assert_equal(s.id, 1)
-    assert_equal(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, Zone)
-
-
-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')
-
-    # 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')
-
-
-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])
diff --git a/tests/testfile.txt b/tests/testfile.txt
deleted file mode 100644
index 90763c69f..000000000
--- a/tests/testfile.txt
+++ /dev/null
@@ -1 +0,0 @@
-OH HAI!
\ No newline at end of file
diff --git a/tests/utils.py b/tests/utils.py
index f878a5e26..4f1ca3f1e 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,29 +1,5 @@
-from nose.tools import ok_
+import unittest
 
 
-def fail(msg):
-    raise AssertionError(msg)
-
-
-def assert_in(thing, seq, msg=None):
-    msg = msg or "'%s' not found in %s" % (thing, seq)
-    ok_(thing in seq, msg)
-
-
-def assert_not_in(thing, seq, msg=None):
-    msg = msg or "unexpected '%s' found in %s" % (thing, seq)
-    ok_(thing not in seq, msg)
-
-
-def assert_has_keys(dict, required=[], optional=[]):
-    keys = dict.keys()
-    for k in required:
-        assert_in(k, keys, "required key %s missing from %s" % (k, dict))
-    allowed_keys = set(required) | set(optional)
-    extra_keys = set(keys).difference(set(required + optional))
-    if extra_keys:
-        fail("found unexpected keys: %s" % list(extra_keys))
-
-
-def assert_isinstance(thing, kls):
-    ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls))
+class TestCase(unittest.TestCase):
+    pass
diff --git a/tests/v1_0/__init__.py b/tests/v1_0/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/fakeserver.py b/tests/v1_0/fakes.py
similarity index 72%
rename from tests/fakeserver.py
rename to tests/v1_0/fakes.py
index a1aa22608..c47affc5e 100644
--- a/tests/fakeserver.py
+++ b/tests/v1_0/fakes.py
@@ -1,74 +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.
-"""
-
 import httplib2
-import urlparse
 import urllib
-from nose.tools import assert_equal
-from novaclient import OpenStack
-from novaclient.client import OpenStackClient
-from utils import fail, assert_in, assert_not_in, assert_has_keys
+import urlparse
+
+from novaclient import client as base_client
+from novaclient.v1_0 import client
+from tests import fakes
 
 
-class FakeServer(OpenStack):
-    def __init__(self, username=None, password=None, project_id=None,
-                 auth_url=None):
-        super(FakeServer, self).__init__('username', 'apikey',
-                                         'project_id', 'auth_url')
-        self.client = FakeClient()
+class FakeClient(fakes.FakeClient, client.Client):
 
-    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 %s; got %s' % \
-                              (expected, self.client.callstack)
-        if body is not None:
-            assert_equal(entry[2], body)
-
-        self.client.callstack = []
-
-    def authenticate(self):
-        pass
+    def __init__(self, *args, **kwargs):
+        client.Client.__init__(self, 'username', 'apikey',
+                               'project_id', 'auth_url')
+        self.client = FakeHTTPClient(**kwargs)
 
 
-class FakeClient(OpenStackClient):
-    def __init__(self):
+class FakeHTTPClient(base_client.HTTPClient):
+    def __init__(self, **kwargs):
         self.username = 'username'
         self.apikey = 'apikey'
         self.auth_url = 'auth_url'
@@ -77,15 +25,15 @@ class FakeClient(OpenStackClient):
     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)))
@@ -202,14 +150,14 @@ class FakeClient(OpenStackClient):
         ]})
 
     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):
@@ -221,8 +169,8 @@ class FakeClient(OpenStackClient):
         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):
@@ -245,8 +193,8 @@ class FakeClient(OpenStackClient):
                       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)
 
@@ -258,32 +206,32 @@ class FakeClient(OpenStackClient):
     #
 
     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_equal(set(body[action].keys()),
-                        set(['name', 'rotation', 'backup_type']))
+            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)
 
     #
@@ -344,8 +292,8 @@ class FakeClient(OpenStackClient):
         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'])
+        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):
@@ -362,8 +310,8 @@ class FakeClient(OpenStackClient):
         }})
 
     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)
 
@@ -390,8 +338,8 @@ class FakeClient(OpenStackClient):
                    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,
@@ -429,16 +377,16 @@ class FakeClient(OpenStackClient):
         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'])
@@ -451,12 +399,14 @@ class FakeClient(OpenStackClient):
     # 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
new file mode 100644
index 000000000..72d77b17e
--- /dev/null
+++ b/tests/v1_0/test_accounts.py
@@ -0,0 +1,25 @@
+
+import StringIO
+
+from tests.v1_0 import fakes
+from tests import utils
+
+
+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
new file mode 100644
index 000000000..f66336f2b
--- /dev/null
+++ b/tests/v1_0/test_auth.py
@@ -0,0 +1,74 @@
+
+import httplib2
+import mock
+
+from novaclient.v1_0 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")
+        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
+            }
+            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_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")
+        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")
+
+        @mock.patch.object(cs.client, 'authenticate')
+        def test_auth_call(m):
+            cs.authenticate()
+            m.assert_called()
+
+        test_auth_call()
diff --git a/tests/v1_0/test_backup_schedules.py b/tests/v1_0/test_backup_schedules.py
new file mode 100644
index 000000000..ebf5dad40
--- /dev/null
+++ b/tests/v1_0/test_backup_schedules.py
@@ -0,0 +1,60 @@
+
+from novaclient.v1_0 import backup_schedules
+from tests.v1_0 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+class BackupSchedulesTest(utils.TestCase):
+
+    def test_get_backup_schedule(self):
+        s = cs.servers.get(1234)
+
+        # 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')
+
+        # access via instance
+        self.assertTrue(isinstance(s.backup_schedule,
+                                   backup_schedules.BackupSchedule))
+        cs.assert_called('GET', '/servers/1234/backup_schedule')
+
+        # Just for coverage's sake
+        b = s.backup_schedule.get()
+        cs.assert_called('GET', '/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_flavors.py b/tests/v1_0/test_flavors.py
new file mode 100644
index 000000000..30cf84c47
--- /dev/null
+++ b/tests/v1_0/test_flavors.py
@@ -0,0 +1,38 @@
+
+from novaclient import exceptions
+from novaclient.v1_0 import flavors
+from tests.v1_0 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+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_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(self):
+        f = cs.flavors.find(ram=256)
+        cs.assert_called('GET', '/flavors/detail')
+        self.assertEqual(f.name, '256 MB Server')
+
+        f = cs.flavors.find(disk=20)
+        self.assertEqual(f.name, '512 MB Server')
+
+        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
new file mode 100644
index 000000000..41c1399f7
--- /dev/null
+++ b/tests/v1_0/test_images.py
@@ -0,0 +1,45 @@
+
+from novaclient.v1_0 import images
+from tests.v1_0 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+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_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(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_find(self):
+        i = cs.images.find(name="CentOS 5.2")
+        self.assertEqual(i.id, 1)
+        cs.assert_called('GET', '/images/detail')
+
+        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
new file mode 100644
index 000000000..3ab308b44
--- /dev/null
+++ b/tests/v1_0/test_ipgroups.py
@@ -0,0 +1,48 @@
+
+from novaclient.v1_0 import ipgroups
+from tests.v1_0 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+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_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(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_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
new file mode 100644
index 000000000..fcb3e9068
--- /dev/null
+++ b/tests/v1_0/test_servers.py
@@ -0,0 +1,169 @@
+
+import StringIO
+
+from novaclient.v1_0 import servers
+from tests.v1_0 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+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(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_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)
+
+        # 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()
+
+        # 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')
+
+    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')
+
+    def test_find(self):
+        s = cs.servers.find(name='sample-server')
+        cs.assert_called('GET', '/servers/detail')
+        self.assertEqual(s.name, 'sample-server')
+
+        # 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)
+
+        # 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_unshare_ip(self):
+        s = cs.servers.get(1234)
+
+        # 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_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_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_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_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')
+
+    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')
+
+    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_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
new file mode 100644
index 000000000..d922c5464
--- /dev/null
+++ b/tests/v1_0/test_shell.py
@@ -0,0 +1,316 @@
+
+import os
+import mock
+
+from novaclient.shell import OpenStackComputeShell
+from novaclient import exceptions
+from tests.v1_0 import fakes
+from tests import utils
+
+
+class ShellTest(utils.TestCase):
+
+    def setUp(self):
+        """Run before each test."""
+        self.old_environment = os.environ.copy()
+        os.environ = {
+            'NOVA_USERNAME': 'username',
+            'NOVA_API_KEY': 'password',
+            'NOVA_PROJECT_ID': 'project_id',
+            'NOVA_VERSION': '1.0',
+        }
+
+        self.shell = OpenStackComputeShell()
+        self.shell.get_api_class = lambda *_: fakes.FakeClient
+
+    def tearDown(self):
+        os.environ = self.old_environment
+
+    def run_command(self, cmd):
+        self.shell.main(cmd.split())
+
+    def assert_called(self, method, url, body=None):
+        return self.shell.cs.assert_called(method, url, body)
+
+    def assert_called_anytime(self, method, url, body=None):
+        return self.shell.cs.assert_called_anytime(method, url, body)
+
+    def test_backup_schedule(self):
+        self.run_command('backup-schedule 1234')
+        self.assert_called('GET', '/servers/1234/backup_schedule')
+
+        self.run_command('backup-schedule sample-server --weekly monday')
+        self.assert_called(
+            'POST', '/servers/1234/backup_schedule',
+            {'backupSchedule': {'enabled': True, 'daily': 'DISABLED',
+                                'weekly': 'MONDAY'}}
+        )
+
+        self.run_command('backup-schedule sample-server '
+              '--weekly disabled --daily h_0000_0200')
+        self.assert_called(
+            'POST', '/servers/1234/backup_schedule',
+            {'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200',
+                                'weekly': 'DISABLED'}}
+        )
+
+        self.run_command('backup-schedule sample-server --disable')
+        self.assert_called(
+            'POST', '/servers/1234/backup_schedule',
+            {'backupSchedule': {'enabled': False, 'daily': 'DISABLED',
+                                'weekly': 'DISABLED'}}
+        )
+
+    def test_backup_schedule_delete(self):
+        self.run_command('backup-schedule-delete 1234')
+        self.assert_called('DELETE', '/servers/1234/backup_schedule')
+
+    def test_boot(self):
+        self.run_command('boot --image 1 some-server')
+        self.assert_called(
+            'POST', '/servers',
+            {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1',
+             'min_count': 1, 'max_count': 1}}
+        )
+
+        self.run_command('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
+        self.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(self):
+        testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
+        expected_file_data = open(testfile).read().encode('base64')
+
+        self.run_command('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' %
+                                                             (testfile, testfile))
+
+        self.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}
+                        ]}
+            }
+        )
+
+    def test_boot_invalid_file(self):
+        invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf')
+        self.assertRaises(exceptions.CommandError, self.run_command, '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():
+            self.run_command('boot some-server --image 1 --key')
+            self.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, self.run_command,
+                              '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')
+        self.run_command('boot some-server --image 1 --key %s' % testfile)
+        self.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, self.run_command, 'boot some-server '
+                                               '--image 1 --key %s' % invalid_file)
+
+    def test_boot_ipgroup(self):
+        self.run_command('boot --image 1 --ipgroup 1 some-server')
+        self.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):
+        self.run_command('boot --image 1 --ipgroup group1 some-server')
+        self.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):
+        self.run_command('flavor-list')
+        self.assert_called_anytime('GET', '/flavors/detail')
+
+    def test_image_list(self):
+        self.run_command('image-list')
+        self.assert_called('GET', '/images/detail')
+
+    def test_snapshot_create(self):
+        self.run_command('image-create sample-server mysnapshot')
+        self.assert_called(
+            'POST', '/images',
+            {'image': {'name': 'mysnapshot', 'serverId': 1234}}
+        )
+
+    def test_image_delete(self):
+        self.run_command('image-delete 1')
+        self.assert_called('DELETE', '/images/1')
+
+    def test_ip_share(self):
+        self.run_command('ip-share sample-server 1 1.2.3.4')
+        self.assert_called(
+            'PUT', '/servers/1234/ips/public/1.2.3.4',
+            {'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}}
+        )
+
+    def test_ip_unshare(self):
+        self.run_command('ip-unshare sample-server 1.2.3.4')
+        self.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4')
+
+    def test_ipgroup_list(self):
+        self.run_command('ipgroup-list')
+        assert ('GET', '/shared_ip_groups/detail', None) in \
+                  self.shell.cs.client.callstack
+        self.assert_called('GET', '/servers/5678')
+
+    def test_ipgroup_show(self):
+        self.run_command('ipgroup-show 1')
+        self.assert_called('GET', '/shared_ip_groups/1')
+        self.run_command('ipgroup-show group2')
+        # does a search, not a direct GET
+        self.assert_called('GET', '/shared_ip_groups/detail')
+
+    def test_ipgroup_create(self):
+        self.run_command('ipgroup-create a-group')
+        self.assert_called(
+            'POST', '/shared_ip_groups',
+            {'sharedIpGroup': {'name': 'a-group'}}
+        )
+        self.run_command('ipgroup-create a-group sample-server')
+        self.assert_called(
+            'POST', '/shared_ip_groups',
+            {'sharedIpGroup': {'name': 'a-group', 'server': 1234}}
+        )
+
+    def test_ipgroup_delete(self):
+        self.run_command('ipgroup-delete group1')
+        self.assert_called('DELETE', '/shared_ip_groups/1')
+
+    def test_list(self):
+        self.run_command('list')
+        self.assert_called('GET', '/servers/detail')
+
+    def test_reboot(self):
+        self.run_command('reboot sample-server')
+        self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
+        self.run_command('reboot sample-server --hard')
+        self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
+
+    def test_rebuild(self):
+        self.run_command('rebuild sample-server 1')
+        self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}})
+
+    def test_rename(self):
+        self.run_command('rename sample-server newname')
+        self.assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
+
+    def test_resize(self):
+        self.run_command('resize sample-server 1')
+        self.assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}})
+
+    def test_resize_confirm(self):
+        self.run_command('resize-confirm sample-server')
+        self.assert_called('POST', '/servers/1234/action', {'confirmResize': None})
+
+    def test_resize_revert(self):
+        self.run_command('resize-revert sample-server')
+        self.assert_called('POST', '/servers/1234/action', {'revertResize': None})
+
+    def test_backup(self):
+        self.run_command('backup sample-server mybackup daily 1')
+        self.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):
+        self.run_command('root-password sample-server')
+        self.assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}})
+
+    def test_show(self):
+        self.run_command('show 1234')
+        # XXX need a way to test multiple calls
+        # self.assert_called('GET', '/servers/1234')
+        self.assert_called('GET', '/images/2')
+
+    def test_delete(self):
+        self.run_command('delete 1234')
+        self.assert_called('DELETE', '/servers/1234')
+        self.run_command('delete sample-server')
+        self.assert_called('DELETE', '/servers/1234')
+
+    def test_zone(self):
+        self.run_command('zone 1')
+        self.assert_called('GET', '/zones/1')
+
+        self.run_command('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx')
+        self.assert_called(
+            'PUT', '/zones/1',
+            {'zone': {'api_url': 'http://zzz', 'username': 'frank',
+                      'password': 'xxx'}}
+        )
+
+    def test_zone_add(self):
+        self.run_command('zone-add http://zzz frank xxx 0.0 1.0')
+        self.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):
+        self.run_command('zone-delete 1')
+        self.assert_called('DELETE', '/zones/1')
+
+    def test_zone_list(self):
+        self.run_command('zone-list')
+        assert ('GET', '/zones/detail', None) in self.shell.cs.client.callstack
diff --git a/tests/v1_0/test_zones.py b/tests/v1_0/test_zones.py
new file mode 100644
index 000000000..c6b96a3f1
--- /dev/null
+++ b/tests/v1_0/test_zones.py
@@ -0,0 +1,76 @@
+
+import StringIO
+
+from novaclient.v1_0 import zones
+from tests.v1_0 import fakes
+from tests import utils
+
+
+os = fakes.FakeClient()
+
+
+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_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(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)
+
+        # 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()
+
+        # 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')
+
+        # 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
new file mode 100644
index 000000000..e4e860f38
--- /dev/null
+++ b/tests/v1_0/testfile.txt
@@ -0,0 +1 @@
+BLAH
diff --git a/tests/v1_0/utils.py b/tests/v1_0/utils.py
new file mode 100644
index 000000000..f878a5e26
--- /dev/null
+++ b/tests/v1_0/utils.py
@@ -0,0 +1,29 @@
+from nose.tools import ok_
+
+
+def fail(msg):
+    raise AssertionError(msg)
+
+
+def assert_in(thing, seq, msg=None):
+    msg = msg or "'%s' not found in %s" % (thing, seq)
+    ok_(thing in seq, msg)
+
+
+def assert_not_in(thing, seq, msg=None):
+    msg = msg or "unexpected '%s' found in %s" % (thing, seq)
+    ok_(thing not in seq, msg)
+
+
+def assert_has_keys(dict, required=[], optional=[]):
+    keys = dict.keys()
+    for k in required:
+        assert_in(k, keys, "required key %s missing from %s" % (k, dict))
+    allowed_keys = set(required) | set(optional)
+    extra_keys = set(keys).difference(set(required + optional))
+    if extra_keys:
+        fail("found unexpected keys: %s" % list(extra_keys))
+
+
+def assert_isinstance(thing, kls):
+    ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls))
diff --git a/tests/v1_1/__init__.py b/tests/v1_1/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py
new file mode 100644
index 000000000..3985a7843
--- /dev/null
+++ b/tests/v1_1/fakes.py
@@ -0,0 +1,373 @@
+import httplib2
+import urllib
+import urlparse
+
+from novaclient import client as base_client
+from novaclient.v1_1 import client
+from tests import fakes
+
+
+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 FakeHTTPClient(base_client.HTTPClient):
+
+    def __init__(self, **kwargs):
+        self.username = 'username'
+        self.apikey = 'apikey'
+        self.auth_url = 'auth_url'
+        self.callstack = []
+
+    def _cs_request(self, url, method, **kwargs):
+        # Check that certain things are called correctly
+        if method in ['GET', 'DELETE']:
+            assert 'body' not in kwargs
+        elif method in ['PUT', 'POST']:
+            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):
+            raise AssertionError('Called unknown API method: %s %s' % (method, url))
+
+        # Note the call
+        self.callstack.append((method, url, kwargs.get('body', None)))
+
+        status, body = getattr(self, callback)(**kwargs)
+        return httplib2.Response({"status": status}), body
+
+    def _munge_get_url(self, url):
+        return url
+
+    #
+    # Limits
+    #
+
+    def get_limits(self, **kw):
+        return (200, {"limits": {
+            "rate": [
+                {
+                    "verb": "POST",
+                    "URI": "*",
+                    "regex": ".*",
+                    "value": 10,
+                    "remaining": 2,
+                    "unit": "MINUTE",
+                    "resetTime": 1244425439
+                },
+                {
+                    "verb": "POST",
+                    "URI": "*/servers",
+                    "regex": "^/servers",
+                    "value": 50,
+                    "remaining": 49,
+                    "unit": "DAY", "resetTime": 1244511839
+                },
+                {
+                    "verb": "PUT",
+                    "URI": "*",
+                    "regex": ".*",
+                    "value": 10,
+                    "remaining": 2,
+                    "unit": "MINUTE",
+                    "resetTime": 1244425439
+                },
+                {
+                    "verb": "GET",
+                    "URI": "*changes-since*",
+                    "regex": "changes-since",
+                    "value": 3,
+                    "remaining": 3,
+                    "unit": "MINUTE",
+                    "resetTime": 1244425439
+                },
+                {
+                    "verb": "DELETE",
+                    "URI": "*",
+                    "regex": ".*",
+                    "value": 100,
+                    "remaining": 100,
+                    "unit": "MINUTE",
+                    "resetTime": 1244425439
+                }
+            ],
+            "absolute": {
+                "maxTotalRAMSize": 51200,
+                "maxIPGroups": 50,
+                "maxIPGroupMembers": 25
+            }
+        }})
+
+    #
+    # Servers
+    #
+
+    def get_servers(self, **kw):
+        return (200, {"servers": [
+            {'id': 1234, 'name': 'sample-server'},
+            {'id': 5678, 'name': 'sample-server2'}
+        ]})
+
+    def get_servers_detail(self, **kw):
+        return (200, {"servers": [
+            {
+                "id": 1234,
+                "name": "sample-server",
+                "image": {
+                    "id": 2,
+                    "name": "sample image",
+                },
+                "flavor": {
+                    "id": 1,
+                    "name": "256 MB Server",
+                },
+                "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
+                "status": "BUILD",
+                "progress": 60,
+                "addresses": {
+                    "public": [{
+                        "version": 4,
+                        "addr": "1.2.3.4",
+                    },
+                    {
+                        "version": 4,
+                        "addr": "5.6.7.8",
+                    }],
+                    "private": [{
+                        "version": 4,
+                        "addr": "10.11.12.13",
+                    }],
+                },
+                "metadata": {
+                    "Server Label": "Web Head 1",
+                    "Image Version": "2.1"
+                }
+            },
+            {
+                "id": 5678,
+                "name": "sample-server2",
+                "image": {
+                    "id": 2,
+                    "name": "sample image",
+                },
+                "flavor": {
+                    "id": 1,
+                    "name": "256 MB Server",
+                },
+                "hostId": "9e107d9d372bb6826bd81d3542a419d6",
+                "status": "ACTIVE",
+                "addresses": {
+                    "public": [{
+                        "version": 4,
+                        "addr": "4.5.6.7",
+                    },
+                    {
+                        "version": 4,
+                        "addr": "5.6.9.8",
+                    }],
+                    "private": [{
+                        "version": 4,
+                        "addr": "10.13.12.13",
+                    }],
+                },
+                "metadata": {
+                    "Server Label": "DB 1"
+                }
+            }
+        ]})
+
+    def post_servers(self, body, **kw):
+        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']:
+                fakes.assert_has_keys(pfile, required=['path', 'contents'])
+        return (202, self.get_servers_1234()[1])
+
+    def get_servers_1234(self, **kw):
+        r = {'server': self.get_servers_detail()[1]['servers'][0]}
+        return (200, r)
+
+    def get_servers_5678(self, **kw):
+        r = {'server': self.get_servers_detail()[1]['servers'][1]}
+        return (200, r)
+
+    def put_servers_1234(self, body, **kw):
+        assert body.keys() == ['server']
+        fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
+        return (204, None)
+
+    def delete_servers_1234(self, **kw):
+        return (202, None)
+
+    #
+    # Server Addresses
+    #
+
+    def get_servers_1234_ips(self, **kw):
+        return (200, {'addresses':
+                      self.get_servers_1234()[1]['server']['addresses']})
+
+    def get_servers_1234_ips_public(self, **kw):
+        return (200, {'public':
+                      self.get_servers_1234_ips()[1]['addresses']['public']})
+
+    def get_servers_1234_ips_private(self, **kw):
+        return (200, {'private':
+                      self.get_servers_1234_ips()[1]['addresses']['private']})
+
+    def delete_servers_1234_ips_public_1_2_3_4(self, **kw):
+        return (202, None)
+
+    #
+    # Server actions
+    #
+
+    def post_servers_1234_action(self, body, **kw):
+        assert len(body.keys()) == 1
+        action = body.keys()[0]
+        if action == 'reboot':
+            assert body[action].keys() == ['type']
+            assert body[action]['type'] in ['HARD', 'SOFT']
+        elif action == 'rebuild':
+            assert body[action].keys() == ['imageRef']
+        elif action == 'resize':
+            assert body[action].keys() == ['flavorRef']
+        elif action == 'confirmResize':
+            assert body[action] is None
+            # This one method returns a different response code
+            return (204, None)
+        elif action == 'revertResize':
+            assert body[action] is None
+        elif action == 'migrate':
+            assert body[action] is None
+        elif action == 'addFixedIp':
+            assert body[action].keys() == ['networkId']
+        elif action == 'removeFixedIp':
+            assert body[action].keys() == ['address']
+        elif action == 'createImage':
+            assert set(body[action].keys()) == set(['name', 'metadata'])
+        elif action == 'changePassword':
+            assert body[action].keys() == ['adminPass']
+        else:
+            raise AssertionError("Unexpected server action: %s" % action)
+        return (202, None)
+
+    #
+    # Flavors
+    #
+
+    def get_flavors(self, **kw):
+        return (200, {'flavors': [
+            {'id': 1, 'name': '256 MB Server'},
+            {'id': 2, 'name': '512 MB Server'}
+        ]})
+
+    def get_flavors_detail(self, **kw):
+        return (200, {'flavors': [
+            {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10},
+            {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20}
+        ]})
+
+    def get_flavors_1(self, **kw):
+        return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][0]})
+
+    def get_flavors_2(self, **kw):
+        return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][1]})
+
+    #
+    # Images
+    #
+    def get_images(self, **kw):
+        return (200, {'images': [
+            {'id': 1, 'name': 'CentOS 5.2'},
+            {'id': 2, 'name': 'My Server Backup'}
+        ]})
+
+    def get_images_detail(self, **kw):
+        return (200, {'images': [
+            {
+                'id': 1,
+                'name': 'CentOS 5.2',
+                "updated": "2010-10-10T12:00:00Z",
+                "created": "2010-08-10T12:00:00Z",
+                "status": "ACTIVE"
+            },
+            {
+                "id": 743,
+                "name": "My Server Backup",
+                "serverId": 12,
+                "updated": "2010-10-10T12:00:00Z",
+                "created": "2010-08-10T12:00:00Z",
+                "status": "SAVING",
+                "progress": 80
+            }
+        ]})
+
+    def get_images_1(self, **kw):
+        return (200, {'image': self.get_images_detail()[1]['images'][0]})
+
+    def get_images_2(self, **kw):
+        return (200, {'image': self.get_images_detail()[1]['images'][1]})
+
+    def post_images(self, body, **kw):
+        assert body.keys() == ['image']
+        fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
+        return (202, self.get_images_1()[1])
+
+    def delete_images_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 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 body.keys() == ['zone']
+        fakes.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)
+
+
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_flavors.py b/tests/v1_1/test_flavors.py
new file mode 100644
index 000000000..bc30cda1b
--- /dev/null
+++ b/tests/v1_1/test_flavors.py
@@ -0,0 +1,38 @@
+
+from novaclient import exceptions
+from novaclient.v1_1 import flavors
+from tests.v1_1 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+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_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(self):
+        f = cs.flavors.find(ram=256)
+        cs.assert_called('GET', '/flavors/detail')
+        self.assertEqual(f.name, '256 MB Server')
+
+        f = cs.flavors.find(disk=20)
+        self.assertEqual(f.name, '512 MB Server')
+
+        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
new file mode 100644
index 000000000..c2c78ed65
--- /dev/null
+++ b/tests/v1_1/test_images.py
@@ -0,0 +1,40 @@
+
+from novaclient.v1_1 import images
+from tests.v1_1 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+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_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_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')
+
+        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
new file mode 100644
index 000000000..74192965f
--- /dev/null
+++ b/tests/v1_1/test_servers.py
@@ -0,0 +1,135 @@
+
+import StringIO
+
+from novaclient.v1_1 import servers
+from tests.v1_1 import fakes
+from tests import utils
+
+
+cs = fakes.FakeClient()
+
+
+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(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_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)
+
+        # 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()
+
+        # Update via manager
+        cs.servers.update(s, name='hi')
+        cs.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')
+
+    def test_find(self):
+        s = cs.servers.find(name='sample-server')
+        cs.assert_called('GET', '/servers/detail')
+        self.assertEqual(s.name, 'sample-server')
+
+        # 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_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_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_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')
+
+    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_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_1/test_shell.py b/tests/v1_1/test_shell.py
new file mode 100644
index 000000000..870df1fff
--- /dev/null
+++ b/tests/v1_1/test_shell.py
@@ -0,0 +1,217 @@
+
+import os
+import mock
+
+from novaclient.shell import OpenStackComputeShell
+from novaclient import exceptions
+from tests.v1_1 import fakes
+from tests import utils
+
+
+class ShellTest(utils.TestCase):
+
+    # Patch os.environ to avoid required auth info.
+    def setUp(self):
+        """Run before each test."""
+        self.old_environment = os.environ.copy()
+        os.environ = {
+            'NOVA_USERNAME': 'username',
+            'NOVA_API_KEY': 'password',
+            'NOVA_PROJECT_ID': 'project_id',
+            'NOVA_VERSION': '1.1',
+        }
+
+        self.shell = OpenStackComputeShell()
+        self.shell.get_api_class = lambda *_: fakes.FakeClient
+
+    def tearDown(self):
+        os.environ = self.old_environment
+
+    def run_command(self, cmd):
+        self.shell.main(cmd.split())
+
+    def assert_called(self, method, url, body=None):
+        return self.shell.cs.assert_called(method, url, body)
+
+    def assert_called_anytime(self, method, url, body=None):
+        return self.shell.cs.assert_called_anytime(method, url, body)
+
+    def test_boot(self):
+        self.run_command('boot --image 1 some-server')
+        self.assert_called_anytime(
+            'POST', '/servers',
+            {'server': {
+                'flavorRef': 1,
+                'name': 'some-server',
+                'imageRef': '1',
+                'min_count': 1,
+                'max_count': 1,
+            }}
+        )
+
+        self.run_command('boot --image 1 --meta foo=bar --meta spam=eggs some-server ')
+        self.assert_called_anytime(
+            'POST', '/servers',
+            {'server': {
+                'flavorRef': 1,
+                'name': 'some-server',
+                'imageRef': '1',
+                'metadata': {'foo': 'bar', 'spam': 'eggs'},
+                'min_count': 1,
+                'max_count': 1,
+            }}
+        )
+
+    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'
+        self.run_command(cmd % (testfile, testfile))
+
+        self.assert_called_anytime(
+            'POST', '/servers',
+            {'server': {
+                'flavorRef': 1,
+                'name': 'some-server',
+                'imageRef': '1',
+                'min_count': 1,
+                'max_count': 1,
+                'personality': [
+                   {'path': '/tmp/bar', 'contents': expected_file_data},
+                   {'path': '/tmp/foo', 'contents': expected_file_data}
+                ]}
+            }
+        )
+
+    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, self.run_command, 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')
+
+        @mock.patch('os.path.exists', mock_exists)
+        @mock.patch('__builtin__.open', mock_open)
+        def test_shell_call():
+            self.run_command('boot some-server --image 1 --key')
+            self.assert_called_anytime(
+                'POST', '/servers',
+                {'server': {
+                    'flavorRef': 1,
+                    'name': 'some-server',
+                    'imageRef': '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, self.run_command,
+                              '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')
+        self.run_command('boot some-server --image 1 --key %s' % testfile)
+        self.assert_called_anytime(
+            'POST', '/servers',
+            {'server': {
+                'flavorRef': 1,
+                'name': 'some-server',
+                'imageRef': '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, self.run_command, 'boot some-server '
+                                               '--image 1 --key %s' % invalid_file)
+
+    def test_flavor_list(self):
+        self.run_command('flavor-list')
+        self.assert_called_anytime('GET', '/flavors/detail')
+
+    def test_image_list(self):
+        self.run_command('image-list')
+        self.assert_called('GET', '/images/detail')
+
+    def test_create_image(self):
+        self.run_command('image-create sample-server mysnapshot')
+        self.assert_called(
+            'POST', '/servers/1234/action',
+            {'createImage': {'name': 'mysnapshot', 'metadata': {}}}
+        )
+
+    def test_image_delete(self):
+        self.run_command('image-delete 1')
+        self.assert_called('DELETE', '/images/1')
+
+    def test_list(self):
+        self.run_command('list')
+        self.assert_called('GET', '/servers/detail')
+
+    def test_reboot(self):
+        self.run_command('reboot sample-server')
+        self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}})
+        self.run_command('reboot sample-server --hard')
+        self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}})
+
+    def test_rebuild(self):
+        self.run_command('rebuild sample-server 1')
+        self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}})
+
+    def test_rename(self):
+        self.run_command('rename sample-server newname')
+        self.assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}})
+
+    def test_resize(self):
+        self.run_command('resize sample-server 1')
+        self.assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}})
+
+    def test_resize_confirm(self):
+        self.run_command('resize-confirm sample-server')
+        self.assert_called('POST', '/servers/1234/action', {'confirmResize': None})
+
+    def test_resize_revert(self):
+        self.run_command('resize-revert sample-server')
+        self.assert_called('POST', '/servers/1234/action', {'revertResize': None})
+
+    @mock.patch('getpass.getpass', mock.Mock(return_value='p'))
+    def test_root_password(self):
+        self.run_command('root-password sample-server')
+        self.assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}})
+
+    def test_show(self):
+        self.run_command('show 1234')
+        # XXX need a way to test multiple calls
+        # assert_called('GET', '/servers/1234')
+        self.assert_called('GET', '/images/2')
+
+    def test_delete(self):
+        self.run_command('delete 1234')
+        self.assert_called('DELETE', '/servers/1234')
+        self.run_command('delete sample-server')
+        self.assert_called('DELETE', '/servers/1234')
diff --git a/tests/v1_1/test_zones.py b/tests/v1_1/test_zones.py
new file mode 100644
index 000000000..a10382c67
--- /dev/null
+++ b/tests/v1_1/test_zones.py
@@ -0,0 +1,76 @@
+
+import StringIO
+
+from novaclient.v1_1 import zones
+from tests.v1_1 import fakes
+from tests import utils
+
+
+os = fakes.FakeClient()
+
+
+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_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(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)
+
+        # 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()
+
+        # 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')
+
+        # 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_1/testfile.txt b/tests/v1_1/testfile.txt
new file mode 100644
index 000000000..e4e860f38
--- /dev/null
+++ b/tests/v1_1/testfile.txt
@@ -0,0 +1 @@
+BLAH
diff --git a/tests/v1_1/utils.py b/tests/v1_1/utils.py
new file mode 100644
index 000000000..f878a5e26
--- /dev/null
+++ b/tests/v1_1/utils.py
@@ -0,0 +1,29 @@
+from nose.tools import ok_
+
+
+def fail(msg):
+    raise AssertionError(msg)
+
+
+def assert_in(thing, seq, msg=None):
+    msg = msg or "'%s' not found in %s" % (thing, seq)
+    ok_(thing in seq, msg)
+
+
+def assert_not_in(thing, seq, msg=None):
+    msg = msg or "unexpected '%s' found in %s" % (thing, seq)
+    ok_(thing not in seq, msg)
+
+
+def assert_has_keys(dict, required=[], optional=[]):
+    keys = dict.keys()
+    for k in required:
+        assert_in(k, keys, "required key %s missing from %s" % (k, dict))
+    allowed_keys = set(required) | set(optional)
+    extra_keys = set(keys).difference(set(required + optional))
+    if extra_keys:
+        fail("found unexpected keys: %s" % list(extra_keys))
+
+
+def assert_isinstance(thing, kls):
+    ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls))