diff --git a/mistralclient/api/base.py b/mistralclient/api/base.py
index 4c3ef4e1..bfac5649 100644
--- a/mistralclient/api/base.py
+++ b/mistralclient/api/base.py
@@ -15,7 +15,8 @@
 #    limitations under the License.
 
 import json
-import logging
+
+from mistralclient.openstack.common import log as logging
 
 LOG = logging.getLogger(__name__)
 
diff --git a/mistralclient/api/client.py b/mistralclient/api/client.py
index 3f1d0e93..386b2816 100644
--- a/mistralclient/api/client.py
+++ b/mistralclient/api/client.py
@@ -56,8 +56,8 @@ class Client(object):
                      input_auth_token=None):
         if mistral_url and not isinstance(mistral_url, six.string_types):
             raise RuntimeError('Mistral url should be string')
-        if (isinstance(project_name, six.string_types) or
-                isinstance(project_id, six.string_types)):
+        if ((isinstance(project_name, six.string_types) and project_name) or
+                (isinstance(project_id, six.string_types) and project_id)):
             if project_name and project_id:
                 raise RuntimeError('Only project name or '
                                    'project id should be set')
@@ -77,12 +77,10 @@ class Client(object):
             token = keystone.auth_token
             user_id = keystone.user_id
             if project_name and not project_id:
-                if keystone.tenants.find(name=project_name):
-                    project_id = str(keystone.tenants.find(
-                        name=project_name).id)
+                project_id = keystone.project_id
         else:
             raise RuntimeError('Project name or project id should'
-                               ' not be empty and should be string')
+                               ' not be empty and should be non-empty string')
 
         if not mistral_url:
             catalog = keystone.service_catalog.get_endpoints(service_type)
diff --git a/mistralclient/api/workbooks.py b/mistralclient/api/workbooks.py
index 0be0356d..eeffa6dd 100644
--- a/mistralclient/api/workbooks.py
+++ b/mistralclient/api/workbooks.py
@@ -44,7 +44,7 @@ class WorkbookManager(base.ResourceManager):
             'tags': tags,
         }
 
-        return self._update('/workbooks', data)
+        return self._update('/workbooks/%s' % name, data)
 
     def list(self):
         return self._list('/workbooks', 'workbooks')
diff --git a/mistralclient/commands/__init__.py b/mistralclient/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mistralclient/commands/executions.py b/mistralclient/commands/executions.py
new file mode 100644
index 00000000..907d1ad4
--- /dev/null
+++ b/mistralclient/commands/executions.py
@@ -0,0 +1,163 @@
+# Copyright 2014 StackStorm, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+import json
+
+from cliff.command import Command as BaseCommand
+from cliff.lister import Lister as ListCommand
+from cliff.show import ShowOne as ShowCommand
+
+from mistralclient.openstack.common import log as logging
+from mistralclient.api.executions import ExecutionManager
+
+LOG = logging.getLogger(__name__)
+
+
+def format(execution=None):
+    columns = (
+        'ID',
+        'Workbook',
+        'Target',
+        'State'
+    )
+
+    if execution:
+        data = (
+            execution.id,
+            execution.workbook_name,
+            execution.task,
+            execution.state
+        )
+    else:
+        data = (tuple('<none>' for _ in range(len(columns))),)
+
+    return (columns, data)
+
+
+class List(ListCommand):
+    "List all executions"
+
+    def get_parser(self, prog_name):
+        parser = super(List, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Execution workbook')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        data = [format(execution)[1] for execution
+                in ExecutionManager(self.app.client)
+                .list(parsed_args.workbook)]
+
+        if data:
+            return (format()[0], data)
+        else:
+            return format()
+
+
+class Get(ShowCommand):
+    "Show specific execution"
+
+    def get_parser(self, prog_name):
+        parser = super(Get, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Execution workbook')
+        parser.add_argument(
+            'id',
+            help='Execution identifier')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        execution = ExecutionManager(self.app.client)\
+            .get(parsed_args.workbook, parsed_args.id)
+
+        return format(execution)
+
+
+class Create(ShowCommand):
+    "Create new execution"
+
+    def get_parser(self, prog_name):
+        parser = super(Create, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Execution workbook')
+        parser.add_argument(
+            'task',
+            help='Execution task')
+        parser.add_argument(
+            'context',
+            type=json.loads,
+            help='Execution context')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        execution = ExecutionManager(self.app.client)\
+            .create(parsed_args.workbook,
+                    parsed_args.task,
+                    parsed_args.context)
+
+        return format(execution)
+
+
+class Delete(BaseCommand):
+    "Delete execution"
+
+    def get_parser(self, prog_name):
+        parser = super(Delete, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Execution workbook')
+        parser.add_argument(
+            'id',
+            help='Execution identifier')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        ExecutionManager(self.app.client)\
+            .delete(parsed_args.workbook, parsed_args.id)
+
+
+class Update(ShowCommand):
+    "Update execution"
+
+    def get_parser(self, prog_name):
+        parser = super(Update, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Execution workbook')
+        parser.add_argument(
+            'id',
+            help='Execution identifier')
+        parser.add_argument(
+            'state',
+            choices=['RUNNING', 'SUSPENDED', 'STOPPED', 'SUCCESS', 'ERROR'],
+            help='Execution state')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        execution = ExecutionManager(self.app.client)\
+            .update(parsed_args.workbook,
+                    parsed_args.id,
+                    parsed_args.state)
+
+        return format(execution)
diff --git a/mistralclient/commands/tasks.py b/mistralclient/commands/tasks.py
new file mode 100644
index 00000000..f4da96ad
--- /dev/null
+++ b/mistralclient/commands/tasks.py
@@ -0,0 +1,131 @@
+# Copyright 2014 StackStorm, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+from cliff.lister import Lister as ListCommand
+from cliff.show import ShowOne as ShowCommand
+
+from mistralclient.openstack.common import log as logging
+from mistralclient.api.tasks import TaskManager
+
+LOG = logging.getLogger(__name__)
+
+
+def format(task=None):
+    columns = (
+        'ID',
+        'Workbook',
+        'Execution',
+        'Name',
+        'Description',
+        'State',
+        'Tags'
+    )
+
+    if task:
+        data = (
+            task.id,
+            task.workbook_name,
+            task.execution_id,
+            task.name,
+            task.description or '<none>',
+            task.state,
+            task.tags and ', '.join(task.tags) or '<none>'
+        )
+    else:
+        data = (tuple('<none>' for _ in range(len(columns))),)
+
+    return (columns, data)
+
+
+class List(ListCommand):
+    "List all tasks"
+
+    def get_parser(self, prog_name):
+        parser = super(List, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Task workbook')
+        parser.add_argument(
+            'execution',
+            help='Task execution')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        data = [format(task)[1] for task
+                in TaskManager(self.app.client)
+                .list(parsed_args.workbook,
+                      parsed_args.execution)]
+
+        if data:
+            return (format()[0], data)
+        else:
+            return format()
+
+
+class Get(ShowCommand):
+    "Show specific task"
+
+    def get_parser(self, prog_name):
+        parser = super(Get, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Task workbook')
+        parser.add_argument(
+            'execution',
+            help='Task execution')
+        parser.add_argument(
+            'id',
+            help='Task identifier')
+        return parser
+
+    def take_action(self, parsed_args):
+        execution = TaskManager(self.app.client)\
+            .get(parsed_args.workbook,
+                 parsed_args.execution,
+                 parsed_args.id)
+
+        return format(execution)
+
+
+class Update(ShowCommand):
+    "Update task"
+
+    def get_parser(self, prog_name):
+        parser = super(Update, self).get_parser(prog_name)
+        parser.add_argument(
+            'workbook',
+            help='Task workbook')
+        parser.add_argument(
+            'execution',
+            help='Task execution')
+        parser.add_argument(
+            'id',
+            help='Task identifier')
+        parser.add_argument(
+            'state',
+            choices=['IDLE', 'RUNNING', 'SUCCESS', 'ERROR'],
+            help='Task state')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        execution = TaskManager(self.app.client).update(parsed_args.workbook,
+                                                        parsed_args.execution,
+                                                        parsed_args.id,
+                                                        parsed_args.state)
+
+        return format(execution)
diff --git a/mistralclient/commands/workbooks.py b/mistralclient/commands/workbooks.py
new file mode 100644
index 00000000..4e119506
--- /dev/null
+++ b/mistralclient/commands/workbooks.py
@@ -0,0 +1,184 @@
+# Copyright 2014 StackStorm, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+import argparse
+
+from cliff.command import Command as BaseCommand
+from cliff.lister import Lister as ListCommand
+from cliff.show import ShowOne as ShowCommand
+
+from mistralclient.openstack.common import log as logging
+from mistralclient.api.workbooks import WorkbookManager
+
+LOG = logging.getLogger(__name__)
+
+
+def format(workbook=None):
+    columns = (
+        'Name',
+        'Description',
+        'Tags'
+    )
+
+    if workbook:
+        data = (
+            workbook.name,
+            workbook.description or '<none>',
+            ', '.join(workbook.tags) or '<none>'
+        )
+    else:
+        data = (tuple('<none>' for _ in range(len(columns))),)
+
+    return (columns, data)
+
+
+class List(ListCommand):
+    "List all workbooks"
+
+    def take_action(self, parsed_args):
+        data = [format(workbook)[1] for workbook
+                in WorkbookManager(self.app.client).list()]
+
+        if data:
+            return (format()[0], data)
+        else:
+            return format()
+
+
+class Get(ShowCommand):
+    "Show specific workbook"
+
+    def get_parser(self, prog_name):
+        parser = super(Get, self).get_parser(prog_name)
+        parser.add_argument(
+            'name',
+            help='Workbook name')
+        return parser
+
+    def take_action(self, parsed_args):
+        workbook = WorkbookManager(self.app.client).get(parsed_args.name)
+
+        return format(workbook)
+
+
+class Create(ShowCommand):
+    "Create new workbook"
+
+    def get_parser(self, prog_name):
+        parser = super(Create, self).get_parser(prog_name)
+        parser.add_argument(
+            'name',
+            help='Workbook name')
+        parser.add_argument(
+            'description',
+            nargs='?',
+            help='Workbook description')
+        parser.add_argument(
+            'tags',
+            nargs='*',
+            help='Workbook tags')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        workbook = WorkbookManager(self.app.client)\
+            .create(parsed_args.name,
+                    parsed_args.description,
+                    parsed_args.tags)
+
+        return format(workbook)
+
+
+class Delete(BaseCommand):
+    "Delete workbook"
+
+    def get_parser(self, prog_name):
+        parser = super(Delete, self).get_parser(prog_name)
+        parser.add_argument(
+            'name',
+            help='Workbook name')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        WorkbookManager(self.app.client).delete(parsed_args.name)
+
+
+class Update(ShowCommand):
+    "Update workbook"
+
+    def get_parser(self, prog_name):
+        parser = super(Update, self).get_parser(prog_name)
+        parser.add_argument(
+            'name',
+            help='Workbook name')
+        parser.add_argument(
+            'description',
+            nargs='?',
+            help='Workbook description')
+        parser.add_argument(
+            'tags',
+            nargs='*',
+            help='Workbook tags')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        workbook = WorkbookManager(self.app.client)\
+            .update(parsed_args.name,
+                    parsed_args.description,
+                    parsed_args.tags)
+
+        return format(workbook)
+
+
+class UploadDefinition(BaseCommand):
+    "Upload workbook definition"
+
+    def get_parser(self, prog_name):
+        parser = super(UploadDefinition, self).get_parser(prog_name)
+        parser.add_argument(
+            'name',
+            help='Workbook name')
+        parser.add_argument(
+            'path',
+            type=argparse.FileType('r'),
+            help='Workbook definition file')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        WorkbookManager(self.app.client)\
+            .upload_definition(parsed_args.name,
+                               parsed_args.path.read())
+
+
+class GetDefinition(BaseCommand):
+    "Show workbook definition"
+
+    def get_parser(self, prog_name):
+        parser = super(GetDefinition, self).get_parser(prog_name)
+        parser.add_argument(
+            'name',
+            help='Workbook name')
+
+        return parser
+
+    def take_action(self, parsed_args):
+        definition = WorkbookManager(self.app.client)\
+            .get_definition(parsed_args.name)
+
+        self.app.stdout.write(definition)
diff --git a/mistralclient/openstack/__init__.py b/mistralclient/openstack/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mistralclient/openstack/common/__init__.py b/mistralclient/openstack/common/__init__.py
new file mode 100644
index 00000000..2a00f3bc
--- /dev/null
+++ b/mistralclient/openstack/common/__init__.py
@@ -0,0 +1,2 @@
+import six
+six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))
diff --git a/mistralclient/openstack/common/apiclient/__init__.py b/mistralclient/openstack/common/apiclient/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mistralclient/openstack/common/apiclient/auth.py b/mistralclient/openstack/common/apiclient/auth.py
new file mode 100644
index 00000000..3e65c7a2
--- /dev/null
+++ b/mistralclient/openstack/common/apiclient/auth.py
@@ -0,0 +1,221 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright 2013 Spanish National Research Council.
+# 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.
+
+# E0202: An attribute inherited from %s hide this method
+# pylint: disable=E0202
+
+import abc
+import argparse
+import os
+
+import six
+from stevedore import extension
+
+from mistralclient.openstack.common.apiclient import exceptions
+
+
+_discovered_plugins = {}
+
+
+def discover_auth_systems():
+    """Discover the available auth-systems.
+
+    This won't take into account the old style auth-systems.
+    """
+    global _discovered_plugins
+    _discovered_plugins = {}
+
+    def add_plugin(ext):
+        _discovered_plugins[ext.name] = ext.plugin
+
+    ep_namespace = "mistralclient.openstack.common.apiclient.auth"
+    mgr = extension.ExtensionManager(ep_namespace)
+    mgr.map(add_plugin)
+
+
+def load_auth_system_opts(parser):
+    """Load options needed by the available auth-systems into a parser.
+
+    This function will try to populate the parser with options from the
+    available plugins.
+    """
+    group = parser.add_argument_group("Common auth options")
+    BaseAuthPlugin.add_common_opts(group)
+    for name, auth_plugin in six.iteritems(_discovered_plugins):
+        group = parser.add_argument_group(
+            "Auth-system '%s' options" % name,
+            conflict_handler="resolve")
+        auth_plugin.add_opts(group)
+
+
+def load_plugin(auth_system):
+    try:
+        plugin_class = _discovered_plugins[auth_system]
+    except KeyError:
+        raise exceptions.AuthSystemNotFound(auth_system)
+    return plugin_class(auth_system=auth_system)
+
+
+def load_plugin_from_args(args):
+    """Load required plugin and populate it with options.
+
+    Try to guess auth system if it is not specified. Systems are tried in
+    alphabetical order.
+
+    :type args: argparse.Namespace
+    :raises: AuthPluginOptionsMissing
+    """
+    auth_system = args.os_auth_system
+    if auth_system:
+        plugin = load_plugin(auth_system)
+        plugin.parse_opts(args)
+        plugin.sufficient_options()
+        return plugin
+
+    for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
+        plugin_class = _discovered_plugins[plugin_auth_system]
+        plugin = plugin_class()
+        plugin.parse_opts(args)
+        try:
+            plugin.sufficient_options()
+        except exceptions.AuthPluginOptionsMissing:
+            continue
+        return plugin
+    raise exceptions.AuthPluginOptionsMissing(["auth_system"])
+
+
+@six.add_metaclass(abc.ABCMeta)
+class BaseAuthPlugin(object):
+    """Base class for authentication plugins.
+
+    An authentication plugin needs to override at least the authenticate
+    method to be a valid plugin.
+    """
+
+    auth_system = None
+    opt_names = []
+    common_opt_names = [
+        "auth_system",
+        "username",
+        "password",
+        "tenant_name",
+        "token",
+        "auth_url",
+    ]
+
+    def __init__(self, auth_system=None, **kwargs):
+        self.auth_system = auth_system or self.auth_system
+        self.opts = dict((name, kwargs.get(name))
+                         for name in self.opt_names)
+
+    @staticmethod
+    def _parser_add_opt(parser, opt):
+        """Add an option to parser in two variants.
+
+        :param opt: option name (with underscores)
+        """
+        dashed_opt = opt.replace("_", "-")
+        env_var = "OS_%s" % opt.upper()
+        arg_default = os.environ.get(env_var, "")
+        arg_help = "Defaults to env[%s]." % env_var
+        parser.add_argument(
+            "--os-%s" % dashed_opt,
+            metavar="<%s>" % dashed_opt,
+            default=arg_default,
+            help=arg_help)
+        parser.add_argument(
+            "--os_%s" % opt,
+            metavar="<%s>" % dashed_opt,
+            help=argparse.SUPPRESS)
+
+    @classmethod
+    def add_opts(cls, parser):
+        """Populate the parser with the options for this plugin.
+        """
+        for opt in cls.opt_names:
+            # use `BaseAuthPlugin.common_opt_names` since it is never
+            # changed in child classes
+            if opt not in BaseAuthPlugin.common_opt_names:
+                cls._parser_add_opt(parser, opt)
+
+    @classmethod
+    def add_common_opts(cls, parser):
+        """Add options that are common for several plugins.
+        """
+        for opt in cls.common_opt_names:
+            cls._parser_add_opt(parser, opt)
+
+    @staticmethod
+    def get_opt(opt_name, args):
+        """Return option name and value.
+
+        :param opt_name: name of the option, e.g., "username"
+        :param args: parsed arguments
+        """
+        return (opt_name, getattr(args, "os_%s" % opt_name, None))
+
+    def parse_opts(self, args):
+        """Parse the actual auth-system options if any.
+
+        This method is expected to populate the attribute `self.opts` with a
+        dict containing the options and values needed to make authentication.
+        """
+        self.opts.update(dict(self.get_opt(opt_name, args)
+                              for opt_name in self.opt_names))
+
+    def authenticate(self, http_client):
+        """Authenticate using plugin defined method.
+
+        The method usually analyses `self.opts` and performs
+        a request to authentication server.
+
+        :param http_client: client object that needs authentication
+        :type http_client: HTTPClient
+        :raises: AuthorizationFailure
+        """
+        self.sufficient_options()
+        self._do_authenticate(http_client)
+
+    @abc.abstractmethod
+    def _do_authenticate(self, http_client):
+        """Protected method for authentication.
+        """
+
+    def sufficient_options(self):
+        """Check if all required options are present.
+
+        :raises: AuthPluginOptionsMissing
+        """
+        missing = [opt
+                   for opt in self.opt_names
+                   if not self.opts.get(opt)]
+        if missing:
+            raise exceptions.AuthPluginOptionsMissing(missing)
+
+    @abc.abstractmethod
+    def token_and_endpoint(self, endpoint_type, service_type):
+        """Return token and endpoint.
+
+        :param service_type: Service type of the endpoint
+        :type service_type: string
+        :param endpoint_type: Type of endpoint.
+                              Possible values: public or publicURL,
+                                  internal or internalURL,
+                                  admin or adminURL
+        :type endpoint_type: string
+        :returns: tuple of token and endpoint strings
+        :raises: EndpointException
+        """
diff --git a/mistralclient/openstack/common/apiclient/base.py b/mistralclient/openstack/common/apiclient/base.py
new file mode 100644
index 00000000..81c1276d
--- /dev/null
+++ b/mistralclient/openstack/common/apiclient/base.py
@@ -0,0 +1,493 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack Foundation
+# Copyright 2012 Grid Dynamics
+# Copyright 2013 OpenStack Foundation
+# 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.
+"""
+
+# E1102: %s is not callable
+# pylint: disable=E1102
+
+import abc
+import copy
+
+import six
+from six.moves.urllib import parse
+
+from mistralclient.openstack.common.apiclient import exceptions
+from mistralclient.openstack.common import strutils
+
+
+def getid(obj):
+    """Return id if argument is a Resource.
+
+    Abstracts the common pattern of allowing both an object or an object's ID
+    (UUID) as a parameter when dealing with relationships.
+    """
+    try:
+        if obj.uuid:
+            return obj.uuid
+    except AttributeError:
+        pass
+    try:
+        return obj.id
+    except AttributeError:
+        return obj
+
+
+# TODO(aababilov): call run_hooks() in HookableMixin's child classes
+class HookableMixin(object):
+    """Mixin so classes can register and run hooks."""
+    _hooks_map = {}
+
+    @classmethod
+    def add_hook(cls, hook_type, hook_func):
+        """Add a new hook of specified type.
+
+        :param cls: class that registers hooks
+        :param hook_type: hook type, e.g., '__pre_parse_args__'
+        :param hook_func: hook function
+        """
+        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):
+        """Run all hooks of specified type.
+
+        :param cls: class that registers hooks
+        :param hook_type: hook type, e.g., '__pre_parse_args__'
+        :param **args: args to be passed to every hook function
+        :param **kwargs: kwargs to be passed to every hook function
+        """
+        hook_funcs = cls._hooks_map.get(hook_type) or []
+        for hook_func in hook_funcs:
+            hook_func(*args, **kwargs)
+
+
+class BaseManager(HookableMixin):
+    """Basic manager type providing common operations.
+
+    Managers interact with a particular type of API (servers, flavors, images,
+    etc.) and provide CRUD operations for them.
+    """
+    resource_class = None
+
+    def __init__(self, client):
+        """Initializes BaseManager with `client`.
+
+        :param client: instance of BaseClient descendant for HTTP requests
+        """
+        super(BaseManager, self).__init__()
+        self.client = client
+
+    def _list(self, url, response_key, obj_class=None, json=None):
+        """List the collection.
+
+        :param url: a partial URL, e.g., '/servers'
+        :param response_key: the key to be looked up in response dictionary,
+            e.g., 'servers'
+        :param obj_class: class for constructing the returned objects
+            (self.resource_class will be used by default)
+        :param json: data that will be encoded as JSON and passed in POST
+            request (GET will be sent by default)
+        """
+        if json:
+            body = self.client.post(url, json=json).json()
+        else:
+            body = self.client.get(url).json()
+
+        if obj_class is None:
+            obj_class = self.resource_class
+
+        data = body[response_key]
+        # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
+        #           unlike other services which just return the list...
+        try:
+            data = data['values']
+        except (KeyError, TypeError):
+            pass
+
+        return [obj_class(self, res, loaded=True) for res in data if res]
+
+    def _get(self, url, response_key):
+        """Get an object from collection.
+
+        :param url: a partial URL, e.g., '/servers'
+        :param response_key: the key to be looked up in response dictionary,
+            e.g., 'server'
+        """
+        body = self.client.get(url).json()
+        return self.resource_class(self, body[response_key], loaded=True)
+
+    def _head(self, url):
+        """Retrieve request headers for an object.
+
+        :param url: a partial URL, e.g., '/servers'
+        """
+        resp = self.client.head(url)
+        return resp.status_code == 204
+
+    def _post(self, url, json, response_key, return_raw=False):
+        """Create an object.
+
+        :param url: a partial URL, e.g., '/servers'
+        :param json: data that will be encoded as JSON and passed in POST
+            request (GET will be sent by default)
+        :param response_key: the key to be looked up in response dictionary,
+            e.g., 'servers'
+        :param return_raw: flag to force returning raw JSON instead of
+            Python object of self.resource_class
+        """
+        body = self.client.post(url, json=json).json()
+        if return_raw:
+            return body[response_key]
+        return self.resource_class(self, body[response_key])
+
+    def _put(self, url, json=None, response_key=None):
+        """Update an object with PUT method.
+
+        :param url: a partial URL, e.g., '/servers'
+        :param json: data that will be encoded as JSON and passed in POST
+            request (GET will be sent by default)
+        :param response_key: the key to be looked up in response dictionary,
+            e.g., 'servers'
+        """
+        resp = self.client.put(url, json=json)
+        # PUT requests may not return a body
+        if resp.content:
+            body = resp.json()
+            if response_key is not None:
+                return self.resource_class(self, body[response_key])
+            else:
+                return self.resource_class(self, body)
+
+    def _patch(self, url, json=None, response_key=None):
+        """Update an object with PATCH method.
+
+        :param url: a partial URL, e.g., '/servers'
+        :param json: data that will be encoded as JSON and passed in POST
+            request (GET will be sent by default)
+        :param response_key: the key to be looked up in response dictionary,
+            e.g., 'servers'
+        """
+        body = self.client.patch(url, json=json).json()
+        if response_key is not None:
+            return self.resource_class(self, body[response_key])
+        else:
+            return self.resource_class(self, body)
+
+    def _delete(self, url):
+        """Delete an object.
+
+        :param url: a partial URL, e.g., '/servers/my-server'
+        """
+        return self.client.delete(url)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ManagerWithFind(BaseManager):
+    """Manager with additional `find()`/`findall()` methods."""
+
+    @abc.abstractmethod
+    def list(self):
+        pass
+
+    def find(self, **kwargs):
+        """Find a single item with attributes matching ``**kwargs``.
+
+        This isn't very efficient: it loads the entire list then filters on
+        the Python side.
+        """
+        matches = self.findall(**kwargs)
+        num_matches = len(matches)
+        if num_matches == 0:
+            msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+            raise exceptions.NotFound(msg)
+        elif num_matches > 1:
+            raise exceptions.NoUniqueMatch()
+        else:
+            return matches[0]
+
+    def findall(self, **kwargs):
+        """Find all items with attributes matching ``**kwargs``.
+
+        This isn't very efficient: it loads the entire list then filters on
+        the Python side.
+        """
+        found = []
+        searches = kwargs.items()
+
+        for obj in self.list():
+            try:
+                if all(getattr(obj, attr) == value
+                       for (attr, value) in searches):
+                    found.append(obj)
+            except AttributeError:
+                continue
+
+        return found
+
+
+class CrudManager(BaseManager):
+    """Base manager class for manipulating entities.
+
+    Children of this class are expected to define a `collection_key` and `key`.
+
+    - `collection_key`: Usually a plural noun by convention (e.g. `entities`);
+      used to refer collections in both URL's (e.g.  `/v3/entities`) and JSON
+      objects containing a list of member resources (e.g. `{'entities': [{},
+      {}, {}]}`).
+    - `key`: Usually a singular noun by convention (e.g. `entity`); used to
+      refer to an individual member of the collection.
+
+    """
+    collection_key = None
+    key = None
+
+    def build_url(self, base_url=None, **kwargs):
+        """Builds a resource URL for the given kwargs.
+
+        Given an example collection where `collection_key = 'entities'` and
+        `key = 'entity'`, the following URL's could be generated.
+
+        By default, the URL will represent a collection of entities, e.g.::
+
+            /entities
+
+        If kwargs contains an `entity_id`, then the URL will represent a
+        specific member, e.g.::
+
+            /entities/{entity_id}
+
+        :param base_url: if provided, the generated URL will be appended to it
+        """
+        url = base_url if base_url is not None else ''
+
+        url += '/%s' % self.collection_key
+
+        # do we have a specific entity?
+        entity_id = kwargs.get('%s_id' % self.key)
+        if entity_id is not None:
+            url += '/%s' % entity_id
+
+        return url
+
+    def _filter_kwargs(self, kwargs):
+        """Drop null values and handle ids."""
+        for key, ref in six.iteritems(kwargs.copy()):
+            if ref is None:
+                kwargs.pop(key)
+            else:
+                if isinstance(ref, Resource):
+                    kwargs.pop(key)
+                    kwargs['%s_id' % key] = getid(ref)
+        return kwargs
+
+    def create(self, **kwargs):
+        kwargs = self._filter_kwargs(kwargs)
+        return self._post(
+            self.build_url(**kwargs),
+            {self.key: kwargs},
+            self.key)
+
+    def get(self, **kwargs):
+        kwargs = self._filter_kwargs(kwargs)
+        return self._get(
+            self.build_url(**kwargs),
+            self.key)
+
+    def head(self, **kwargs):
+        kwargs = self._filter_kwargs(kwargs)
+        return self._head(self.build_url(**kwargs))
+
+    def list(self, base_url=None, **kwargs):
+        """List the collection.
+
+        :param base_url: if provided, the generated URL will be appended to it
+        """
+        kwargs = self._filter_kwargs(kwargs)
+
+        return self._list(
+            '%(base_url)s%(query)s' % {
+                'base_url': self.build_url(base_url=base_url, **kwargs),
+                'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+            },
+            self.collection_key)
+
+    def put(self, base_url=None, **kwargs):
+        """Update an element.
+
+        :param base_url: if provided, the generated URL will be appended to it
+        """
+        kwargs = self._filter_kwargs(kwargs)
+
+        return self._put(self.build_url(base_url=base_url, **kwargs))
+
+    def update(self, **kwargs):
+        kwargs = self._filter_kwargs(kwargs)
+        params = kwargs.copy()
+        params.pop('%s_id' % self.key)
+
+        return self._patch(
+            self.build_url(**kwargs),
+            {self.key: params},
+            self.key)
+
+    def delete(self, **kwargs):
+        kwargs = self._filter_kwargs(kwargs)
+
+        return self._delete(
+            self.build_url(**kwargs))
+
+    def find(self, base_url=None, **kwargs):
+        """Find a single item with attributes matching ``**kwargs``.
+
+        :param base_url: if provided, the generated URL will be appended to it
+        """
+        kwargs = self._filter_kwargs(kwargs)
+
+        rl = self._list(
+            '%(base_url)s%(query)s' % {
+                'base_url': self.build_url(base_url=base_url, **kwargs),
+                'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+            },
+            self.collection_key)
+        num = len(rl)
+
+        if num == 0:
+            msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+            raise exceptions.NotFound(404, msg)
+        elif num > 1:
+            raise exceptions.NoUniqueMatch
+        else:
+            return rl[0]
+
+
+class Extension(HookableMixin):
+    """Extension descriptor."""
+
+    SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
+    manager_class = None
+
+    def __init__(self, name, module):
+        super(Extension, self).__init__()
+        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)
+            else:
+                try:
+                    if issubclass(attr_value, BaseManager):
+                        self.manager_class = attr_value
+                except TypeError:
+                    pass
+
+    def __repr__(self):
+        return "<Extension '%s'>" % self.name
+
+
+class Resource(object):
+    """Base class for OpenStack resources (tenant, user, etc.).
+
+    This is pretty much just a bag for attributes.
+    """
+
+    HUMAN_ID = False
+    NAME_ATTR = 'name'
+
+    def __init__(self, manager, info, loaded=False):
+        """Populate and bind to a manager.
+
+        :param manager: BaseManager object
+        :param info: dictionary representing resource attributes
+        :param loaded: prevent lazy-loading if set to True
+        """
+        self.manager = manager
+        self._info = info
+        self._add_details(info)
+        self._loaded = loaded
+
+    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)
+
+    @property
+    def human_id(self):
+        """Human-readable ID which can be used for bash completion.
+        """
+        if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
+            return strutils.to_slug(getattr(self, self.NAME_ATTR))
+        return None
+
+    def _add_details(self, info):
+        for (k, v) in six.iteritems(info):
+            try:
+                setattr(self, k, v)
+                self._info[k] = v
+            except AttributeError:
+                # In this case we already defined the attribute on the class
+                pass
+
+    def __getattr__(self, k):
+        if k not in self.__dict__:
+            #NOTE(bcwaldon): disallow lazy-loading if already loaded once
+            if not self.is_loaded:
+                self._get()
+                return self.__getattr__(k)
+
+            raise AttributeError(k)
+        else:
+            return self.__dict__[k]
+
+    def _get(self):
+        # set _loaded first ... so if we have to bail, we know we tried.
+        self._loaded = True
+        if not hasattr(self.manager, 'get'):
+            return
+
+        new = self.manager.get(self.id)
+        if new:
+            self._add_details(new._info)
+
+    def __eq__(self, other):
+        if not isinstance(other, Resource):
+            return NotImplemented
+        # two resources of different types are not equal
+        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
+
+    @property
+    def is_loaded(self):
+        return self._loaded
+
+    def to_dict(self):
+        return copy.deepcopy(self._info)
diff --git a/mistralclient/openstack/common/apiclient/client.py b/mistralclient/openstack/common/apiclient/client.py
new file mode 100644
index 00000000..da407550
--- /dev/null
+++ b/mistralclient/openstack/common/apiclient/client.py
@@ -0,0 +1,358 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack Foundation
+# Copyright 2011 Piston Cloud Computing, Inc.
+# Copyright 2013 Alessio Ababilov
+# Copyright 2013 Grid Dynamics
+# Copyright 2013 OpenStack Foundation
+# 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.
+
+"""
+OpenStack Client interface. Handles the REST calls and responses.
+"""
+
+# E0202: An attribute inherited from %s hide this method
+# pylint: disable=E0202
+
+import logging
+import time
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+import requests
+
+from mistralclient.openstack.common.apiclient import exceptions
+from mistralclient.openstack.common import importutils
+
+
+_logger = logging.getLogger(__name__)
+
+
+class HTTPClient(object):
+    """This client handles sending HTTP requests to OpenStack servers.
+
+    Features:
+    - share authentication information between several clients to different
+      services (e.g., for compute and image clients);
+    - reissue authentication request for expired tokens;
+    - encode/decode JSON bodies;
+    - raise exceptions on HTTP errors;
+    - pluggable authentication;
+    - store authentication information in a keyring;
+    - store time spent for requests;
+    - register clients for particular services, so one can use
+      `http_client.identity` or `http_client.compute`;
+    - log requests and responses in a format that is easy to copy-and-paste
+      into terminal and send the same request with curl.
+    """
+
+    user_agent = "mistralclient.openstack.common.apiclient"
+
+    def __init__(self,
+                 auth_plugin,
+                 region_name=None,
+                 endpoint_type="publicURL",
+                 original_ip=None,
+                 verify=True,
+                 cert=None,
+                 timeout=None,
+                 timings=False,
+                 keyring_saver=None,
+                 debug=False,
+                 user_agent=None,
+                 http=None):
+        self.auth_plugin = auth_plugin
+
+        self.endpoint_type = endpoint_type
+        self.region_name = region_name
+
+        self.original_ip = original_ip
+        self.timeout = timeout
+        self.verify = verify
+        self.cert = cert
+
+        self.keyring_saver = keyring_saver
+        self.debug = debug
+        self.user_agent = user_agent or self.user_agent
+
+        self.times = []  # [("item", starttime, endtime), ...]
+        self.timings = timings
+
+        # requests within the same session can reuse TCP connections from pool
+        self.http = http or requests.Session()
+
+        self.cached_token = None
+
+    def _http_log_req(self, method, url, kwargs):
+        if not self.debug:
+            return
+
+        string_parts = [
+            "curl -i",
+            "-X '%s'" % method,
+            "'%s'" % url,
+        ]
+
+        for element in kwargs['headers']:
+            header = "-H '%s: %s'" % (element, kwargs['headers'][element])
+            string_parts.append(header)
+
+        _logger.debug("REQ: %s" % " ".join(string_parts))
+        if 'data' in kwargs:
+            _logger.debug("REQ BODY: %s\n" % (kwargs['data']))
+
+    def _http_log_resp(self, resp):
+        if not self.debug:
+            return
+        _logger.debug(
+            "RESP: [%s] %s\n",
+            resp.status_code,
+            resp.headers)
+        if resp._content_consumed:
+            _logger.debug(
+                "RESP BODY: %s\n",
+                resp.text)
+
+    def serialize(self, kwargs):
+        if kwargs.get('json') is not None:
+            kwargs['headers']['Content-Type'] = 'application/json'
+            kwargs['data'] = json.dumps(kwargs['json'])
+        try:
+            del kwargs['json']
+        except KeyError:
+            pass
+
+    def get_timings(self):
+        return self.times
+
+    def reset_timings(self):
+        self.times = []
+
+    def request(self, method, url, **kwargs):
+        """Send an http request with the specified characteristics.
+
+        Wrapper around `requests.Session.request` to handle tasks such as
+        setting headers, JSON encoding/decoding, and error handling.
+
+        :param method: method of HTTP request
+        :param url: URL of HTTP request
+        :param kwargs: any other parameter that can be passed to
+'            requests.Session.request (such as `headers`) or `json`
+             that will be encoded as JSON and used as `data` argument
+        """
+        kwargs.setdefault("headers", kwargs.get("headers", {}))
+        kwargs["headers"]["User-Agent"] = self.user_agent
+        if self.original_ip:
+            kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
+                self.original_ip, self.user_agent)
+        if self.timeout is not None:
+            kwargs.setdefault("timeout", self.timeout)
+        kwargs.setdefault("verify", self.verify)
+        if self.cert is not None:
+            kwargs.setdefault("cert", self.cert)
+        self.serialize(kwargs)
+
+        self._http_log_req(method, url, kwargs)
+        if self.timings:
+            start_time = time.time()
+        resp = self.http.request(method, url, **kwargs)
+        if self.timings:
+            self.times.append(("%s %s" % (method, url),
+                               start_time, time.time()))
+        self._http_log_resp(resp)
+
+        if resp.status_code >= 400:
+            _logger.debug(
+                "Request returned failure status: %s",
+                resp.status_code)
+            raise exceptions.from_response(resp, method, url)
+
+        return resp
+
+    @staticmethod
+    def concat_url(endpoint, url):
+        """Concatenate endpoint and final URL.
+
+        E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
+        "http://keystone/v2.0/tokens".
+
+        :param endpoint: the base URL
+        :param url: the final URL
+        """
+        return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
+
+    def client_request(self, client, method, url, **kwargs):
+        """Send an http request using `client`'s endpoint and specified `url`.
+
+        If request was rejected as unauthorized (possibly because the token is
+        expired), issue one authorization attempt and send the request once
+        again.
+
+        :param client: instance of BaseClient descendant
+        :param method: method of HTTP request
+        :param url: URL of HTTP request
+        :param kwargs: any other parameter that can be passed to
+'            `HTTPClient.request`
+        """
+
+        filter_args = {
+            "endpoint_type": client.endpoint_type or self.endpoint_type,
+            "service_type": client.service_type,
+        }
+        token, endpoint = (self.cached_token, client.cached_endpoint)
+        just_authenticated = False
+        if not (token and endpoint):
+            try:
+                token, endpoint = self.auth_plugin.token_and_endpoint(
+                    **filter_args)
+            except exceptions.EndpointException:
+                pass
+            if not (token and endpoint):
+                self.authenticate()
+                just_authenticated = True
+                token, endpoint = self.auth_plugin.token_and_endpoint(
+                    **filter_args)
+                if not (token and endpoint):
+                    raise exceptions.AuthorizationFailure(
+                        "Cannot find endpoint or token for request")
+
+        old_token_endpoint = (token, endpoint)
+        kwargs.setdefault("headers", {})["X-Auth-Token"] = token
+        self.cached_token = token
+        client.cached_endpoint = endpoint
+        # Perform the request once. If we get Unauthorized, then it
+        # might be because the auth token expired, so try to
+        # re-authenticate and try again. If it still fails, bail.
+        try:
+            return self.request(
+                method, self.concat_url(endpoint, url), **kwargs)
+        except exceptions.Unauthorized as unauth_ex:
+            if just_authenticated:
+                raise
+            self.cached_token = None
+            client.cached_endpoint = None
+            self.authenticate()
+            try:
+                token, endpoint = self.auth_plugin.token_and_endpoint(
+                    **filter_args)
+            except exceptions.EndpointException:
+                raise unauth_ex
+            if (not (token and endpoint) or
+                    old_token_endpoint == (token, endpoint)):
+                raise unauth_ex
+            self.cached_token = token
+            client.cached_endpoint = endpoint
+            kwargs["headers"]["X-Auth-Token"] = token
+            return self.request(
+                method, self.concat_url(endpoint, url), **kwargs)
+
+    def add_client(self, base_client_instance):
+        """Add a new instance of :class:`BaseClient` descendant.
+
+        `self` will store a reference to `base_client_instance`.
+
+        Example:
+
+        >>> def test_clients():
+        ...     from keystoneclient.auth import keystone
+        ...     from openstack.common.apiclient import client
+        ...     auth = keystone.KeystoneAuthPlugin(
+        ...         username="user", password="pass", tenant_name="tenant",
+        ...         auth_url="http://auth:5000/v2.0")
+        ...     openstack_client = client.HTTPClient(auth)
+        ...     # create nova client
+        ...     from novaclient.v1_1 import client
+        ...     client.Client(openstack_client)
+        ...     # create keystone client
+        ...     from keystoneclient.v2_0 import client
+        ...     client.Client(openstack_client)
+        ...     # use them
+        ...     openstack_client.identity.tenants.list()
+        ...     openstack_client.compute.servers.list()
+        """
+        service_type = base_client_instance.service_type
+        if service_type and not hasattr(self, service_type):
+            setattr(self, service_type, base_client_instance)
+
+    def authenticate(self):
+        self.auth_plugin.authenticate(self)
+        # Store the authentication results in the keyring for later requests
+        if self.keyring_saver:
+            self.keyring_saver.save(self)
+
+
+class BaseClient(object):
+    """Top-level object to access the OpenStack API.
+
+    This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
+    will handle a bunch of issues such as authentication.
+    """
+
+    service_type = None
+    endpoint_type = None  # "publicURL" will be used
+    cached_endpoint = None
+
+    def __init__(self, http_client, extensions=None):
+        self.http_client = http_client
+        http_client.add_client(self)
+
+        # Add in any extensions...
+        if extensions:
+            for extension in extensions:
+                if extension.manager_class:
+                    setattr(self, extension.name,
+                            extension.manager_class(self))
+
+    def client_request(self, method, url, **kwargs):
+        return self.http_client.client_request(
+            self, method, url, **kwargs)
+
+    def head(self, url, **kwargs):
+        return self.client_request("HEAD", url, **kwargs)
+
+    def get(self, url, **kwargs):
+        return self.client_request("GET", url, **kwargs)
+
+    def post(self, url, **kwargs):
+        return self.client_request("POST", url, **kwargs)
+
+    def put(self, url, **kwargs):
+        return self.client_request("PUT", url, **kwargs)
+
+    def delete(self, url, **kwargs):
+        return self.client_request("DELETE", url, **kwargs)
+
+    def patch(self, url, **kwargs):
+        return self.client_request("PATCH", url, **kwargs)
+
+    @staticmethod
+    def get_class(api_name, version, version_map):
+        """Returns the client class for the requested API version
+
+        :param api_name: the name of the API, e.g. 'compute', 'image', etc
+        :param version: the requested API version
+        :param version_map: a dict of client classes keyed by version
+        :rtype: a client class for the requested API version
+        """
+        try:
+            client_path = version_map[str(version)]
+        except (KeyError, ValueError):
+            msg = "Invalid %s client version '%s'. must be one of: %s" % (
+                  (api_name, version, ', '.join(version_map.keys())))
+            raise exceptions.UnsupportedVersion(msg)
+
+        return importutils.import_class(client_path)
diff --git a/mistralclient/openstack/common/apiclient/exceptions.py b/mistralclient/openstack/common/apiclient/exceptions.py
new file mode 100644
index 00000000..ada1344f
--- /dev/null
+++ b/mistralclient/openstack/common/apiclient/exceptions.py
@@ -0,0 +1,459 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 Nebula, Inc.
+# Copyright 2013 Alessio Ababilov
+# Copyright 2013 OpenStack Foundation
+# 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.
+
+"""
+Exception definitions.
+"""
+
+import inspect
+import sys
+
+import six
+
+
+class ClientException(Exception):
+    """The base exception class for all exceptions this library raises.
+    """
+    pass
+
+
+class MissingArgs(ClientException):
+    """Supplied arguments are not sufficient for calling a function."""
+    def __init__(self, missing):
+        self.missing = missing
+        msg = "Missing argument(s): %s" % ", ".join(missing)
+        super(MissingArgs, self).__init__(msg)
+
+
+class ValidationError(ClientException):
+    """Error in validation on API client side."""
+    pass
+
+
+class UnsupportedVersion(ClientException):
+    """User is trying to use an unsupported version of the API."""
+    pass
+
+
+class CommandError(ClientException):
+    """Error in CLI tool."""
+    pass
+
+
+class AuthorizationFailure(ClientException):
+    """Cannot authorize API client."""
+    pass
+
+
+class ConnectionRefused(ClientException):
+    """Cannot connect to API service."""
+    pass
+
+
+class AuthPluginOptionsMissing(AuthorizationFailure):
+    """Auth plugin misses some options."""
+    def __init__(self, opt_names):
+        super(AuthPluginOptionsMissing, self).__init__(
+            "Authentication failed. Missing options: %s" %
+            ", ".join(opt_names))
+        self.opt_names = opt_names
+
+
+class AuthSystemNotFound(AuthorizationFailure):
+    """User has specified a AuthSystem that is not installed."""
+    def __init__(self, auth_system):
+        super(AuthSystemNotFound, self).__init__(
+            "AuthSystemNotFound: %s" % repr(auth_system))
+        self.auth_system = auth_system
+
+
+class NoUniqueMatch(ClientException):
+    """Multiple entities found instead of one."""
+    pass
+
+
+class EndpointException(ClientException):
+    """Something is rotten in Service Catalog."""
+    pass
+
+
+class EndpointNotFound(EndpointException):
+    """Could not find requested endpoint in Service Catalog."""
+    pass
+
+
+class AmbiguousEndpoints(EndpointException):
+    """Found more than one matching endpoint in Service Catalog."""
+    def __init__(self, endpoints=None):
+        super(AmbiguousEndpoints, self).__init__(
+            "AmbiguousEndpoints: %s" % repr(endpoints))
+        self.endpoints = endpoints
+
+
+class HttpError(ClientException):
+    """The base exception class for all HTTP exceptions.
+    """
+    http_status = 0
+    message = "HTTP Error"
+
+    def __init__(self, message=None, details=None,
+                 response=None, request_id=None,
+                 url=None, method=None, http_status=None):
+        self.http_status = http_status or self.http_status
+        self.message = message or self.message
+        self.details = details
+        self.request_id = request_id
+        self.response = response
+        self.url = url
+        self.method = method
+        formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
+        if request_id:
+            formatted_string += " (Request-ID: %s)" % request_id
+        super(HttpError, self).__init__(formatted_string)
+
+
+class HTTPRedirection(HttpError):
+    """HTTP Redirection."""
+    message = "HTTP Redirection"
+
+
+class HTTPClientError(HttpError):
+    """Client-side HTTP error.
+
+    Exception for cases in which the client seems to have erred.
+    """
+    message = "HTTP Client Error"
+
+
+class HttpServerError(HttpError):
+    """Server-side HTTP error.
+
+    Exception for cases in which the server is aware that it has
+    erred or is incapable of performing the request.
+    """
+    message = "HTTP Server Error"
+
+
+class MultipleChoices(HTTPRedirection):
+    """HTTP 300 - Multiple Choices.
+
+    Indicates multiple options for the resource that the client may follow.
+    """
+
+    http_status = 300
+    message = "Multiple Choices"
+
+
+class BadRequest(HTTPClientError):
+    """HTTP 400 - Bad Request.
+
+    The request cannot be fulfilled due to bad syntax.
+    """
+    http_status = 400
+    message = "Bad Request"
+
+
+class Unauthorized(HTTPClientError):
+    """HTTP 401 - Unauthorized.
+
+    Similar to 403 Forbidden, but specifically for use when authentication
+    is required and has failed or has not yet been provided.
+    """
+    http_status = 401
+    message = "Unauthorized"
+
+
+class PaymentRequired(HTTPClientError):
+    """HTTP 402 - Payment Required.
+
+    Reserved for future use.
+    """
+    http_status = 402
+    message = "Payment Required"
+
+
+class Forbidden(HTTPClientError):
+    """HTTP 403 - Forbidden.
+
+    The request was a valid request, but the server is refusing to respond
+    to it.
+    """
+    http_status = 403
+    message = "Forbidden"
+
+
+class NotFound(HTTPClientError):
+    """HTTP 404 - Not Found.
+
+    The requested resource could not be found but may be available again
+    in the future.
+    """
+    http_status = 404
+    message = "Not Found"
+
+
+class MethodNotAllowed(HTTPClientError):
+    """HTTP 405 - Method Not Allowed.
+
+    A request was made of a resource using a request method not supported
+    by that resource.
+    """
+    http_status = 405
+    message = "Method Not Allowed"
+
+
+class NotAcceptable(HTTPClientError):
+    """HTTP 406 - Not Acceptable.
+
+    The requested resource is only capable of generating content not
+    acceptable according to the Accept headers sent in the request.
+    """
+    http_status = 406
+    message = "Not Acceptable"
+
+
+class ProxyAuthenticationRequired(HTTPClientError):
+    """HTTP 407 - Proxy Authentication Required.
+
+    The client must first authenticate itself with the proxy.
+    """
+    http_status = 407
+    message = "Proxy Authentication Required"
+
+
+class RequestTimeout(HTTPClientError):
+    """HTTP 408 - Request Timeout.
+
+    The server timed out waiting for the request.
+    """
+    http_status = 408
+    message = "Request Timeout"
+
+
+class Conflict(HTTPClientError):
+    """HTTP 409 - Conflict.
+
+    Indicates that the request could not be processed because of conflict
+    in the request, such as an edit conflict.
+    """
+    http_status = 409
+    message = "Conflict"
+
+
+class Gone(HTTPClientError):
+    """HTTP 410 - Gone.
+
+    Indicates that the resource requested is no longer available and will
+    not be available again.
+    """
+    http_status = 410
+    message = "Gone"
+
+
+class LengthRequired(HTTPClientError):
+    """HTTP 411 - Length Required.
+
+    The request did not specify the length of its content, which is
+    required by the requested resource.
+    """
+    http_status = 411
+    message = "Length Required"
+
+
+class PreconditionFailed(HTTPClientError):
+    """HTTP 412 - Precondition Failed.
+
+    The server does not meet one of the preconditions that the requester
+    put on the request.
+    """
+    http_status = 412
+    message = "Precondition Failed"
+
+
+class RequestEntityTooLarge(HTTPClientError):
+    """HTTP 413 - Request Entity Too Large.
+
+    The request is larger than the server is willing or able to process.
+    """
+    http_status = 413
+    message = "Request Entity Too Large"
+
+    def __init__(self, *args, **kwargs):
+        try:
+            self.retry_after = int(kwargs.pop('retry_after'))
+        except (KeyError, ValueError):
+            self.retry_after = 0
+
+        super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
+
+
+class RequestUriTooLong(HTTPClientError):
+    """HTTP 414 - Request-URI Too Long.
+
+    The URI provided was too long for the server to process.
+    """
+    http_status = 414
+    message = "Request-URI Too Long"
+
+
+class UnsupportedMediaType(HTTPClientError):
+    """HTTP 415 - Unsupported Media Type.
+
+    The request entity has a media type which the server or resource does
+    not support.
+    """
+    http_status = 415
+    message = "Unsupported Media Type"
+
+
+class RequestedRangeNotSatisfiable(HTTPClientError):
+    """HTTP 416 - Requested Range Not Satisfiable.
+
+    The client has asked for a portion of the file, but the server cannot
+    supply that portion.
+    """
+    http_status = 416
+    message = "Requested Range Not Satisfiable"
+
+
+class ExpectationFailed(HTTPClientError):
+    """HTTP 417 - Expectation Failed.
+
+    The server cannot meet the requirements of the Expect request-header field.
+    """
+    http_status = 417
+    message = "Expectation Failed"
+
+
+class UnprocessableEntity(HTTPClientError):
+    """HTTP 422 - Unprocessable Entity.
+
+    The request was well-formed but was unable to be followed due to semantic
+    errors.
+    """
+    http_status = 422
+    message = "Unprocessable Entity"
+
+
+class InternalServerError(HttpServerError):
+    """HTTP 500 - Internal Server Error.
+
+    A generic error message, given when no more specific message is suitable.
+    """
+    http_status = 500
+    message = "Internal Server Error"
+
+
+# NotImplemented is a python keyword.
+class HttpNotImplemented(HttpServerError):
+    """HTTP 501 - Not Implemented.
+
+    The server either does not recognize the request method, or it lacks
+    the ability to fulfill the request.
+    """
+    http_status = 501
+    message = "Not Implemented"
+
+
+class BadGateway(HttpServerError):
+    """HTTP 502 - Bad Gateway.
+
+    The server was acting as a gateway or proxy and received an invalid
+    response from the upstream server.
+    """
+    http_status = 502
+    message = "Bad Gateway"
+
+
+class ServiceUnavailable(HttpServerError):
+    """HTTP 503 - Service Unavailable.
+
+    The server is currently unavailable.
+    """
+    http_status = 503
+    message = "Service Unavailable"
+
+
+class GatewayTimeout(HttpServerError):
+    """HTTP 504 - Gateway Timeout.
+
+    The server was acting as a gateway or proxy and did not receive a timely
+    response from the upstream server.
+    """
+    http_status = 504
+    message = "Gateway Timeout"
+
+
+class HttpVersionNotSupported(HttpServerError):
+    """HTTP 505 - HttpVersion Not Supported.
+
+    The server does not support the HTTP protocol version used in the request.
+    """
+    http_status = 505
+    message = "HTTP Version Not Supported"
+
+
+# _code_map contains all the classes that have http_status attribute.
+_code_map = dict(
+    (getattr(obj, 'http_status', None), obj)
+    for name, obj in six.iteritems(vars(sys.modules[__name__]))
+    if inspect.isclass(obj) and getattr(obj, 'http_status', False)
+)
+
+
+def from_response(response, method, url):
+    """Returns an instance of :class:`HttpError` or subclass based on response.
+
+    :param response: instance of `requests.Response` class
+    :param method: HTTP method used for request
+    :param url: URL used for request
+    """
+    kwargs = {
+        "http_status": response.status_code,
+        "response": response,
+        "method": method,
+        "url": url,
+        "request_id": response.headers.get("x-compute-request-id"),
+    }
+    if "retry-after" in response.headers:
+        kwargs["retry_after"] = response.headers["retry-after"]
+
+    content_type = response.headers.get("Content-Type", "")
+    if content_type.startswith("application/json"):
+        try:
+            body = response.json()
+        except ValueError:
+            pass
+        else:
+            if isinstance(body, dict):
+                error = list(body.values())[0]
+                kwargs["message"] = error.get("message")
+                kwargs["details"] = error.get("details")
+    elif content_type.startswith("text/"):
+        kwargs["details"] = response.text
+
+    try:
+        cls = _code_map[response.status_code]
+    except KeyError:
+        if 500 <= response.status_code < 600:
+            cls = HttpServerError
+        elif 400 <= response.status_code < 500:
+            cls = HTTPClientError
+        else:
+            cls = HttpError
+    return cls(**kwargs)
diff --git a/mistralclient/openstack/common/apiclient/fake_client.py b/mistralclient/openstack/common/apiclient/fake_client.py
new file mode 100644
index 00000000..5f08790f
--- /dev/null
+++ b/mistralclient/openstack/common/apiclient/fake_client.py
@@ -0,0 +1,173 @@
+# Copyright 2013 OpenStack Foundation
+# 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.
+
+"""
+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.
+"""
+
+# W0102: Dangerous default value %s as argument
+# pylint: disable=W0102
+
+import json
+
+import requests
+import six
+from six.moves.urllib import parse
+
+from mistralclient.openstack.common.apiclient import client
+
+
+def assert_has_keys(dct, required=[], optional=[]):
+    for k in required:
+        try:
+            assert k in dct
+        except AssertionError:
+            extra_keys = set(dct.keys()).difference(set(required + optional))
+            raise AssertionError("found unexpected keys: %s" %
+                                 list(extra_keys))
+
+
+class TestResponse(requests.Response):
+    """Wrap requests.Response and provide a convenient initialization.
+    """
+
+    def __init__(self, data):
+        super(TestResponse, self).__init__()
+        self._content_consumed = True
+        if isinstance(data, dict):
+            self.status_code = data.get('status_code', 200)
+            # Fake the text attribute to streamline Response creation
+            text = data.get('text', "")
+            if isinstance(text, (dict, list)):
+                self._content = json.dumps(text)
+                default_headers = {
+                    "Content-Type": "application/json",
+                }
+            else:
+                self._content = text
+                default_headers = {}
+            if six.PY3 and isinstance(self._content, six.string_types):
+                self._content = self._content.encode('utf-8', 'strict')
+            self.headers = data.get('headers') or default_headers
+        else:
+            self.status_code = data
+
+    def __eq__(self, other):
+        return (self.status_code == other.status_code and
+                self.headers == other.headers and
+                self._content == other._content)
+
+
+class FakeHTTPClient(client.HTTPClient):
+
+    def __init__(self, *args, **kwargs):
+        self.callstack = []
+        self.fixtures = kwargs.pop("fixtures", None) or {}
+        if not args and not "auth_plugin" in kwargs:
+            args = (None, )
+        super(FakeHTTPClient, self).__init__(*args, **kwargs)
+
+    def assert_called(self, method, url, body=None, pos=-1):
+        """Assert than an API method was just called.
+        """
+        expected = (method, url)
+        called = self.callstack[pos][0:2]
+        assert self.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:
+            if self.callstack[pos][3] != body:
+                raise AssertionError('%r != %r' %
+                                     (self.callstack[pos][3], body))
+
+    def assert_called_anytime(self, method, url, body=None):
+        """Assert than an API method was called anytime in the test.
+        """
+        expected = (method, url)
+
+        assert self.callstack, \
+            "Expected %s %s but no calls were made." % expected
+
+        found = False
+        entry = None
+        for entry in self.callstack:
+            if expected == entry[0:2]:
+                found = True
+                break
+
+        assert found, 'Expected %s %s; got %s' % \
+            (method, url, self.callstack)
+        if body is not None:
+            assert entry[3] == body, "%s != %s" % (entry[3], body)
+
+        self.callstack = []
+
+    def clear_callstack(self):
+        self.callstack = []
+
+    def authenticate(self):
+        pass
+
+    def client_request(self, client, method, url, **kwargs):
+        # Check that certain things are called correctly
+        if method in ["GET", "DELETE"]:
+            assert "json" not in kwargs
+
+        # Note the call
+        self.callstack.append(
+            (method,
+             url,
+             kwargs.get("headers") or {},
+             kwargs.get("json") or kwargs.get("data")))
+        try:
+            fixture = self.fixtures[url][method]
+        except KeyError:
+            pass
+        else:
+            return TestResponse({"headers": fixture[0],
+                                 "text": fixture[1]})
+
+        # Call the method
+        args = parse.parse_qsl(parse.urlparse(url)[4])
+        kwargs.update(args)
+        munged_url = url.rsplit('?', 1)[0]
+        munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
+        munged_url = munged_url.replace('-', '_')
+
+        callback = "%s_%s" % (method.lower(), munged_url)
+
+        if not hasattr(self, callback):
+            raise AssertionError('Called unknown API method: %s %s, '
+                                 'expected fakes method name: %s' %
+                                 (method, url, callback))
+
+        resp = getattr(self, callback)(**kwargs)
+        if len(resp) == 3:
+            status, headers, body = resp
+        else:
+            status, body = resp
+            headers = {}
+        return TestResponse({
+            "status_code": status,
+            "text": body,
+            "headers": headers,
+        })
diff --git a/mistralclient/openstack/common/cliutils.py b/mistralclient/openstack/common/cliutils.py
new file mode 100644
index 00000000..cde7ad76
--- /dev/null
+++ b/mistralclient/openstack/common/cliutils.py
@@ -0,0 +1,309 @@
+# Copyright 2012 Red Hat, Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+# W0603: Using the global statement
+# W0621: Redefining name %s from outer scope
+# pylint: disable=W0603,W0621
+
+from __future__ import print_function
+
+import getpass
+import inspect
+import os
+import sys
+import textwrap
+
+import prettytable
+import six
+from six import moves
+
+from mistralclient.openstack.common.apiclient import exceptions
+from mistralclient.openstack.common.gettextutils import _
+from mistralclient.openstack.common import strutils
+from mistralclient.openstack.common import uuidutils
+
+
+def validate_args(fn, *args, **kwargs):
+    """Check that the supplied args are sufficient for calling a function.
+
+    >>> validate_args(lambda a: None)
+    Traceback (most recent call last):
+        ...
+    MissingArgs: Missing argument(s): a
+    >>> validate_args(lambda a, b, c, d: None, 0, c=1)
+    Traceback (most recent call last):
+        ...
+    MissingArgs: Missing argument(s): b, d
+
+    :param fn: the function to check
+    :param arg: the positional arguments supplied
+    :param kwargs: the keyword arguments supplied
+    """
+    argspec = inspect.getargspec(fn)
+
+    num_defaults = len(argspec.defaults or [])
+    required_args = argspec.args[:len(argspec.args) - num_defaults]
+
+    def isbound(method):
+        return getattr(method, 'im_self', None) is not None
+
+    if isbound(fn):
+        required_args.pop(0)
+
+    missing = [arg for arg in required_args if arg not in kwargs]
+    missing = missing[len(args):]
+    if missing:
+        raise exceptions.MissingArgs(missing)
+
+
+def arg(*args, **kwargs):
+    """Decorator for CLI args.
+
+    Example:
+
+    >>> @arg("name", help="Name of the new entity")
+    ... def entity_create(args):
+    ...     pass
+    """
+    def _decorator(func):
+        add_arg(func, *args, **kwargs)
+        return func
+    return _decorator
+
+
+def env(*args, **kwargs):
+    """Returns the first environment variable set.
+
+    If all are empty, defaults to '' or keyword arg `default`.
+    """
+    for arg in args:
+        value = os.environ.get(arg)
+        if value:
+            return value
+    return kwargs.get('default', '')
+
+
+def add_arg(func, *args, **kwargs):
+    """Bind CLI arguments to a shell.py `do_foo` function."""
+
+    if not hasattr(func, 'arguments'):
+        func.arguments = []
+
+    # NOTE(sirp): avoid dups that can occur when the module is shared across
+    # tests.
+    if (args, kwargs) not in func.arguments:
+        # Because of the semantics of decorator composition if we just append
+        # to the options list positional options will appear to be backwards.
+        func.arguments.insert(0, (args, kwargs))
+
+
+def unauthenticated(func):
+    """Adds 'unauthenticated' attribute to decorated function.
+
+    Usage:
+
+    >>> @unauthenticated
+    ... def mymethod(f):
+    ...     pass
+    """
+    func.unauthenticated = True
+    return func
+
+
+def isunauthenticated(func):
+    """Checks if the function does not require authentication.
+
+    Mark such functions with the `@unauthenticated` decorator.
+
+    :returns: bool
+    """
+    return getattr(func, 'unauthenticated', False)
+
+
+def print_list(objs, fields, formatters=None, sortby_index=0,
+               mixed_case_fields=None):
+    """Print a list or objects as a table, one row per object.
+
+    :param objs: iterable of :class:`Resource`
+    :param fields: attributes that correspond to columns, in order
+    :param formatters: `dict` of callables for field formatting
+    :param sortby_index: index of the field for sorting table rows
+    :param mixed_case_fields: fields corresponding to object attributes that
+        have mixed case names (e.g., 'serverId')
+    """
+    formatters = formatters or {}
+    mixed_case_fields = mixed_case_fields or []
+    if sortby_index is None:
+        kwargs = {}
+    else:
+        kwargs = {'sortby': fields[sortby_index]}
+    pt = prettytable.PrettyTable(fields, caching=False)
+    pt.align = 'l'
+
+    for o in objs:
+        row = []
+        for field in fields:
+            if field in formatters:
+                row.append(formatters[field](o))
+            else:
+                if field in mixed_case_fields:
+                    field_name = field.replace(' ', '_')
+                else:
+                    field_name = field.lower().replace(' ', '_')
+                data = getattr(o, field_name, '')
+                row.append(data)
+        pt.add_row(row)
+
+    print(strutils.safe_encode(pt.get_string(**kwargs)))
+
+
+def print_dict(dct, dict_property="Property", wrap=0):
+    """Print a `dict` as a table of two columns.
+
+    :param dct: `dict` to print
+    :param dict_property: name of the first column
+    :param wrap: wrapping for the second column
+    """
+    pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False)
+    pt.align = 'l'
+    for k, v in six.iteritems(dct):
+        # convert dict to str to check length
+        if isinstance(v, dict):
+            v = six.text_type(v)
+        if wrap > 0:
+            v = textwrap.fill(six.text_type(v), wrap)
+        # if value has a newline, add in multiple rows
+        # e.g. fault with stacktrace
+        if v and isinstance(v, six.string_types) and r'\n' in v:
+            lines = v.strip().split(r'\n')
+            col1 = k
+            for line in lines:
+                pt.add_row([col1, line])
+                col1 = ''
+        else:
+            pt.add_row([k, v])
+    print(strutils.safe_encode(pt.get_string()))
+
+
+def get_password(max_password_prompts=3):
+    """Read password from TTY."""
+    verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
+    pw = None
+    if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
+        # Check for Ctrl-D
+        try:
+            for __ in moves.range(max_password_prompts):
+                pw1 = getpass.getpass("OS Password: ")
+                if verify:
+                    pw2 = getpass.getpass("Please verify: ")
+                else:
+                    pw2 = pw1
+                if pw1 == pw2 and pw1:
+                    pw = pw1
+                    break
+        except EOFError:
+            pass
+    return pw
+
+
+def find_resource(manager, name_or_id, **find_args):
+    """Look for resource in a given manager.
+
+    Used as a helper for the _find_* methods.
+    Example:
+
+        def _find_hypervisor(cs, hypervisor):
+            #Get a hypervisor by name or ID.
+            return cliutils.find_resource(cs.hypervisors, hypervisor)
+    """
+    # first try to get entity as integer id
+    try:
+        return manager.get(int(name_or_id))
+    except (TypeError, ValueError, exceptions.NotFound):
+        pass
+
+    # now try to get entity as uuid
+    try:
+        tmp_id = strutils.safe_encode(name_or_id)
+
+        if uuidutils.is_uuid_like(tmp_id):
+            return manager.get(tmp_id)
+    except (TypeError, ValueError, exceptions.NotFound):
+        pass
+
+    # for str id which is not uuid
+    if getattr(manager, 'is_alphanum_id_allowed', False):
+        try:
+            return manager.get(name_or_id)
+        except exceptions.NotFound:
+            pass
+
+    try:
+        try:
+            return manager.find(human_id=name_or_id, **find_args)
+        except exceptions.NotFound:
+            pass
+
+        # finally try to find entity by name
+        try:
+            resource = getattr(manager, 'resource_class', None)
+            name_attr = resource.NAME_ATTR if resource else 'name'
+            kwargs = {name_attr: name_or_id}
+            kwargs.update(find_args)
+            return manager.find(**kwargs)
+        except exceptions.NotFound:
+            msg = _("No %(name)s with a name or "
+                    "ID of '%(name_or_id)s' exists.") % \
+                {
+                    "name": manager.resource_class.__name__.lower(),
+                    "name_or_id": name_or_id
+                }
+            raise exceptions.CommandError(msg)
+    except exceptions.NoUniqueMatch:
+        msg = _("Multiple %(name)s matches found for "
+                "'%(name_or_id)s', use an ID to be more specific.") % \
+            {
+                "name": manager.resource_class.__name__.lower(),
+                "name_or_id": name_or_id
+            }
+        raise exceptions.CommandError(msg)
+
+
+def service_type(stype):
+    """Adds 'service_type' attribute to decorated function.
+
+    Usage:
+        @service_type('volume')
+        def mymethod(f):
+            ...
+    """
+    def inner(f):
+        f.service_type = stype
+        return f
+    return inner
+
+
+def get_service_type(f):
+    """Retrieves service type from function."""
+    return getattr(f, 'service_type', None)
+
+
+def pretty_choice_list(l):
+    return ', '.join("'%s'" % i for i in l)
+
+
+def exit(msg=''):
+    if msg:
+        print (msg, file=sys.stderr)
+    sys.exit(1)
diff --git a/mistralclient/openstack/common/gettextutils.py b/mistralclient/openstack/common/gettextutils.py
new file mode 100644
index 00000000..18e26c63
--- /dev/null
+++ b/mistralclient/openstack/common/gettextutils.py
@@ -0,0 +1,474 @@
+# Copyright 2012 Red Hat, Inc.
+# Copyright 2013 IBM Corp.
+# 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.
+
+"""
+gettext for openstack-common modules.
+
+Usual usage in an openstack.common module:
+
+    from mistralclient.openstack.common.gettextutils import _
+"""
+
+import copy
+import functools
+import gettext
+import locale
+from logging import handlers
+import os
+import re
+
+from babel import localedata
+import six
+
+_localedir = os.environ.get('mistralclient'.upper() + '_LOCALEDIR')
+_t = gettext.translation('mistralclient', localedir=_localedir, fallback=True)
+
+# We use separate translation catalogs for each log level, so set up a
+# mapping between the log level name and the translator. The domain
+# for the log level is project_name + "-log-" + log_level so messages
+# for each level end up in their own catalog.
+_t_log_levels = dict(
+    (level, gettext.translation('mistralclient' + '-log-' + level,
+                                localedir=_localedir,
+                                fallback=True))
+    for level in ['info', 'warning', 'error', 'critical']
+)
+
+_AVAILABLE_LANGUAGES = {}
+USE_LAZY = False
+
+
+def enable_lazy():
+    """Convenience function for configuring _() to use lazy gettext
+
+    Call this at the start of execution to enable the gettextutils._
+    function to use lazy gettext functionality. This is useful if
+    your project is importing _ directly instead of using the
+    gettextutils.install() way of importing the _ function.
+    """
+    global USE_LAZY
+    USE_LAZY = True
+
+
+def _(msg):
+    if USE_LAZY:
+        return Message(msg, domain='mistralclient')
+    else:
+        if six.PY3:
+            return _t.gettext(msg)
+        return _t.ugettext(msg)
+
+
+def _log_translation(msg, level):
+    """Build a single translation of a log message
+    """
+    if USE_LAZY:
+        return Message(msg, domain='mistralclient' + '-log-' + level)
+    else:
+        translator = _t_log_levels[level]
+        if six.PY3:
+            return translator.gettext(msg)
+        return translator.ugettext(msg)
+
+# Translators for log levels.
+#
+# The abbreviated names are meant to reflect the usual use of a short
+# name like '_'. The "L" is for "log" and the other letter comes from
+# the level.
+_LI = functools.partial(_log_translation, level='info')
+_LW = functools.partial(_log_translation, level='warning')
+_LE = functools.partial(_log_translation, level='error')
+_LC = functools.partial(_log_translation, level='critical')
+
+
+def install(domain, lazy=False):
+    """Install a _() function using the given translation domain.
+
+    Given a translation domain, install a _() function using gettext's
+    install() function.
+
+    The main difference from gettext.install() is that we allow
+    overriding the default localedir (e.g. /usr/share/locale) using
+    a translation-domain-specific environment variable (e.g.
+    NOVA_LOCALEDIR).
+
+    :param domain: the translation domain
+    :param lazy: indicates whether or not to install the lazy _() function.
+                 The lazy _() introduces a way to do deferred translation
+                 of messages by installing a _ that builds Message objects,
+                 instead of strings, which can then be lazily translated into
+                 any available locale.
+    """
+    if lazy:
+        # NOTE(mrodden): Lazy gettext functionality.
+        #
+        # The following introduces a deferred way to do translations on
+        # messages in OpenStack. We override the standard _() function
+        # and % (format string) operation to build Message objects that can
+        # later be translated when we have more information.
+        def _lazy_gettext(msg):
+            """Create and return a Message object.
+
+            Lazy gettext function for a given domain, it is a factory method
+            for a project/module to get a lazy gettext function for its own
+            translation domain (i.e. nova, glance, cinder, etc.)
+
+            Message encapsulates a string so that we can translate
+            it later when needed.
+            """
+            return Message(msg, domain=domain)
+
+        from six import moves
+        moves.builtins.__dict__['_'] = _lazy_gettext
+    else:
+        localedir = '%s_LOCALEDIR' % domain.upper()
+        if six.PY3:
+            gettext.install(domain,
+                            localedir=os.environ.get(localedir))
+        else:
+            gettext.install(domain,
+                            localedir=os.environ.get(localedir),
+                            unicode=True)
+
+
+class Message(six.text_type):
+    """A Message object is a unicode object that can be translated.
+
+    Translation of Message is done explicitly using the translate() method.
+    For all non-translation intents and purposes, a Message is simply unicode,
+    and can be treated as such.
+    """
+
+    def __new__(cls, msgid, msgtext=None, params=None,
+                domain='mistralclient', *args):
+        """Create a new Message object.
+
+        In order for translation to work gettext requires a message ID, this
+        msgid will be used as the base unicode text. It is also possible
+        for the msgid and the base unicode text to be different by passing
+        the msgtext parameter.
+        """
+        # If the base msgtext is not given, we use the default translation
+        # of the msgid (which is in English) just in case the system locale is
+        # not English, so that the base text will be in that locale by default.
+        if not msgtext:
+            msgtext = Message._translate_msgid(msgid, domain)
+        # We want to initialize the parent unicode with the actual object that
+        # would have been plain unicode if 'Message' was not enabled.
+        msg = super(Message, cls).__new__(cls, msgtext)
+        msg.msgid = msgid
+        msg.domain = domain
+        msg.params = params
+        return msg
+
+    def translate(self, desired_locale=None):
+        """Translate this message to the desired locale.
+
+        :param desired_locale: The desired locale to translate the message to,
+                               if no locale is provided the message will be
+                               translated to the system's default locale.
+
+        :returns: the translated message in unicode
+        """
+
+        translated_message = Message._translate_msgid(self.msgid,
+                                                      self.domain,
+                                                      desired_locale)
+        if self.params is None:
+            # No need for more translation
+            return translated_message
+
+        # This Message object may have been formatted with one or more
+        # Message objects as substitution arguments, given either as a single
+        # argument, part of a tuple, or as one or more values in a dictionary.
+        # When translating this Message we need to translate those Messages too
+        translated_params = _translate_args(self.params, desired_locale)
+
+        translated_message = translated_message % translated_params
+
+        return translated_message
+
+    @staticmethod
+    def _translate_msgid(msgid, domain, desired_locale=None):
+        if not desired_locale:
+            system_locale = locale.getdefaultlocale()
+            # If the system locale is not available to the runtime use English
+            if not system_locale[0]:
+                desired_locale = 'en_US'
+            else:
+                desired_locale = system_locale[0]
+
+        locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
+        lang = gettext.translation(domain,
+                                   localedir=locale_dir,
+                                   languages=[desired_locale],
+                                   fallback=True)
+        if six.PY3:
+            translator = lang.gettext
+        else:
+            translator = lang.ugettext
+
+        translated_message = translator(msgid)
+        return translated_message
+
+    def __mod__(self, other):
+        # When we mod a Message we want the actual operation to be performed
+        # by the parent class (i.e. unicode()), the only thing  we do here is
+        # save the original msgid and the parameters in case of a translation
+        params = self._sanitize_mod_params(other)
+        unicode_mod = super(Message, self).__mod__(params)
+        modded = Message(self.msgid,
+                         msgtext=unicode_mod,
+                         params=params,
+                         domain=self.domain)
+        return modded
+
+    def _sanitize_mod_params(self, other):
+        """Sanitize the object being modded with this Message.
+
+        - Add support for modding 'None' so translation supports it
+        - Trim the modded object, which can be a large dictionary, to only
+        those keys that would actually be used in a translation
+        - Snapshot the object being modded, in case the message is
+        translated, it will be used as it was when the Message was created
+        """
+        if other is None:
+            params = (other,)
+        elif isinstance(other, dict):
+            params = self._trim_dictionary_parameters(other)
+        else:
+            params = self._copy_param(other)
+        return params
+
+    def _trim_dictionary_parameters(self, dict_param):
+        """Return a dict that only has matching entries in the msgid."""
+        # NOTE(luisg): Here we trim down the dictionary passed as parameters
+        # to avoid carrying a lot of unnecessary weight around in the message
+        # object, for example if someone passes in Message() % locals() but
+        # only some params are used, and additionally we prevent errors for
+        # non-deepcopyable objects by unicoding() them.
+
+        # Look for %(param) keys in msgid;
+        # Skip %% and deal with the case where % is first character on the line
+        keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
+
+        # If we don't find any %(param) keys but have a %s
+        if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
+            # Apparently the full dictionary is the parameter
+            params = self._copy_param(dict_param)
+        else:
+            params = {}
+            # Save our existing parameters as defaults to protect
+            # ourselves from losing values if we are called through an
+            # (erroneous) chain that builds a valid Message with
+            # arguments, and then does something like "msg % kwds"
+            # where kwds is an empty dictionary.
+            src = {}
+            if isinstance(self.params, dict):
+                src.update(self.params)
+            src.update(dict_param)
+            for key in keys:
+                params[key] = self._copy_param(src[key])
+
+        return params
+
+    def _copy_param(self, param):
+        try:
+            return copy.deepcopy(param)
+        except TypeError:
+            # Fallback to casting to unicode this will handle the
+            # python code-like objects that can't be deep-copied
+            return six.text_type(param)
+
+    def __add__(self, other):
+        msg = _('Message objects do not support addition.')
+        raise TypeError(msg)
+
+    def __radd__(self, other):
+        return self.__add__(other)
+
+    def __str__(self):
+        # NOTE(luisg): Logging in python 2.6 tries to str() log records,
+        # and it expects specifically a UnicodeError in order to proceed.
+        msg = _('Message objects do not support str() because they may '
+                'contain non-ascii characters. '
+                'Please use unicode() or translate() instead.')
+        raise UnicodeError(msg)
+
+
+def get_available_languages(domain):
+    """Lists the available languages for the given translation domain.
+
+    :param domain: the domain to get languages for
+    """
+    if domain in _AVAILABLE_LANGUAGES:
+        return copy.copy(_AVAILABLE_LANGUAGES[domain])
+
+    localedir = '%s_LOCALEDIR' % domain.upper()
+    find = lambda x: gettext.find(domain,
+                                  localedir=os.environ.get(localedir),
+                                  languages=[x])
+
+    # NOTE(mrodden): en_US should always be available (and first in case
+    # order matters) since our in-line message strings are en_US
+    language_list = ['en_US']
+    # NOTE(luisg): Babel <1.0 used a function called list(), which was
+    # renamed to locale_identifiers() in >=1.0, the requirements master list
+    # requires >=0.9.6, uncapped, so defensively work with both. We can remove
+    # this check when the master list updates to >=1.0, and update all projects
+    list_identifiers = (getattr(localedata, 'list', None) or
+                        getattr(localedata, 'locale_identifiers'))
+    locale_identifiers = list_identifiers()
+
+    for i in locale_identifiers:
+        if find(i) is not None:
+            language_list.append(i)
+
+    # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
+    # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
+    # are perfectly legitimate locales:
+    #     https://github.com/mitsuhiko/babel/issues/37
+    # In Babel 1.3 they fixed the bug and they support these locales, but
+    # they are still not explicitly "listed" by locale_identifiers().
+    # That is  why we add the locales here explicitly if necessary so that
+    # they are listed as supported.
+    aliases = {'zh': 'zh_CN',
+               'zh_Hant_HK': 'zh_HK',
+               'zh_Hant': 'zh_TW',
+               'fil': 'tl_PH'}
+    for (locale, alias) in six.iteritems(aliases):
+        if locale in language_list and alias not in language_list:
+            language_list.append(alias)
+
+    _AVAILABLE_LANGUAGES[domain] = language_list
+    return copy.copy(language_list)
+
+
+def translate(obj, desired_locale=None):
+    """Gets the translated unicode representation of the given object.
+
+    If the object is not translatable it is returned as-is.
+    If the locale is None the object is translated to the system locale.
+
+    :param obj: the object to translate
+    :param desired_locale: the locale to translate the message to, if None the
+                           default system locale will be used
+    :returns: the translated object in unicode, or the original object if
+              it could not be translated
+    """
+    message = obj
+    if not isinstance(message, Message):
+        # If the object to translate is not already translatable,
+        # let's first get its unicode representation
+        message = six.text_type(obj)
+    if isinstance(message, Message):
+        # Even after unicoding() we still need to check if we are
+        # running with translatable unicode before translating
+        return message.translate(desired_locale)
+    return obj
+
+
+def _translate_args(args, desired_locale=None):
+    """Translates all the translatable elements of the given arguments object.
+
+    This method is used for translating the translatable values in method
+    arguments which include values of tuples or dictionaries.
+    If the object is not a tuple or a dictionary the object itself is
+    translated if it is translatable.
+
+    If the locale is None the object is translated to the system locale.
+
+    :param args: the args to translate
+    :param desired_locale: the locale to translate the args to, if None the
+                           default system locale will be used
+    :returns: a new args object with the translated contents of the original
+    """
+    if isinstance(args, tuple):
+        return tuple(translate(v, desired_locale) for v in args)
+    if isinstance(args, dict):
+        translated_dict = {}
+        for (k, v) in six.iteritems(args):
+            translated_v = translate(v, desired_locale)
+            translated_dict[k] = translated_v
+        return translated_dict
+    return translate(args, desired_locale)
+
+
+class TranslationHandler(handlers.MemoryHandler):
+    """Handler that translates records before logging them.
+
+    The TranslationHandler takes a locale and a target logging.Handler object
+    to forward LogRecord objects to after translating them. This handler
+    depends on Message objects being logged, instead of regular strings.
+
+    The handler can be configured declaratively in the logging.conf as follows:
+
+        [handlers]
+        keys = translatedlog, translator
+
+        [handler_translatedlog]
+        class = handlers.WatchedFileHandler
+        args = ('/var/log/api-localized.log',)
+        formatter = context
+
+        [handler_translator]
+        class = openstack.common.log.TranslationHandler
+        target = translatedlog
+        args = ('zh_CN',)
+
+    If the specified locale is not available in the system, the handler will
+    log in the default locale.
+    """
+
+    def __init__(self, locale=None, target=None):
+        """Initialize a TranslationHandler
+
+        :param locale: locale to use for translating messages
+        :param target: logging.Handler object to forward
+                       LogRecord objects to after translation
+        """
+        # NOTE(luisg): In order to allow this handler to be a wrapper for
+        # other handlers, such as a FileHandler, and still be able to
+        # configure it using logging.conf, this handler has to extend
+        # MemoryHandler because only the MemoryHandlers' logging.conf
+        # parsing is implemented such that it accepts a target handler.
+        handlers.MemoryHandler.__init__(self, capacity=0, target=target)
+        self.locale = locale
+
+    def setFormatter(self, fmt):
+        self.target.setFormatter(fmt)
+
+    def emit(self, record):
+        # We save the message from the original record to restore it
+        # after translation, so other handlers are not affected by this
+        original_msg = record.msg
+        original_args = record.args
+
+        try:
+            self._translate_and_log_record(record)
+        finally:
+            record.msg = original_msg
+            record.args = original_args
+
+    def _translate_and_log_record(self, record):
+        record.msg = translate(record.msg, self.locale)
+
+        # In addition to translating the message, we also need to translate
+        # arguments that were passed to the log method that were not part
+        # of the main message e.g., log.info(_('Some message %s'), this_one))
+        record.args = _translate_args(record.args, self.locale)
+
+        self.target.emit(record)
diff --git a/mistralclient/openstack/common/importutils.py b/mistralclient/openstack/common/importutils.py
new file mode 100644
index 00000000..4f901ae9
--- /dev/null
+++ b/mistralclient/openstack/common/importutils.py
@@ -0,0 +1,73 @@
+# Copyright 2011 OpenStack Foundation.
+# 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 related utilities and helper functions.
+"""
+
+import sys
+import traceback
+
+
+def import_class(import_str):
+    """Returns a class from a string including module and class."""
+    mod_str, _sep, class_str = import_str.rpartition('.')
+    try:
+        __import__(mod_str)
+        return getattr(sys.modules[mod_str], class_str)
+    except (ValueError, AttributeError):
+        raise ImportError('Class %s cannot be found (%s)' %
+                          (class_str,
+                           traceback.format_exception(*sys.exc_info())))
+
+
+def import_object(import_str, *args, **kwargs):
+    """Import a class and return an instance of it."""
+    return import_class(import_str)(*args, **kwargs)
+
+
+def import_object_ns(name_space, import_str, *args, **kwargs):
+    """Tries to import object from default namespace.
+
+    Imports a class and return an instance of it, first by trying
+    to find the class in a default namespace, then failing back to
+    a full path if not found in the default namespace.
+    """
+    import_value = "%s.%s" % (name_space, import_str)
+    try:
+        return import_class(import_value)(*args, **kwargs)
+    except ImportError:
+        return import_class(import_str)(*args, **kwargs)
+
+
+def import_module(import_str):
+    """Import a module."""
+    __import__(import_str)
+    return sys.modules[import_str]
+
+
+def import_versioned_module(version, submodule=None):
+    module = 'mistralclient.v%s' % version
+    if submodule:
+        module = '.'.join((module, submodule))
+    return import_module(module)
+
+
+def try_import(import_str, default=None):
+    """Try to import a module and if it fails return default."""
+    try:
+        return import_module(import_str)
+    except ImportError:
+        return default
diff --git a/mistralclient/openstack/common/jsonutils.py b/mistralclient/openstack/common/jsonutils.py
new file mode 100644
index 00000000..43f62cbb
--- /dev/null
+++ b/mistralclient/openstack/common/jsonutils.py
@@ -0,0 +1,182 @@
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 Justin Santa Barbara
+# 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.
+
+'''
+JSON related utilities.
+
+This module provides a few things:
+
+    1) A handy function for getting an object down to something that can be
+    JSON serialized.  See to_primitive().
+
+    2) Wrappers around loads() and dumps().  The dumps() wrapper will
+    automatically use to_primitive() for you if needed.
+
+    3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson
+    is available.
+'''
+
+
+import datetime
+import functools
+import inspect
+import itertools
+import json
+try:
+    import xmlrpclib
+except ImportError:
+    # NOTE(jaypipes): xmlrpclib was renamed to xmlrpc.client in Python3
+    #                 however the function and object call signatures
+    #                 remained the same. This whole try/except block should
+    #                 be removed and replaced with a call to six.moves once
+    #                 six 1.4.2 is released. See http://bit.ly/1bqrVzu
+    import xmlrpc.client as xmlrpclib
+
+import six
+
+from mistralclient.openstack.common import gettextutils
+from mistralclient.openstack.common import importutils
+from mistralclient.openstack.common import timeutils
+
+netaddr = importutils.try_import("netaddr")
+
+_nasty_type_tests = [inspect.ismodule, inspect.isclass, inspect.ismethod,
+                     inspect.isfunction, inspect.isgeneratorfunction,
+                     inspect.isgenerator, inspect.istraceback, inspect.isframe,
+                     inspect.iscode, inspect.isbuiltin, inspect.isroutine,
+                     inspect.isabstract]
+
+_simple_types = (six.string_types + six.integer_types
+                 + (type(None), bool, float))
+
+
+def to_primitive(value, convert_instances=False, convert_datetime=True,
+                 level=0, max_depth=3):
+    """Convert a complex object into primitives.
+
+    Handy for JSON serialization. We can optionally handle instances,
+    but since this is a recursive function, we could have cyclical
+    data structures.
+
+    To handle cyclical data structures we could track the actual objects
+    visited in a set, but not all objects are hashable. Instead we just
+    track the depth of the object inspections and don't go too deep.
+
+    Therefore, convert_instances=True is lossy ... be aware.
+
+    """
+    # handle obvious types first - order of basic types determined by running
+    # full tests on nova project, resulting in the following counts:
+    # 572754 <type 'NoneType'>
+    # 460353 <type 'int'>
+    # 379632 <type 'unicode'>
+    # 274610 <type 'str'>
+    # 199918 <type 'dict'>
+    # 114200 <type 'datetime.datetime'>
+    #  51817 <type 'bool'>
+    #  26164 <type 'list'>
+    #   6491 <type 'float'>
+    #    283 <type 'tuple'>
+    #     19 <type 'long'>
+    if isinstance(value, _simple_types):
+        return value
+
+    if isinstance(value, datetime.datetime):
+        if convert_datetime:
+            return timeutils.strtime(value)
+        else:
+            return value
+
+    # value of itertools.count doesn't get caught by nasty_type_tests
+    # and results in infinite loop when list(value) is called.
+    if type(value) == itertools.count:
+        return six.text_type(value)
+
+    # FIXME(vish): Workaround for LP bug 852095. Without this workaround,
+    #              tests that raise an exception in a mocked method that
+    #              has a @wrap_exception with a notifier will fail. If
+    #              we up the dependency to 0.5.4 (when it is released) we
+    #              can remove this workaround.
+    if getattr(value, '__module__', None) == 'mox':
+        return 'mock'
+
+    if level > max_depth:
+        return '?'
+
+    # The try block may not be necessary after the class check above,
+    # but just in case ...
+    try:
+        recursive = functools.partial(to_primitive,
+                                      convert_instances=convert_instances,
+                                      convert_datetime=convert_datetime,
+                                      level=level,
+                                      max_depth=max_depth)
+        if isinstance(value, dict):
+            return dict((k, recursive(v)) for k, v in six.iteritems(value))
+        elif isinstance(value, (list, tuple)):
+            return [recursive(lv) for lv in value]
+
+        # It's not clear why xmlrpclib created their own DateTime type, but
+        # for our purposes, make it a datetime type which is explicitly
+        # handled
+        if isinstance(value, xmlrpclib.DateTime):
+            value = datetime.datetime(*tuple(value.timetuple())[:6])
+
+        if convert_datetime and isinstance(value, datetime.datetime):
+            return timeutils.strtime(value)
+        elif isinstance(value, gettextutils.Message):
+            return value.data
+        elif hasattr(value, 'iteritems'):
+            return recursive(dict(value.iteritems()), level=level + 1)
+        elif hasattr(value, '__iter__'):
+            return recursive(list(value))
+        elif convert_instances and hasattr(value, '__dict__'):
+            # Likely an instance of something. Watch for cycles.
+            # Ignore class member vars.
+            return recursive(value.__dict__, level=level + 1)
+        elif netaddr and isinstance(value, netaddr.IPAddress):
+            return six.text_type(value)
+        else:
+            if any(test(value) for test in _nasty_type_tests):
+                return six.text_type(value)
+            return value
+    except TypeError:
+        # Class objects are tricky since they may define something like
+        # __iter__ defined but it isn't callable as list().
+        return six.text_type(value)
+
+
+def dumps(value, default=to_primitive, **kwargs):
+    return json.dumps(value, default=default, **kwargs)
+
+
+def loads(s):
+    return json.loads(s)
+
+
+def load(s):
+    return json.load(s)
+
+
+try:
+    import anyjson
+except ImportError:
+    pass
+else:
+    anyjson._modules.append((__name__, 'dumps', TypeError,
+                                       'loads', ValueError, 'load'))
+    anyjson.force_implementation(__name__)
diff --git a/mistralclient/openstack/common/local.py b/mistralclient/openstack/common/local.py
new file mode 100644
index 00000000..0819d5b9
--- /dev/null
+++ b/mistralclient/openstack/common/local.py
@@ -0,0 +1,45 @@
+# Copyright 2011 OpenStack Foundation.
+# 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.
+
+"""Local storage of variables using weak references"""
+
+import threading
+import weakref
+
+
+class WeakLocal(threading.local):
+    def __getattribute__(self, attr):
+        rval = super(WeakLocal, self).__getattribute__(attr)
+        if rval:
+            # NOTE(mikal): this bit is confusing. What is stored is a weak
+            # reference, not the value itself. We therefore need to lookup
+            # the weak reference and return the inner value here.
+            rval = rval()
+        return rval
+
+    def __setattr__(self, attr, value):
+        value = weakref.ref(value)
+        return super(WeakLocal, self).__setattr__(attr, value)
+
+
+# NOTE(mikal): the name "store" should be deprecated in the future
+store = WeakLocal()
+
+# A "weak" store uses weak references and allows an object to fall out of scope
+# when it falls out of scope in the code that uses the thread local storage. A
+# "strong" store will hold a reference to the object so that it never falls out
+# of scope.
+weak_store = WeakLocal()
+strong_store = threading.local()
diff --git a/mistralclient/openstack/common/log.py b/mistralclient/openstack/common/log.py
new file mode 100644
index 00000000..396111e7
--- /dev/null
+++ b/mistralclient/openstack/common/log.py
@@ -0,0 +1,712 @@
+# Copyright 2011 OpenStack Foundation.
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""OpenStack logging handler.
+
+This module adds to logging functionality by adding the option to specify
+a context object when calling the various log methods.  If the context object
+is not specified, default formatting is used. Additionally, an instance uuid
+may be passed as part of the log message, which is intended to make it easier
+for admins to find messages related to a specific instance.
+
+It also allows setting of formatting information through conf.
+
+"""
+
+import inspect
+import itertools
+import logging
+import logging.config
+import logging.handlers
+import os
+import re
+import sys
+import traceback
+
+from oslo.config import cfg
+import six
+from six import moves
+
+from mistralclient.openstack.common.gettextutils import _
+from mistralclient.openstack.common import importutils
+from mistralclient.openstack.common import jsonutils
+from mistralclient.openstack.common import local
+
+
+_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
+
+# NOTE(ldbragst): Let's build a list of regex objects using the list of
+# _SANITIZE_KEYS we already have. This way, we only have to add the new key
+# to the list of _SANITIZE_KEYS and we can generate regular expressions
+# for XML and JSON automatically.
+_SANITIZE_PATTERNS = []
+_FORMAT_PATTERNS = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
+                    r'(<%(key)s>).*?(</%(key)s>)',
+                    r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
+                    r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])']
+
+for key in _SANITIZE_KEYS:
+    for pattern in _FORMAT_PATTERNS:
+        reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
+        _SANITIZE_PATTERNS.append(reg_ex)
+
+
+common_cli_opts = [
+    cfg.BoolOpt('debug',
+                short='d',
+                default=False,
+                help='Print debugging output (set logging level to '
+                     'DEBUG instead of default WARNING level).'),
+    cfg.BoolOpt('verbose',
+                short='v',
+                default=False,
+                help='Print more verbose output (set logging level to '
+                     'INFO instead of default WARNING level).'),
+]
+
+logging_cli_opts = [
+    cfg.StrOpt('log-config-append',
+               metavar='PATH',
+               deprecated_name='log-config',
+               help='The name of logging configuration file. It does not '
+                    'disable existing loggers, but just appends specified '
+                    'logging configuration to any other existing logging '
+                    'options. Please see the Python logging module '
+                    'documentation for details on logging configuration '
+                    'files.'),
+    cfg.StrOpt('log-format',
+               default=None,
+               metavar='FORMAT',
+               help='DEPRECATED. '
+                    'A logging.Formatter log message format string which may '
+                    'use any of the available logging.LogRecord attributes. '
+                    'This option is deprecated.  Please use '
+                    'logging_context_format_string and '
+                    'logging_default_format_string instead.'),
+    cfg.StrOpt('log-date-format',
+               default=_DEFAULT_LOG_DATE_FORMAT,
+               metavar='DATE_FORMAT',
+               help='Format string for %%(asctime)s in log records. '
+                    'Default: %(default)s'),
+    cfg.StrOpt('log-file',
+               metavar='PATH',
+               deprecated_name='logfile',
+               help='(Optional) Name of log file to output to. '
+                    'If no default is set, logging will go to stdout.'),
+    cfg.StrOpt('log-dir',
+               deprecated_name='logdir',
+               help='(Optional) The base directory used for relative '
+                    '--log-file paths'),
+    cfg.BoolOpt('use-syslog',
+                default=False,
+                help='Use syslog for logging. '
+                     'Existing syslog format is DEPRECATED during I, '
+                     'and then will be changed in J to honor RFC5424'),
+    cfg.BoolOpt('use-syslog-rfc-format',
+                # TODO(bogdando) remove or use True after existing
+                #    syslog format deprecation in J
+                default=False,
+                help='(Optional) Use syslog rfc5424 format for logging. '
+                     'If enabled, will add APP-NAME (RFC5424) before the '
+                     'MSG part of the syslog message.  The old format '
+                     'without APP-NAME is deprecated in I, '
+                     'and will be removed in J.'),
+    cfg.StrOpt('syslog-log-facility',
+               default='LOG_USER',
+               help='Syslog facility to receive log lines')
+]
+
+generic_log_opts = [
+    cfg.BoolOpt('use_stderr',
+                default=True,
+                help='Log output to standard error')
+]
+
+log_opts = [
+    cfg.StrOpt('logging_context_format_string',
+               default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
+                       '%(name)s [%(request_id)s %(user_identity)s] '
+                       '%(instance)s%(message)s',
+               help='Format string to use for log messages with context'),
+    cfg.StrOpt('logging_default_format_string',
+               default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
+                       '%(name)s [-] %(instance)s%(message)s',
+               help='Format string to use for log messages without context'),
+    cfg.StrOpt('logging_debug_format_suffix',
+               default='%(funcName)s %(pathname)s:%(lineno)d',
+               help='Data to append to log format when level is DEBUG'),
+    cfg.StrOpt('logging_exception_prefix',
+               default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s '
+               '%(instance)s',
+               help='Prefix each line of exception output with this format'),
+    cfg.ListOpt('default_log_levels',
+                default=[
+                    'amqp=WARN',
+                    'amqplib=WARN',
+                    'boto=WARN',
+                    'qpid=WARN',
+                    'sqlalchemy=WARN',
+                    'suds=INFO',
+                    'iso8601=WARN',
+                    'requests.packages.urllib3.connectionpool=WARN'
+                ],
+                help='List of logger=LEVEL pairs'),
+    cfg.BoolOpt('publish_errors',
+                default=False,
+                help='Publish error events'),
+    cfg.BoolOpt('fatal_deprecations',
+                default=False,
+                help='Make deprecations fatal'),
+
+    # NOTE(mikal): there are two options here because sometimes we are handed
+    # a full instance (and could include more information), and other times we
+    # are just handed a UUID for the instance.
+    cfg.StrOpt('instance_format',
+               default='[instance: %(uuid)s] ',
+               help='If an instance is passed with the log message, format '
+                    'it like this'),
+    cfg.StrOpt('instance_uuid_format',
+               default='[instance: %(uuid)s] ',
+               help='If an instance UUID is passed with the log message, '
+                    'format it like this'),
+]
+
+CONF = cfg.CONF
+CONF.register_cli_opts(common_cli_opts)
+CONF.register_cli_opts(logging_cli_opts)
+CONF.register_opts(generic_log_opts)
+CONF.register_opts(log_opts)
+
+# our new audit level
+# NOTE(jkoelker) Since we synthesized an audit level, make the logging
+#                module aware of it so it acts like other levels.
+logging.AUDIT = logging.INFO + 1
+logging.addLevelName(logging.AUDIT, 'AUDIT')
+
+
+try:
+    NullHandler = logging.NullHandler
+except AttributeError:  # NOTE(jkoelker) NullHandler added in Python 2.7
+    class NullHandler(logging.Handler):
+        def handle(self, record):
+            pass
+
+        def emit(self, record):
+            pass
+
+        def createLock(self):
+            self.lock = None
+
+
+def _dictify_context(context):
+    if context is None:
+        return None
+    if not isinstance(context, dict) and getattr(context, 'to_dict', None):
+        context = context.to_dict()
+    return context
+
+
+def _get_binary_name():
+    return os.path.basename(inspect.stack()[-1][1])
+
+
+def _get_log_file_path(binary=None):
+    logfile = CONF.log_file
+    logdir = CONF.log_dir
+
+    if logfile and not logdir:
+        return logfile
+
+    if logfile and logdir:
+        return os.path.join(logdir, logfile)
+
+    if logdir:
+        binary = binary or _get_binary_name()
+        return '%s.log' % (os.path.join(logdir, binary),)
+
+    return None
+
+
+def mask_password(message, secret="***"):
+    """Replace password with 'secret' in message.
+
+    :param message: The string which includes security information.
+    :param secret: value with which to replace passwords.
+    :returns: The unicode value of message with the password fields masked.
+
+    For example:
+
+    >>> mask_password("'adminPass' : 'aaaaa'")
+    "'adminPass' : '***'"
+    >>> mask_password("'admin_pass' : 'aaaaa'")
+    "'admin_pass' : '***'"
+    >>> mask_password('"password" : "aaaaa"')
+    '"password" : "***"'
+    >>> mask_password("'original_password' : 'aaaaa'")
+    "'original_password' : '***'"
+    >>> mask_password("u'original_password' :   u'aaaaa'")
+    "u'original_password' :   u'***'"
+    """
+    message = six.text_type(message)
+
+    # NOTE(ldbragst): Check to see if anything in message contains any key
+    # specified in _SANITIZE_KEYS, if not then just return the message since
+    # we don't have to mask any passwords.
+    if not any(key in message for key in _SANITIZE_KEYS):
+        return message
+
+    secret = r'\g<1>' + secret + r'\g<2>'
+    for pattern in _SANITIZE_PATTERNS:
+        message = re.sub(pattern, secret, message)
+    return message
+
+
+class BaseLoggerAdapter(logging.LoggerAdapter):
+
+    def audit(self, msg, *args, **kwargs):
+        self.log(logging.AUDIT, msg, *args, **kwargs)
+
+
+class LazyAdapter(BaseLoggerAdapter):
+    def __init__(self, name='unknown', version='unknown'):
+        self._logger = None
+        self.extra = {}
+        self.name = name
+        self.version = version
+
+    @property
+    def logger(self):
+        if not self._logger:
+            self._logger = getLogger(self.name, self.version)
+        return self._logger
+
+
+class ContextAdapter(BaseLoggerAdapter):
+    warn = logging.LoggerAdapter.warning
+
+    def __init__(self, logger, project_name, version_string):
+        self.logger = logger
+        self.project = project_name
+        self.version = version_string
+        self._deprecated_messages_sent = dict()
+
+    @property
+    def handlers(self):
+        return self.logger.handlers
+
+    def deprecated(self, msg, *args, **kwargs):
+        """Call this method when a deprecated feature is used.
+
+        If the system is configured for fatal deprecations then the message
+        is logged at the 'critical' level and :class:`DeprecatedConfig` will
+        be raised.
+
+        Otherwise, the message will be logged (once) at the 'warn' level.
+
+        :raises: :class:`DeprecatedConfig` if the system is configured for
+                 fatal deprecations.
+
+        """
+        stdmsg = _("Deprecated: %s") % msg
+        if CONF.fatal_deprecations:
+            self.critical(stdmsg, *args, **kwargs)
+            raise DeprecatedConfig(msg=stdmsg)
+
+        # Using a list because a tuple with dict can't be stored in a set.
+        sent_args = self._deprecated_messages_sent.setdefault(msg, list())
+
+        if args in sent_args:
+            # Already logged this message, so don't log it again.
+            return
+
+        sent_args.append(args)
+        self.warn(stdmsg, *args, **kwargs)
+
+    def process(self, msg, kwargs):
+        # NOTE(mrodden): catch any Message/other object and
+        #                coerce to unicode before they can get
+        #                to the python logging and possibly
+        #                cause string encoding trouble
+        if not isinstance(msg, six.string_types):
+            msg = six.text_type(msg)
+
+        if 'extra' not in kwargs:
+            kwargs['extra'] = {}
+        extra = kwargs['extra']
+
+        context = kwargs.pop('context', None)
+        if not context:
+            context = getattr(local.store, 'context', None)
+        if context:
+            extra.update(_dictify_context(context))
+
+        instance = kwargs.pop('instance', None)
+        instance_uuid = (extra.get('instance_uuid') or
+                         kwargs.pop('instance_uuid', None))
+        instance_extra = ''
+        if instance:
+            instance_extra = CONF.instance_format % instance
+        elif instance_uuid:
+            instance_extra = (CONF.instance_uuid_format
+                              % {'uuid': instance_uuid})
+        extra['instance'] = instance_extra
+
+        extra.setdefault('user_identity', kwargs.pop('user_identity', None))
+
+        extra['project'] = self.project
+        extra['version'] = self.version
+        extra['extra'] = extra.copy()
+        return msg, kwargs
+
+
+class JSONFormatter(logging.Formatter):
+    def __init__(self, fmt=None, datefmt=None):
+        # NOTE(jkoelker) we ignore the fmt argument, but its still there
+        #                since logging.config.fileConfig passes it.
+        self.datefmt = datefmt
+
+    def formatException(self, ei, strip_newlines=True):
+        lines = traceback.format_exception(*ei)
+        if strip_newlines:
+            lines = [moves.filter(
+                lambda x: x,
+                line.rstrip().splitlines()) for line in lines]
+            lines = list(itertools.chain(*lines))
+        return lines
+
+    def format(self, record):
+        message = {'message': record.getMessage(),
+                   'asctime': self.formatTime(record, self.datefmt),
+                   'name': record.name,
+                   'msg': record.msg,
+                   'args': record.args,
+                   'levelname': record.levelname,
+                   'levelno': record.levelno,
+                   'pathname': record.pathname,
+                   'filename': record.filename,
+                   'module': record.module,
+                   'lineno': record.lineno,
+                   'funcname': record.funcName,
+                   'created': record.created,
+                   'msecs': record.msecs,
+                   'relative_created': record.relativeCreated,
+                   'thread': record.thread,
+                   'thread_name': record.threadName,
+                   'process_name': record.processName,
+                   'process': record.process,
+                   'traceback': None}
+
+        if hasattr(record, 'extra'):
+            message['extra'] = record.extra
+
+        if record.exc_info:
+            message['traceback'] = self.formatException(record.exc_info)
+
+        return jsonutils.dumps(message)
+
+
+def _create_logging_excepthook(product_name):
+    def logging_excepthook(exc_type, value, tb):
+        extra = {}
+        if CONF.verbose or CONF.debug:
+            extra['exc_info'] = (exc_type, value, tb)
+        getLogger(product_name).critical(
+            "".join(traceback.format_exception_only(exc_type, value)),
+            **extra)
+    return logging_excepthook
+
+
+class LogConfigError(Exception):
+
+    message = _('Error loading logging config %(log_config)s: %(err_msg)s')
+
+    def __init__(self, log_config, err_msg):
+        self.log_config = log_config
+        self.err_msg = err_msg
+
+    def __str__(self):
+        return self.message % dict(log_config=self.log_config,
+                                   err_msg=self.err_msg)
+
+
+def _load_log_config(log_config_append):
+    try:
+        logging.config.fileConfig(log_config_append,
+                                  disable_existing_loggers=False)
+    except moves.configparser.Error as exc:
+        raise LogConfigError(log_config_append, str(exc))
+
+
+def setup(product_name, version='unknown'):
+    """Setup logging."""
+    if CONF.log_config_append:
+        _load_log_config(CONF.log_config_append)
+    else:
+        _setup_logging_from_conf(product_name, version)
+    sys.excepthook = _create_logging_excepthook(product_name)
+
+
+def set_defaults(logging_context_format_string):
+    cfg.set_defaults(log_opts,
+                     logging_context_format_string=
+                     logging_context_format_string)
+
+
+def _find_facility_from_conf():
+    facility_names = logging.handlers.SysLogHandler.facility_names
+    facility = getattr(logging.handlers.SysLogHandler,
+                       CONF.syslog_log_facility,
+                       None)
+
+    if facility is None and CONF.syslog_log_facility in facility_names:
+        facility = facility_names.get(CONF.syslog_log_facility)
+
+    if facility is None:
+        valid_facilities = facility_names.keys()
+        consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON',
+                  'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS',
+                  'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP',
+                  'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3',
+                  'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7']
+        valid_facilities.extend(consts)
+        raise TypeError(_('syslog facility must be one of: %s') %
+                        ', '.join("'%s'" % fac
+                                  for fac in valid_facilities))
+
+    return facility
+
+
+class RFCSysLogHandler(logging.handlers.SysLogHandler):
+    def __init__(self, *args, **kwargs):
+        self.binary_name = _get_binary_name()
+        super(RFCSysLogHandler, self).__init__(*args, **kwargs)
+
+    def format(self, record):
+        msg = super(RFCSysLogHandler, self).format(record)
+        msg = self.binary_name + ' ' + msg
+        return msg
+
+
+def _setup_logging_from_conf(project, version):
+    log_root = getLogger(None).logger
+    for handler in log_root.handlers:
+        log_root.removeHandler(handler)
+
+    if CONF.use_syslog:
+        facility = _find_facility_from_conf()
+        # TODO(bogdando) use the format provided by RFCSysLogHandler
+        #   after existing syslog format deprecation in J
+        if CONF.use_syslog_rfc_format:
+            syslog = RFCSysLogHandler(address='/dev/log',
+                                      facility=facility)
+        else:
+            syslog = logging.handlers.SysLogHandler(address='/dev/log',
+                                                    facility=facility)
+        log_root.addHandler(syslog)
+
+    logpath = _get_log_file_path()
+    if logpath:
+        filelog = logging.handlers.WatchedFileHandler(logpath)
+        log_root.addHandler(filelog)
+
+    if CONF.use_stderr:
+        streamlog = ColorHandler()
+        log_root.addHandler(streamlog)
+
+    elif not logpath:
+        # pass sys.stdout as a positional argument
+        # python2.6 calls the argument strm, in 2.7 it's stream
+        streamlog = logging.StreamHandler(sys.stdout)
+        log_root.addHandler(streamlog)
+
+    if CONF.publish_errors:
+        handler = importutils.import_object(
+            "mistralclient.openstack.common.log_handler.PublishErrorsHandler",
+            logging.ERROR)
+        log_root.addHandler(handler)
+
+    datefmt = CONF.log_date_format
+    for handler in log_root.handlers:
+        # NOTE(alaski): CONF.log_format overrides everything currently.  This
+        # should be deprecated in favor of context aware formatting.
+        if CONF.log_format:
+            handler.setFormatter(logging.Formatter(fmt=CONF.log_format,
+                                                   datefmt=datefmt))
+            log_root.info('Deprecated: log_format is now deprecated and will '
+                          'be removed in the next release')
+        else:
+            handler.setFormatter(ContextFormatter(project=project,
+                                                  version=version,
+                                                  datefmt=datefmt))
+
+    if CONF.debug:
+        log_root.setLevel(logging.DEBUG)
+    elif CONF.verbose:
+        log_root.setLevel(logging.INFO)
+    else:
+        log_root.setLevel(logging.WARNING)
+
+    for pair in CONF.default_log_levels:
+        mod, _sep, level_name = pair.partition('=')
+        level = logging.getLevelName(level_name)
+        logger = logging.getLogger(mod)
+        logger.setLevel(level)
+
+_loggers = {}
+
+
+def getLogger(name='unknown', version='unknown'):
+    if name not in _loggers:
+        _loggers[name] = ContextAdapter(logging.getLogger(name),
+                                        name,
+                                        version)
+    return _loggers[name]
+
+
+def getLazyLogger(name='unknown', version='unknown'):
+    """Returns lazy logger.
+
+    Creates a pass-through logger that does not create the real logger
+    until it is really needed and delegates all calls to the real logger
+    once it is created.
+    """
+    return LazyAdapter(name, version)
+
+
+class WritableLogger(object):
+    """A thin wrapper that responds to `write` and logs."""
+
+    def __init__(self, logger, level=logging.INFO):
+        self.logger = logger
+        self.level = level
+
+    def write(self, msg):
+        self.logger.log(self.level, msg.rstrip())
+
+
+class ContextFormatter(logging.Formatter):
+    """A context.RequestContext aware formatter configured through flags.
+
+    The flags used to set format strings are: logging_context_format_string
+    and logging_default_format_string.  You can also specify
+    logging_debug_format_suffix to append extra formatting if the log level is
+    debug.
+
+    For information about what variables are available for the formatter see:
+    http://docs.python.org/library/logging.html#formatter
+
+    If available, uses the context value stored in TLS - local.store.context
+
+    """
+
+    def __init__(self, *args, **kwargs):
+        """Initialize ContextFormatter instance
+
+        Takes additional keyword arguments which can be used in the message
+        format string.
+
+        :keyword project: project name
+        :type project: string
+        :keyword version: project version
+        :type version: string
+
+        """
+
+        self.project = kwargs.pop('project', 'unknown')
+        self.version = kwargs.pop('version', 'unknown')
+
+        logging.Formatter.__init__(self, *args, **kwargs)
+
+    def format(self, record):
+        """Uses contextstring if request_id is set, otherwise default."""
+
+        # store project info
+        record.project = self.project
+        record.version = self.version
+
+        # store request info
+        context = getattr(local.store, 'context', None)
+        if context:
+            d = _dictify_context(context)
+            for k, v in d.items():
+                setattr(record, k, v)
+
+        # NOTE(sdague): default the fancier formatting params
+        # to an empty string so we don't throw an exception if
+        # they get used
+        for key in ('instance', 'color'):
+            if key not in record.__dict__:
+                record.__dict__[key] = ''
+
+        if record.__dict__.get('request_id'):
+            self._fmt = CONF.logging_context_format_string
+        else:
+            self._fmt = CONF.logging_default_format_string
+
+        if (record.levelno == logging.DEBUG and
+                CONF.logging_debug_format_suffix):
+            self._fmt += " " + CONF.logging_debug_format_suffix
+
+        # Cache this on the record, Logger will respect our formatted copy
+        if record.exc_info:
+            record.exc_text = self.formatException(record.exc_info, record)
+        return logging.Formatter.format(self, record)
+
+    def formatException(self, exc_info, record=None):
+        """Format exception output with CONF.logging_exception_prefix."""
+        if not record:
+            return logging.Formatter.formatException(self, exc_info)
+
+        stringbuffer = moves.StringIO()
+        traceback.print_exception(exc_info[0], exc_info[1], exc_info[2],
+                                  None, stringbuffer)
+        lines = stringbuffer.getvalue().split('\n')
+        stringbuffer.close()
+
+        if CONF.logging_exception_prefix.find('%(asctime)') != -1:
+            record.asctime = self.formatTime(record, self.datefmt)
+
+        formatted_lines = []
+        for line in lines:
+            pl = CONF.logging_exception_prefix % record.__dict__
+            fl = '%s%s' % (pl, line)
+            formatted_lines.append(fl)
+        return '\n'.join(formatted_lines)
+
+
+class ColorHandler(logging.StreamHandler):
+    LEVEL_COLORS = {
+        logging.DEBUG: '\033[00;32m',  # GREEN
+        logging.INFO: '\033[00;36m',  # CYAN
+        logging.AUDIT: '\033[01;36m',  # BOLD CYAN
+        logging.WARN: '\033[01;33m',  # BOLD YELLOW
+        logging.ERROR: '\033[01;31m',  # BOLD RED
+        logging.CRITICAL: '\033[01;31m',  # BOLD RED
+    }
+
+    def format(self, record):
+        record.color = self.LEVEL_COLORS[record.levelno]
+        return logging.StreamHandler.format(self, record)
+
+
+class DeprecatedConfig(Exception):
+    message = _("Fatal call to deprecated config: %(msg)s")
+
+    def __init__(self, msg):
+        super(Exception, self).__init__(self.message % dict(msg=msg))
diff --git a/mistralclient/openstack/common/strutils.py b/mistralclient/openstack/common/strutils.py
new file mode 100644
index 00000000..e1ee0703
--- /dev/null
+++ b/mistralclient/openstack/common/strutils.py
@@ -0,0 +1,244 @@
+# Copyright 2011 OpenStack Foundation.
+# 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.
+
+"""
+System-level utilities and helper functions.
+"""
+
+import math
+import re
+import sys
+import unicodedata
+
+import six
+
+from mistralclient.openstack.common.gettextutils import _
+
+
+UNIT_PREFIX_EXPONENT = {
+    'k': 1,
+    'K': 1,
+    'Ki': 1,
+    'M': 2,
+    'Mi': 2,
+    'G': 3,
+    'Gi': 3,
+    'T': 4,
+    'Ti': 4,
+}
+UNIT_SYSTEM_INFO = {
+    'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')),
+    'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')),
+}
+
+TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
+FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
+
+SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
+SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
+
+
+def int_from_bool_as_string(subject):
+    """Interpret a string as a boolean and return either 1 or 0.
+
+    Any string value in:
+
+        ('True', 'true', 'On', 'on', '1')
+
+    is interpreted as a boolean True.
+
+    Useful for JSON-decoded stuff and config file parsing
+    """
+    return bool_from_string(subject) and 1 or 0
+
+
+def bool_from_string(subject, strict=False, default=False):
+    """Interpret a string as a boolean.
+
+    A case-insensitive match is performed such that strings matching 't',
+    'true', 'on', 'y', 'yes', or '1' are considered True and, when
+    `strict=False`, anything else returns the value specified by 'default'.
+
+    Useful for JSON-decoded stuff and config file parsing.
+
+    If `strict=True`, unrecognized values, including None, will raise a
+    ValueError which is useful when parsing values passed in from an API call.
+    Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
+    """
+    if not isinstance(subject, six.string_types):
+        subject = str(subject)
+
+    lowered = subject.strip().lower()
+
+    if lowered in TRUE_STRINGS:
+        return True
+    elif lowered in FALSE_STRINGS:
+        return False
+    elif strict:
+        acceptable = ', '.join(
+            "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
+        msg = _("Unrecognized value '%(val)s', acceptable values are:"
+                " %(acceptable)s") % {'val': subject,
+                                      'acceptable': acceptable}
+        raise ValueError(msg)
+    else:
+        return default
+
+
+def safe_decode(text, incoming=None, errors='strict'):
+    """Decodes incoming str using `incoming` if they're not already unicode.
+
+    :param incoming: Text's current encoding
+    :param errors: Errors handling policy. See here for valid
+        values http://docs.python.org/2/library/codecs.html
+    :returns: text or a unicode `incoming` encoded
+                representation of it.
+    :raises TypeError: If text is not an instance of str
+    """
+    if not isinstance(text, six.string_types):
+        raise TypeError("%s can't be decoded" % type(text))
+
+    if isinstance(text, six.text_type):
+        return text
+
+    if not incoming:
+        incoming = (sys.stdin.encoding or
+                    sys.getdefaultencoding())
+
+    try:
+        return text.decode(incoming, errors)
+    except UnicodeDecodeError:
+        # Note(flaper87) If we get here, it means that
+        # sys.stdin.encoding / sys.getdefaultencoding
+        # didn't return a suitable encoding to decode
+        # text. This happens mostly when global LANG
+        # var is not set correctly and there's no
+        # default encoding. In this case, most likely
+        # python will use ASCII or ANSI encoders as
+        # default encodings but they won't be capable
+        # of decoding non-ASCII characters.
+        #
+        # Also, UTF-8 is being used since it's an ASCII
+        # extension.
+        return text.decode('utf-8', errors)
+
+
+def safe_encode(text, incoming=None,
+                encoding='utf-8', errors='strict'):
+    """Encodes incoming str/unicode using `encoding`.
+
+    If incoming is not specified, text is expected to be encoded with
+    current python's default encoding. (`sys.getdefaultencoding`)
+
+    :param incoming: Text's current encoding
+    :param encoding: Expected encoding for text (Default UTF-8)
+    :param errors: Errors handling policy. See here for valid
+        values http://docs.python.org/2/library/codecs.html
+    :returns: text or a bytestring `encoding` encoded
+                representation of it.
+    :raises TypeError: If text is not an instance of str
+    """
+    if not isinstance(text, six.string_types):
+        raise TypeError("%s can't be encoded" % type(text))
+
+    if not incoming:
+        incoming = (sys.stdin.encoding or
+                    sys.getdefaultencoding())
+
+    if isinstance(text, six.text_type):
+        if six.PY3:
+            return text.encode(encoding, errors).decode(incoming)
+        else:
+            return text.encode(encoding, errors)
+    elif text and encoding != incoming:
+        # Decode text before encoding it with `encoding`
+        text = safe_decode(text, incoming, errors)
+        if six.PY3:
+            return text.encode(encoding, errors).decode(incoming)
+        else:
+            return text.encode(encoding, errors)
+
+    return text
+
+
+def string_to_bytes(text, unit_system='IEC', return_int=False):
+    """Converts a string into an float representation of bytes.
+
+    The units supported for IEC ::
+
+        Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it)
+        KB, KiB, MB, MiB, GB, GiB, TB, TiB
+
+    The units supported for SI ::
+
+        kb(it), Mb(it), Gb(it), Tb(it)
+        kB, MB, GB, TB
+
+    Note that the SI unit system does not support capital letter 'K'
+
+    :param text: String input for bytes size conversion.
+    :param unit_system: Unit system for byte size conversion.
+    :param return_int: If True, returns integer representation of text
+                       in bytes. (default: decimal)
+    :returns: Numerical representation of text in bytes.
+    :raises ValueError: If text has an invalid value.
+
+    """
+    try:
+        base, reg_ex = UNIT_SYSTEM_INFO[unit_system]
+    except KeyError:
+        msg = _('Invalid unit system: "%s"') % unit_system
+        raise ValueError(msg)
+    match = reg_ex.match(text)
+    if match:
+        magnitude = float(match.group(1))
+        unit_prefix = match.group(2)
+        if match.group(3) in ['b', 'bit']:
+            magnitude /= 8
+    else:
+        msg = _('Invalid string format: %s') % text
+        raise ValueError(msg)
+    if not unit_prefix:
+        res = magnitude
+    else:
+        res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix])
+    if return_int:
+        return int(math.ceil(res))
+    return res
+
+
+def to_slug(value, incoming=None, errors="strict"):
+    """Normalize string.
+
+    Convert to lowercase, remove non-word characters, and convert spaces
+    to hyphens.
+
+    Inspired by Django's `slugify` filter.
+
+    :param value: Text to slugify
+    :param incoming: Text's current encoding
+    :param errors: Errors handling policy. See here for valid
+        values http://docs.python.org/2/library/codecs.html
+    :returns: slugified unicode representation of `value`
+    :raises TypeError: If text is not an instance of str
+    """
+    value = safe_decode(value, incoming, errors)
+    # NOTE(aababilov): no need to use safe_(encode|decode) here:
+    # encodings are always "ascii", error handling is always "ignore"
+    # and types are always known (first: unicode; second: str)
+    value = unicodedata.normalize("NFKD", value).encode(
+        "ascii", "ignore").decode("ascii")
+    value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
+    return SLUGIFY_HYPHENATE_RE.sub("-", value)
diff --git a/mistralclient/openstack/common/timeutils.py b/mistralclient/openstack/common/timeutils.py
new file mode 100644
index 00000000..52688a02
--- /dev/null
+++ b/mistralclient/openstack/common/timeutils.py
@@ -0,0 +1,210 @@
+# Copyright 2011 OpenStack Foundation.
+# 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.
+
+"""
+Time related utilities and helper functions.
+"""
+
+import calendar
+import datetime
+import time
+
+import iso8601
+import six
+
+
+# ISO 8601 extended time format with microseconds
+_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f'
+_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
+PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND
+
+
+def isotime(at=None, subsecond=False):
+    """Stringify time in ISO 8601 format."""
+    if not at:
+        at = utcnow()
+    st = at.strftime(_ISO8601_TIME_FORMAT
+                     if not subsecond
+                     else _ISO8601_TIME_FORMAT_SUBSECOND)
+    tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
+    st += ('Z' if tz == 'UTC' else tz)
+    return st
+
+
+def parse_isotime(timestr):
+    """Parse time from ISO 8601 format."""
+    try:
+        return iso8601.parse_date(timestr)
+    except iso8601.ParseError as e:
+        raise ValueError(six.text_type(e))
+    except TypeError as e:
+        raise ValueError(six.text_type(e))
+
+
+def strtime(at=None, fmt=PERFECT_TIME_FORMAT):
+    """Returns formatted utcnow."""
+    if not at:
+        at = utcnow()
+    return at.strftime(fmt)
+
+
+def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT):
+    """Turn a formatted time back into a datetime."""
+    return datetime.datetime.strptime(timestr, fmt)
+
+
+def normalize_time(timestamp):
+    """Normalize time in arbitrary timezone to UTC naive object."""
+    offset = timestamp.utcoffset()
+    if offset is None:
+        return timestamp
+    return timestamp.replace(tzinfo=None) - offset
+
+
+def is_older_than(before, seconds):
+    """Return True if before is older than seconds."""
+    if isinstance(before, six.string_types):
+        before = parse_strtime(before).replace(tzinfo=None)
+    else:
+        before = before.replace(tzinfo=None)
+
+    return utcnow() - before > datetime.timedelta(seconds=seconds)
+
+
+def is_newer_than(after, seconds):
+    """Return True if after is newer than seconds."""
+    if isinstance(after, six.string_types):
+        after = parse_strtime(after).replace(tzinfo=None)
+    else:
+        after = after.replace(tzinfo=None)
+
+    return after - utcnow() > datetime.timedelta(seconds=seconds)
+
+
+def utcnow_ts():
+    """Timestamp version of our utcnow function."""
+    if utcnow.override_time is None:
+        # NOTE(kgriffs): This is several times faster
+        # than going through calendar.timegm(...)
+        return int(time.time())
+
+    return calendar.timegm(utcnow().timetuple())
+
+
+def utcnow():
+    """Overridable version of utils.utcnow."""
+    if utcnow.override_time:
+        try:
+            return utcnow.override_time.pop(0)
+        except AttributeError:
+            return utcnow.override_time
+    return datetime.datetime.utcnow()
+
+
+def iso8601_from_timestamp(timestamp):
+    """Returns a iso8601 formatted date from timestamp."""
+    return isotime(datetime.datetime.utcfromtimestamp(timestamp))
+
+
+utcnow.override_time = None
+
+
+def set_time_override(override_time=None):
+    """Overrides utils.utcnow.
+
+    Make it return a constant time or a list thereof, one at a time.
+
+    :param override_time: datetime instance or list thereof. If not
+                          given, defaults to the current UTC time.
+    """
+    utcnow.override_time = override_time or datetime.datetime.utcnow()
+
+
+def advance_time_delta(timedelta):
+    """Advance overridden time using a datetime.timedelta."""
+    assert(not utcnow.override_time is None)
+    try:
+        for dt in utcnow.override_time:
+            dt += timedelta
+    except TypeError:
+        utcnow.override_time += timedelta
+
+
+def advance_time_seconds(seconds):
+    """Advance overridden time by seconds."""
+    advance_time_delta(datetime.timedelta(0, seconds))
+
+
+def clear_time_override():
+    """Remove the overridden time."""
+    utcnow.override_time = None
+
+
+def marshall_now(now=None):
+    """Make an rpc-safe datetime with microseconds.
+
+    Note: tzinfo is stripped, but not required for relative times.
+    """
+    if not now:
+        now = utcnow()
+    return dict(day=now.day, month=now.month, year=now.year, hour=now.hour,
+                minute=now.minute, second=now.second,
+                microsecond=now.microsecond)
+
+
+def unmarshall_time(tyme):
+    """Unmarshall a datetime dict."""
+    return datetime.datetime(day=tyme['day'],
+                             month=tyme['month'],
+                             year=tyme['year'],
+                             hour=tyme['hour'],
+                             minute=tyme['minute'],
+                             second=tyme['second'],
+                             microsecond=tyme['microsecond'])
+
+
+def delta_seconds(before, after):
+    """Return the difference between two timing objects.
+
+    Compute the difference in seconds between two date, time, or
+    datetime objects (as a float, to microsecond resolution).
+    """
+    delta = after - before
+    return total_seconds(delta)
+
+
+def total_seconds(delta):
+    """Return the total seconds of datetime.timedelta object.
+
+    Compute total seconds of datetime.timedelta, datetime.timedelta
+    doesn't have method total_seconds in Python2.6, calculate it manually.
+    """
+    try:
+        return delta.total_seconds()
+    except AttributeError:
+        return ((delta.days * 24 * 3600) + delta.seconds +
+                float(delta.microseconds) / (10 ** 6))
+
+
+def is_soon(dt, window):
+    """Determines if time is going to happen in the next window seconds.
+
+    :param dt: the time
+    :param window: minimum seconds to remain to consider the time not soon
+
+    :return: True if expiration is within the given duration
+    """
+    soon = (utcnow() + datetime.timedelta(seconds=window))
+    return normalize_time(dt) <= soon
diff --git a/mistralclient/openstack/common/uuidutils.py b/mistralclient/openstack/common/uuidutils.py
new file mode 100644
index 00000000..234b880c
--- /dev/null
+++ b/mistralclient/openstack/common/uuidutils.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2012 Intel Corporation.
+# 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.
+
+"""
+UUID related utilities and helper functions.
+"""
+
+import uuid
+
+
+def generate_uuid():
+    return str(uuid.uuid4())
+
+
+def is_uuid_like(val):
+    """Returns validation of a value as a UUID.
+
+    For our purposes, a UUID is a canonical form string:
+    aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
+
+    """
+    try:
+        return str(uuid.UUID(val)) == val
+    except (TypeError, ValueError, AttributeError):
+        return False
diff --git a/mistralclient/shell.py b/mistralclient/shell.py
new file mode 100644
index 00000000..30eccdfd
--- /dev/null
+++ b/mistralclient/shell.py
@@ -0,0 +1,201 @@
+# Copyright 2014 StackStorm, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+"""
+Command-line interface to the Mistral APIs
+"""
+
+import sys
+
+from mistralclient.openstack.common.cliutils import env
+from mistralclient.openstack.common import log as logging
+
+from mistralclient.api.client import Client
+
+import mistralclient.commands.workbooks
+import mistralclient.commands.executions
+import mistralclient.commands.tasks
+
+from cliff.app import App
+from cliff.help import HelpAction
+from cliff.commandmanager import CommandManager
+
+import argparse
+
+LOG = logging.getLogger(__name__)
+
+
+class MistralShell(App):
+
+    def __init__(self):
+        super(MistralShell, self).__init__(
+            description=__doc__.strip(),
+            version='0.1',
+            command_manager=CommandManager('mistral.cli'),
+        )
+
+        self.commands = {
+            'workbook-list': mistralclient.commands.workbooks.List,
+            'workbook-get': mistralclient.commands.workbooks.Get,
+            'workbook-create': mistralclient.commands.workbooks.Create,
+            'workbook-delete': mistralclient.commands.workbooks.Delete,
+            'workbook-update': mistralclient.commands.workbooks.Update,
+            'workbook-upload-definition':
+            mistralclient.commands.workbooks.UploadDefinition,
+            'workbook-get-definition':
+            mistralclient.commands.workbooks.GetDefinition,
+            'execution-list': mistralclient.commands.executions.List,
+            'execution-get': mistralclient.commands.executions.Get,
+            'execution-create': mistralclient.commands.executions.Create,
+            'execution-delete': mistralclient.commands.executions.Delete,
+            'execution-update': mistralclient.commands.executions.Update,
+            'task-list': mistralclient.commands.tasks.List,
+            'task-get': mistralclient.commands.tasks.Get,
+            'task-update': mistralclient.commands.tasks.Update,
+        }
+
+        for k, v in self.commands.items():
+            self.command_manager.add_command(k, v)
+
+    def build_option_parser(self, description, version,
+                            argparse_kwargs=None):
+        """Return an argparse option parser for this application.
+
+        Subclasses may override this method to extend
+        the parser with more global options.
+
+        :param description: full description of the application
+        :paramtype description: str
+        :param version: version number for the application
+        :paramtype version: str
+        :param argparse_kwargs: extra keyword argument passed to the
+                                ArgumentParser constructor
+        :paramtype extra_kwargs: dict
+        """
+        argparse_kwargs = argparse_kwargs or {}
+        parser = argparse.ArgumentParser(
+            description=description,
+            add_help=False,
+            **argparse_kwargs
+        )
+        parser.add_argument(
+            '--version',
+            action='version',
+            version='%(prog)s {0}'.format(version),
+            help='Show program\'s version number and exit.'
+        )
+        parser.add_argument(
+            '-v', '--verbose',
+            action='count',
+            dest='verbose_level',
+            default=self.DEFAULT_VERBOSE_LEVEL,
+            help='Increase verbosity of output. Can be repeated.',
+        )
+        parser.add_argument(
+            '--log-file',
+            action='store',
+            default=None,
+            help='Specify a file to log output. Disabled by default.',
+        )
+        parser.add_argument(
+            '-q', '--quiet',
+            action='store_const',
+            dest='verbose_level',
+            const=0,
+            help='Suppress output except warnings and errors.',
+        )
+        parser.add_argument(
+            '-h', '--help',
+            action=HelpAction,
+            nargs=0,
+            default=self,  # tricky
+            help="Show this help message and exit.",
+        )
+        parser.add_argument(
+            '--debug',
+            default=False,
+            action='store_true',
+            help='Show tracebacks on errors.',
+        )
+        parser.add_argument(
+            '--os-mistral-url',
+            action='store',
+            dest='mistral_url',
+            default=env('OS_MISTRAL_URL', default='http://localhost:8989/v1'),
+            help='Mistral API host (Env: OS_MISTRAL_URL)'
+        )
+        parser.add_argument(
+            '--os-username',
+            action='store',
+            dest='username',
+            default=env('OS_USERNAME', default='admin'),
+            help='Authentication username (Env: OS_USERNAME)'
+        )
+        parser.add_argument(
+            '--os-password',
+            action='store',
+            dest='password',
+            default=env('OS_PASSWORD', default='openstack'),
+            help='Authentication password (Env: OS_PASSWORD)'
+        )
+        parser.add_argument(
+            '--os-tenant-id',
+            action='store',
+            dest='tenant_id',
+            default=env('OS_TENANT_ID'),
+            help='Authentication tenant identifier (Env: OS_TENANT_ID)'
+        )
+        parser.add_argument(
+            '--os-tenant-name',
+            action='store',
+            dest='tenant_name',
+            default=env('OS_TENANT_NAME', 'Default'),
+            help='Authentication tenant name (Env: OS_TENANT_NAME)'
+        )
+        parser.add_argument(
+            '--os-auth-token',
+            action='store',
+            dest='token',
+            default=env('OS_AUTH_TOKEN'),
+            help='Authentication token (Env: OS_AUTH_TOKEN)'
+        )
+        parser.add_argument(
+            '--os-auth-url',
+            action='store',
+            dest='auth_url',
+            default=env('OS_AUTH_URL', default='http://localhost:5000/v3'),
+            help='Authentication URL (Env: OS_AUTH_URL)'
+        )
+        return parser
+
+    def initialize_app(self, argv):
+        self.client = Client(mistral_url=self.options.mistral_url,
+                             username=self.options.username,
+                             api_key=self.options.password,
+                             project_name=self.options.tenant_name,
+                             auth_url=self.options.auth_url,
+                             project_id=self.options.tenant_id,
+                             endpoint_type='publicURL',
+                             service_type='workflow',
+                             input_auth_token=self.options.token)
+
+
+def main(argv=sys.argv[1:]):
+    return MistralShell().run(argv)
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/mistralclient/tests/base.py b/mistralclient/tests/base.py
index 6090535d..7688b6c3 100644
--- a/mistralclient/tests/base.py
+++ b/mistralclient/tests/base.py
@@ -59,3 +59,14 @@ class BaseClientTest(unittest2.TestCase):
     def mock_http_delete(self, status_code=204):
         self._client.http_client.delete = \
             mock.MagicMock(return_value=FakeResponse(status_code))
+
+
+class BaseCommandTest(unittest2.TestCase):
+    def setUp(self):
+        self.app = mock.Mock()
+        self.app.client = mock.Mock()
+
+    def call(self, command, app_args=[], prog_name=''):
+        cmd = command(self.app, app_args)
+        parsed_args = cmd.get_parser(prog_name).parse_args(app_args)
+        return cmd.take_action(parsed_args)
diff --git a/mistralclient/tests/test_cli_executions.py b/mistralclient/tests/test_cli_executions.py
new file mode 100644
index 00000000..51d5fe77
--- /dev/null
+++ b/mistralclient/tests/test_cli_executions.py
@@ -0,0 +1,70 @@
+# Copyright 2014 StackStorm, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+import mock
+
+from mistralclient.tests import base
+from mistralclient.commands import executions
+from mistralclient.api.executions import Execution
+
+EXECUTION = Execution(mock, {
+    'id': '123',
+    'workbook_name': 'some',
+    'task': 'else',
+    'state': 'RUNNING'
+})
+
+
+class TestCLIExecutions(base.BaseCommandTest):
+    @mock.patch('mistralclient.api.executions.ExecutionManager.create')
+    def test_create(self, mock):
+        mock.return_value = EXECUTION
+
+        result = self.call(executions.Create,
+                           app_args=['name', 'id', '{ "context": true }'])
+
+        self.assertEqual(('123', 'some', 'else', 'RUNNING'), result[1])
+
+    @mock.patch('mistralclient.api.executions.ExecutionManager.update')
+    def test_update(self, mock):
+        mock.return_value = EXECUTION
+
+        result = self.call(executions.Update,
+                           app_args=['name', 'id', 'SUCCESS'])
+
+        self.assertEqual(('123', 'some', 'else', 'RUNNING'), result[1])
+
+    @mock.patch('mistralclient.api.executions.ExecutionManager.list')
+    def test_list(self, mock):
+        mock.return_value = (EXECUTION,)
+
+        result = self.call(executions.List, app_args=['name'])
+
+        self.assertEqual([('123', 'some', 'else', 'RUNNING')], result[1])
+
+    @mock.patch('mistralclient.api.executions.ExecutionManager.get')
+    def test_get(self, mock):
+        mock.return_value = EXECUTION
+
+        result = self.call(executions.Get, app_args=['name', 'id'])
+
+        self.assertEqual(('123', 'some', 'else', 'RUNNING'), result[1])
+
+    @mock.patch('mistralclient.api.executions.ExecutionManager.delete')
+    def test_delete(self, mock):
+        result = self.call(executions.Delete, app_args=['name', 'id'])
+
+        self.assertIsNone(result)
diff --git a/mistralclient/tests/test_cli_tasks.py b/mistralclient/tests/test_cli_tasks.py
new file mode 100644
index 00000000..47b72218
--- /dev/null
+++ b/mistralclient/tests/test_cli_tasks.py
@@ -0,0 +1,65 @@
+# Copyright 2014 StackStorm, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+import mock
+
+from mistralclient.tests import base
+
+from mistralclient.commands import tasks
+from mistralclient.api.tasks import Task
+
+TASK = Task(mock, {
+    'id': '123',
+    'workbook_name': 'some',
+    'execution_id': 'thing',
+    'name': 'else',
+    'description': 'keeps',
+    'state': 'RUNNING',
+    'tags': ['a', 'b'],
+})
+
+
+class TestCLIExecutions(base.BaseCommandTest):
+    @mock.patch('mistralclient.api.tasks.TaskManager.update')
+    def test_update(self, mock):
+        mock.return_value = TASK
+
+        result = self.call(tasks.Update,
+                           app_args=['workbook', 'executor', 'id', 'ERROR'])
+
+        self.assertEqual(
+            ('123', 'some', 'thing', 'else', 'keeps', 'RUNNING', 'a, b'),
+            result[1])
+
+    @mock.patch('mistralclient.api.tasks.TaskManager.list')
+    def test_list(self, mock):
+        mock.return_value = (TASK,)
+
+        result = self.call(tasks.List, app_args=['workbook', 'executor'])
+
+        self.assertEqual(
+            [('123', 'some', 'thing', 'else', 'keeps', 'RUNNING', 'a, b')],
+            result[1])
+
+    @mock.patch('mistralclient.api.tasks.TaskManager.get')
+    def test_get(self, mock):
+        mock.return_value = TASK
+
+        result = self.call(tasks.Get, app_args=['workbook', 'executor', 'id'])
+
+        self.assertEqual(
+            ('123', 'some', 'thing', 'else', 'keeps', 'RUNNING', 'a, b'),
+            result[1])
diff --git a/mistralclient/tests/test_cli_workbooks.py b/mistralclient/tests/test_cli_workbooks.py
new file mode 100644
index 00000000..b411134d
--- /dev/null
+++ b/mistralclient/tests/test_cli_workbooks.py
@@ -0,0 +1,87 @@
+# Copyright 2014 StackStorm, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+import mock
+
+from mistralclient.tests import base
+
+from mistralclient.commands import workbooks
+from mistralclient.api.workbooks import Workbook
+
+WORKBOOK = Workbook(mock, {
+    'name': 'a',
+    'description': 'some',
+    'tags': ['a', 'b']
+})
+
+
+class TestCLIWorkbooks(base.BaseCommandTest):
+    @mock.patch('mistralclient.api.workbooks.WorkbookManager.create')
+    def test_create(self, mock):
+        mock.return_value = WORKBOOK
+
+        result = self.call(workbooks.Create, app_args=['name'])
+
+        self.assertEqual(('a', 'some', 'a, b'), result[1])
+
+    @mock.patch('mistralclient.api.workbooks.WorkbookManager.update')
+    def test_update(self, mock):
+        mock.return_value = WORKBOOK
+
+        result = self.call(workbooks.Update, app_args=['name'])
+
+        self.assertEqual(('a', 'some', 'a, b'), result[1])
+
+    @mock.patch('mistralclient.api.workbooks.WorkbookManager.list')
+    def test_list(self, mock):
+        mock.return_value = (WORKBOOK,)
+
+        result = self.call(workbooks.List)
+
+        self.assertEqual([('a', 'some', 'a, b')], result[1])
+
+    @mock.patch('mistralclient.api.workbooks.WorkbookManager.get')
+    def test_get(self, mock):
+        mock.return_value = WORKBOOK
+
+        result = self.call(workbooks.Get, app_args=['name'])
+
+        self.assertEqual(('a', 'some', 'a, b'), result[1])
+
+    @mock.patch('mistralclient.api.workbooks.WorkbookManager.delete')
+    def test_delete(self, mock):
+        self.assertIsNone(self.call(workbooks.Delete, app_args=['name']))
+
+    @mock.patch('argparse.open', create=True)
+    @mock.patch(
+        'mistralclient.api.workbooks.WorkbookManager.upload_definition'
+    )
+    def test_upload_definition(self, mock, mock_open):
+        mock.return_value = WORKBOOK
+        mock_open.return_value = mock.MagicMock(spec=file)
+
+        result = self.call(workbooks.UploadDefinition,
+                           app_args=['name', '1.txt'])
+
+        self.assertIsNone(result)
+
+    @mock.patch('mistralclient.api.workbooks.WorkbookManager.get_definition')
+    def test_get_definition(self, mock):
+        mock.return_value = 'sometext'
+
+        self.call(workbooks.GetDefinition, app_args=['name'])
+
+        self.app.stdout.write.assert_called_with('sometext')
diff --git a/openstack-common.conf b/openstack-common.conf
index 29d311bd..9849be28 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -2,6 +2,8 @@
 
 # The list of modules to copy from oslo-incubator.git
 # TODO(rakhmerov): We'll need to use apiclient later.
+module=cliutils
+module=log
 
 # The base module to hold the copy of openstack.common
 base=mistralclient
diff --git a/requirements.txt b/requirements.txt
index 3896e5f7..9ce8cf9e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 pbr>=0.5.21,<1.0
 requests
-python-keystoneclient>=0.3.2
\ No newline at end of file
+python-keystoneclient>=0.3.2
+cliff>=1.5.2
diff --git a/setup.cfg b/setup.cfg
index c0d724e9..b683672b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,6 +20,10 @@ author-email = openstack-dev@lists.openstack.org
 packages =
     mistralclient
 
+[entry_points]
+console_scripts =
+    mistral = mistralclient.shell:main
+
 [nosetests]
 cover-package = mistralclient