diff --git a/novaclient/base.py b/novaclient/base.py
index 1f73ffda1..a401dcd25 100644
--- a/novaclient/base.py
+++ b/novaclient/base.py
@@ -22,6 +22,7 @@ Base utilities to build API operation managers and objects on top of.
 import contextlib
 import os
 from novaclient import exceptions
+from novaclient import utils
 
 
 # Python 2.4 compat
@@ -50,7 +51,7 @@ def getid(obj):
         return obj
 
 
-class Manager(object):
+class Manager(utils.HookableMixin):
     """
     Managers interact with a particular type of API (servers, flavors, images,
     etc.) and provide CRUD operations for them.
@@ -125,7 +126,8 @@ class Manager(object):
         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):
+    def _create(self, url, body, response_key, return_raw=False, **kwargs):
+        self.run_hooks('modify_body_for_create', body, **kwargs)
         resp, body = self.api.client.post(url, body=body)
         if return_raw:
             return body[response_key]
@@ -136,7 +138,8 @@ class Manager(object):
     def _delete(self, url):
         resp, body = self.api.client.delete(url)
 
-    def _update(self, url, body):
+    def _update(self, url, body, **kwargs):
+        self.run_hooks('modify_body_for_update', body, **kwargs)
         resp, body = self.api.client.put(url, body=body)
 
 
@@ -184,7 +187,7 @@ class BootingManagerWithFind(ManagerWithFind):
     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):
+              max_count=None, **kwargs):
         """
         Create (boot) a new server.
 
@@ -245,7 +248,7 @@ class BootingManagerWithFind(ManagerWithFind):
                 })
 
         return self._create(resource_url, body, response_key,
-                            return_raw=return_raw)
+                            return_raw=return_raw, **kwargs)
 
 
 class Resource(object):
diff --git a/novaclient/extension.py b/novaclient/extension.py
new file mode 100644
index 000000000..7c91a8e89
--- /dev/null
+++ b/novaclient/extension.py
@@ -0,0 +1,39 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from novaclient import base
+from novaclient import utils
+
+
+class Extension(utils.HookableMixin):
+    """Extension descriptor."""
+
+    SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
+
+    def __init__(self, name, module):
+        self.name = name
+        self.module = module
+        self._parse_extension_module()
+
+    def _parse_extension_module(self):
+        self.manager_class = None
+        for attr_name, attr_value in self.module.__dict__.items():
+            if attr_name in self.SUPPORTED_HOOKS:
+                self.add_hook(attr_name, attr_value)
+            elif utils.safe_issubclass(attr_value, base.Manager):
+                self.manager_class = attr_value
+
+    def __repr__(self):
+        return "<Extension '%s'>" % self.name
diff --git a/novaclient/shell.py b/novaclient/shell.py
index 4ebcdec01..58cea4066 100644
--- a/novaclient/shell.py
+++ b/novaclient/shell.py
@@ -27,6 +27,7 @@ import sys
 
 from novaclient import base
 from novaclient import exceptions as exc
+import novaclient.extension
 from novaclient import utils
 from novaclient.v1_1 import shell as shell_v1_1
 from novaclient.keystone import shell as shell_keystone
@@ -123,7 +124,7 @@ class OpenStackComputeShell(object):
 
         return parser
 
-    def get_subcommand_parser(self, version, extensions):
+    def get_subcommand_parser(self, version):
         parser = self.get_base_parser()
 
         self.subcommands = {}
@@ -141,8 +142,8 @@ class OpenStackComputeShell(object):
         self._find_actions(subparsers, shell_keystone)
         self._find_actions(subparsers, self)
 
-        for _, _, ext_module in extensions:
-            self._find_actions(subparsers, ext_module)
+        for extension in self.extensions:
+            self._find_actions(subparsers, extension.module)
 
         self._add_bash_completion_subparser(subparsers)
 
@@ -161,22 +162,9 @@ class OpenStackComputeShell(object):
             if name == "__init__":
                 continue
 
-            ext_module = imp.load_source(name, ext_path)
-
-            # Extract Manager class
-            ext_manager_class = None
-            for attr_value in ext_module.__dict__.values():
-                try:
-                    if issubclass(attr_value, base.Manager):
-                        ext_manager_class = attr_value
-                        break
-                except TypeError:
-                    continue  # in case attr_value isn't a class...
-
-            if not ext_manager_class:
-                raise Exception("Could not find Manager class in extension.")
-
-            extensions.append((name, ext_manager_class, ext_module))
+            module = imp.load_source(name, ext_path)
+            extension = novaclient.extension.Extension(name, module)
+            extensions.append(extension)
 
         return extensions
 
@@ -218,13 +206,14 @@ class OpenStackComputeShell(object):
         (options, args) = parser.parse_known_args(argv)
 
         # build available subcommands based on version
-        extensions = self._discover_extensions(options.version)
-        subcommand_parser = self.get_subcommand_parser(
-                options.version, extensions)
+        self.extensions = self._discover_extensions(options.version)
+        self._run_extension_hooks('__pre_parse_args__')
+
+        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)
+        self._run_extension_hooks('__post_parse_args__', args)
 
         # Deal with global arguments
         if args.debug:
@@ -287,7 +276,7 @@ class OpenStackComputeShell(object):
                                      projectid, url, insecure,
                                      region_name=region_name,
                                      endpoint_name=endpoint_name,
-                                     extensions=extensions)
+                                     extensions=self.extensions)
 
         try:
             if not utils.isunauthenticated(args.func):
@@ -299,6 +288,11 @@ class OpenStackComputeShell(object):
 
         args.func(self.cs, args)
 
+    def _run_extension_hooks(self, hook_type, *args, **kwargs):
+        """Run hooks for all registered extensions."""
+        for extension in self.extensions:
+            extension.run_hooks(hook_type, *args, **kwargs)
+
     def get_api_class(self, version):
         try:
             return {
diff --git a/novaclient/utils.py b/novaclient/utils.py
index 9c6dbd079..8a3617ff7 100644
--- a/novaclient/utils.py
+++ b/novaclient/utils.py
@@ -5,16 +5,63 @@ import prettytable
 from novaclient import exceptions
 
 
-# Decorator for cli-args
 def arg(*args, **kwargs):
+    """Decorator for CLI args."""
     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))
+        add_arg(func, *args, **kwargs)
         return func
     return _decorator
 
 
+def add_arg(f, *args, **kwargs):
+    """Bind CLI arguments to a shell.py `do_foo` function."""
+
+    if not hasattr(f, 'arguments'):
+        f.arguments = []
+
+    # NOTE(sirp): avoid dups that can occur when the module is shared across
+    # tests.
+    if (args, kwargs) not in f.arguments:
+        # Because of the sematics of decorator composition if we just append
+        # to the options list positional options will appear to be backwards.
+        f.arguments.insert(0, (args, kwargs))
+
+
+def add_resource_manager_extra_kwargs_hook(f, hook):
+    """Adds hook to bind CLI arguments to ResourceManager calls.
+
+    The `do_foo` calls in shell.py will receive CLI args and then in turn pass
+    them through to the ResourceManager. Before passing through the args, the
+    hooks registered here will be called, giving us a chance to add extra
+    kwargs (taken from the command-line) to what's passed to the
+    ResourceManager.
+    """
+    if not hasattr(f, 'resource_manager_kwargs_hooks'):
+        f.resource_manager_kwargs_hooks = []
+
+    names = [h.__name__ for h in f.resource_manager_kwargs_hooks]
+    if hook.__name__ not in names:
+        f.resource_manager_kwargs_hooks.append(hook)
+
+
+def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
+    """Return extra_kwargs by calling resource manager kwargs hooks."""
+    hooks = getattr(f, "resource_manager_kwargs_hooks", [])
+    extra_kwargs = {}
+    for hook in hooks:
+        hook_name = hook.__name__
+        hook_kwargs = hook(args)
+
+        conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
+        if conflicting_keys and not allow_conflicts:
+            raise Exception("Hook '%(hook_name)s' is attempting to redefine"
+                            " attributes '%(conflicting_keys)s'" % locals())
+
+        extra_kwargs.update(hook_kwargs)
+
+    return extra_kwargs
+
+
 def unauthenticated(f):
     """
     Adds 'unauthenticated' attribute to decorated function.
@@ -104,3 +151,33 @@ def _format_servers_list_networks(server):
         output.append(group)
 
     return '; '.join(output)
+
+
+class HookableMixin(object):
+    """Mixin so classes can register and run hooks."""
+    _hooks_map = {}
+
+    @classmethod
+    def add_hook(cls, hook_type, hook_func):
+        if hook_type not in cls._hooks_map:
+            cls._hooks_map[hook_type] = []
+
+        cls._hooks_map[hook_type].append(hook_func)
+
+    @classmethod
+    def run_hooks(cls, hook_type, *args, **kwargs):
+        hook_funcs = cls._hooks_map.get(hook_type) or []
+        for hook_func in hook_funcs:
+            hook_func(*args, **kwargs)
+
+
+def safe_issubclass(*args):
+    """Like issubclass, but will just return False if not a class."""
+
+    try:
+        if issubclass(*args):
+            return True
+    except TypeError:
+        pass
+
+    return False
diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py
index 7c326c7f1..ced9d7412 100644
--- a/novaclient/v1_1/base.py
+++ b/novaclient/v1_1/base.py
@@ -20,13 +20,16 @@ import base64
 from novaclient import base
 
 
+# FIXME(sirp): Now that v1_0 has been removed, this can be merged with
+# base.ManagerWithFind
 class BootingManagerWithFind(base.ManagerWithFind):
     """Like a `ManagerWithFind`, but has the ability to boot servers."""
     def _boot(self, resource_url, response_key, name, image, flavor,
               meta=None, files=None, zone_blob=None, userdata=None,
               reservation_id=None, return_raw=False, min_count=None,
               max_count=None, security_groups=None, key_name=None,
-              availability_zone=None, block_device_mapping=None, nics=None):
+              availability_zone=None, block_device_mapping=None, nics=None,
+              **kwargs):
         """
         Create (boot) a new server.
 
@@ -144,4 +147,4 @@ class BootingManagerWithFind(base.ManagerWithFind):
             body['server']['networks'] = all_net_data
 
         return self._create(resource_url, body, response_key,
-                            return_raw=return_raw)
+                            return_raw=return_raw, **kwargs)
diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py
index 8f363f05d..5ead8ab3b 100644
--- a/novaclient/v1_1/client.py
+++ b/novaclient/v1_1/client.py
@@ -55,8 +55,10 @@ class Client(object):
 
         # Add in any extensions...
         if extensions:
-            for (ext_name, ext_manager_class, ext_module) in extensions:
-                setattr(self, ext_name, ext_manager_class(self))
+            for extension in extensions:
+                if extension.manager_class:
+                    setattr(self, extension.name,
+                            extension.manager_class(self))
 
         self.client = client.HTTPClient(username,
                                         password,
diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py
index 8291f8eda..7da3474c0 100644
--- a/novaclient/v1_1/servers.py
+++ b/novaclient/v1_1/servers.py
@@ -336,7 +336,7 @@ class ServerManager(local_base.BootingManagerWithFind):
                zone_blob=None, reservation_id=None, min_count=None,
                max_count=None, security_groups=None, userdata=None,
                key_name=None, availability_zone=None,
-               block_device_mapping=None, nics=None):
+               block_device_mapping=None, nics=None, **kwargs):
         # TODO: (anthony) indicate in doc string if param is an extension
         # and/or optional
         """
@@ -375,22 +375,26 @@ class ServerManager(local_base.BootingManagerWithFind):
             max_count = min_count
         if min_count > max_count:
             min_count = max_count
+
+        boot_args = [name, image, flavor]
+
+        boot_kwargs = dict(
+            meta=meta, files=files, userdata=userdata, zone_blob=zone_blob,
+            reservation_id=reservation_id, min_count=min_count,
+            max_count=max_count, security_groups=security_groups,
+            key_name=key_name, availability_zone=availability_zone,
+            **kwargs)
+
         if block_device_mapping:
-            return self._boot("/os-volumes_boot", "server",
-                        name, image, flavor,
-                        meta=meta, files=files, userdata=userdata,
-                        zone_blob=zone_blob, reservation_id=reservation_id,
-                        min_count=min_count, max_count=max_count,
-                        security_groups=security_groups, key_name=key_name,
-                        availability_zone=availability_zone,
-                        block_device_mapping=block_device_mapping)
+            resource_url = "/os-volumes_boot"
+            boot_kwargs['block_device_mapping'] = block_device_mapping
         else:
-            return self._boot("/servers", "server", name, image, flavor,
-                          meta=meta, files=files, userdata=userdata,
-                          zone_blob=zone_blob, reservation_id=reservation_id,
-                          min_count=min_count, max_count=max_count,
-                          security_groups=security_groups, key_name=key_name,
-                          availability_zone=availability_zone, nics=nics)
+            resource_url = "/servers"
+            boot_kwargs['nics'] = nics
+
+        response_key = "server"
+        return self._boot(resource_url, response_key, *boot_args,
+                **boot_kwargs)
 
     def update(self, server, name=None):
         """
diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py
index cc8f7c90b..4813ec74c 100644
--- a/novaclient/v1_1/shell.py
+++ b/novaclient/v1_1/shell.py
@@ -51,7 +51,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
     flavor = args.flavor
     image = args.image
 
-    metadata = dict(v.split('=') for v in args.meta)
+    meta = dict(v.split('=') for v in args.meta)
 
     files = {}
     for f in args.files:
@@ -89,12 +89,12 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
 
     if args.user_data:
         try:
-            user_data = open(args.user_data)
+            userdata = open(args.user_data)
         except IOError, e:
             raise exceptions.CommandError("Can't open '%s': %s" % \
                                           (args.user_data, e))
     else:
-        user_data = None
+        userdata = None
 
     if args.availability_zone:
         availability_zone = args.availability_zone
@@ -119,9 +119,22 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
             nic_info[k] = v
         nics.append(nic_info)
 
-    return (args.name, image, flavor, metadata, files, key_name,
-            reservation_id, min_count, max_count, user_data, \
-            availability_zone, security_groups, block_device_mapping, nics)
+    boot_args = [args.name, image, flavor]
+
+    boot_kwargs = dict(
+            meta=meta,
+            files=files,
+            key_name=key_name,
+            reservation_id=reservation_id,
+            min_count=min_count,
+            max_count=max_count,
+            userdata=userdata,
+            availability_zone=availability_zone,
+            security_groups=security_groups,
+            block_device_mapping=block_device_mapping,
+            nics=nics)
+
+    return boot_args, boot_kwargs
 
 
 @utils.arg('--flavor',
@@ -186,21 +199,12 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
            "v4-fixed-ip: IPv4 fixed address for NIC (optional).")
 def do_boot(cs, args):
     """Boot a new server."""
-    name, image, flavor, metadata, files, key_name, reservation_id, \
-        min_count, max_count, user_data, availability_zone, \
-        security_groups, block_device_mapping, nics = _boot(cs, args)
+    boot_args, boot_kwargs = _boot(cs, args)
 
-    server = cs.servers.create(args.name, image, flavor,
-                                    meta=metadata,
-                                    files=files,
-                                    min_count=min_count,
-                                    max_count=max_count,
-                                    userdata=user_data,
-                                    availability_zone=availability_zone,
-                                    security_groups=security_groups,
-                                    key_name=key_name,
-                                    block_device_mapping=block_device_mapping,
-                                    nics=nics)
+    extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args)
+    boot_kwargs.update(extra_boot_kwargs)
+
+    server = cs.servers.create(*boot_args, **boot_kwargs)
 
     # Keep any information (like adminPass) returned by create
     info = server._info
@@ -269,23 +273,17 @@ def do_boot(cs, args):
 @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,\
-            user_data, availability_zone, security_groups = \
-                             _boot(cs, args,
-                                        reservation_id=reservation_id,
-                                        min_count=min_count,
-                                        max_count=max_count)
+    boot_args, boot_kwargs = _boot(cs,
+                                   args,
+                                   reservation_id=args.reservation_id,
+                                   min_count=args.min_instances,
+                                   max_count=args.max_instances)
 
-    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)
+    extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(
+            do_zone_boot, args)
+    boot_kwargs.update(extra_boot_kwargs)
+
+    reservation_id = cs.zones.boot(*boot_args, **boot_kwargs)
     print "Reservation ID=", reservation_id
 
 
diff --git a/novaclient/v1_1/zones.py b/novaclient/v1_1/zones.py
index c8308d489..1bc7cfd1a 100644
--- a/novaclient/v1_1/zones.py
+++ b/novaclient/v1_1/zones.py
@@ -120,7 +120,7 @@ class ZoneManager(local_base.BootingManagerWithFind):
 
     def boot(self, name, image, flavor, meta=None, files=None,
                zone_blob=None, reservation_id=None, min_count=None,
-               max_count=None):
+               max_count=None, **kwargs):
         """
         Create (boot) a new server while being aware of Zones.
 
@@ -150,7 +150,7 @@ class ZoneManager(local_base.BootingManagerWithFind):
                           meta=meta, files=files,
                           zone_blob=zone_blob, reservation_id=reservation_id,
                           return_raw=True, min_count=min_count,
-                          max_count=max_count)
+                          max_count=max_count, **kwargs)
 
     def select(self, *args, **kwargs):
         """