diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 00000000..a23c8bd4
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,4 @@
+cliff
+jsonschema>=0.7
+requests
+python-keystoneclient>=0.2.0
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100644
index 00000000..5004b8bf
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+#
+# monikerclient documentation build configuration file, created by
+# sphinx-quickstart on Wed Oct 31 18:58:17 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'monikerclient'
+copyright = u'2012, Managed I.T.'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+from monikerclient.version import version_info as monikerclient_version
+version = monikerclient_version.canonical_version_string()
+# The full version, including alpha/beta/rc tags.
+release = monikerclient_version.version_string_with_vcs()
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'monikerclientdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'monikerclient.tex', u'Moniker Client Documentation',
+   u'Managed I.T.', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'monikerclient', u'Moniker Client Documentation',
+     [u'Managed I.T.'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'monikerclient', u'Moniker Client Documentation',
+   u'Managed I.T.', 'monikerclient', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
diff --git a/doc/source/index.rst b/doc/source/index.rst
new file mode 100644
index 00000000..1d3ef9e9
--- /dev/null
+++ b/doc/source/index.rst
@@ -0,0 +1,21 @@
+.. moniker documentation master file, created by
+   sphinx-quickstart on Wed Oct 31 18:58:17 2012.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to monikerclients's documentation!
+===================================
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/monikerclient/auth.py b/monikerclient/auth.py
new file mode 100644
index 00000000..36c7e3c5
--- /dev/null
+++ b/monikerclient/auth.py
@@ -0,0 +1,94 @@
+# Copyright 2012 Managed I.T.
+#
+# Author: Kiall Mac Innes <kiall@managedit.ie>
+#
+# 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 urlparse import urlparse
+from requests.auth import AuthBase
+
+from keystoneclient.v2_0.client import Client
+
+
+class KeystoneAuth(AuthBase):
+    def __init__(self, auth_url, username=None, password=None, tenant_id=None,
+                 tenant_name=None, token=None, service_type=None,
+                 endpoint_type=None):
+        self.auth_url = str(auth_url).rstrip('/')
+        self.username = username
+        self.password = password
+        self.tenant_id = tenant_id
+        self.tenant_name = tenant_name
+        self.token = token
+
+        if (not username and not password) and not token:
+            raise ValueError('A username and password, or token is required')
+
+        if not service_type or not endpoint_type:
+            raise ValueError("Need service_type and/or endpoint_type")
+
+        self.service_type = service_type
+        self.endpoint_type = endpoint_type
+
+        self.refresh_auth()
+
+    def __call__(self, request):
+        if not self.token:
+            self.refresh_auth()
+
+        request.headers['X-Auth-Token'] = self.token
+
+        return request
+
+    def get_ksclient(self):
+        insecure = urlparse(self.auth_url).scheme != 'https'
+
+        return Client(username=self.username,
+                      password=self.password,
+                      tenant_id=self.tenant_id,
+                      tenant_name=self.tenant_name,
+                      auth_url=self.auth_url,
+                      insecure=insecure)
+
+    def get_endpoints(self, service_type=None, endpoint_type=None):
+        return self.service_catalog.get_endpoints(
+            service_type=service_type,
+            endpoint_type=endpoint_type)
+
+    def get_url(self, service_type=None, endpoint_type=None):
+        service_type = service_type or self.service_type
+        endpoint_type = endpoint_type or self.endpoint_type
+        endpoints = self.get_endpoints(service_type, endpoint_type)
+
+        return endpoints[service_type][0][endpoint_type].rstrip('/')
+
+    def refresh_auth(self):
+        ks = self.get_ksclient()
+        self.token = ks.auth_token
+        self.service_catalog = ks.service_catalog
+
+    def args_hook(self, args):
+        url = urlparse(args['url'])
+
+        if str(url.scheme) == '':
+            if not self.token:
+                self.refresh_token()
+
+            endpoints = self.get_endpoints()
+
+            if url.netloc in endpoints.keys():
+
+                args['url'] = '%s/%s?%s' % (
+                    self.get_url(),
+                    url.path.lstrip('/'),
+                    url.query
+                )
diff --git a/monikerclient/cli/base.py b/monikerclient/cli/base.py
new file mode 100644
index 00000000..a5d7993c
--- /dev/null
+++ b/monikerclient/cli/base.py
@@ -0,0 +1,94 @@
+# Copyright 2012 Managed I.T.
+#
+# Author: Kiall Mac Innes <kiall@managedit.ie>
+#
+# 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 abc
+from cliff.command import Command as CliffCommand
+from cliff.lister import Lister
+from cliff.show import ShowOne
+from monikerclient.v1 import Client
+
+
+class Command(CliffCommand):
+    __metaclass__ = abc.ABCMeta
+
+    def run(self, parsed_args):
+        client_args = {
+            'endpoint': self.app.options.os_endpoint,
+            'auth_url': self.app.options.os_auth_url,
+            'username': self.app.options.os_username,
+            'password': self.app.options.os_password,
+            'tenant_id': self.app.options.os_tenant_id,
+            'tenant_name': self.app.options.os_tenant_name,
+            'token': self.app.options.os_token,
+            'region_name': self.app.options.os_region_name,
+        }
+
+        self.client = Client(**client_args)
+
+        return super(Command, self).run(parsed_args)
+
+    @abc.abstractmethod
+    def execute(self, parsed_args):
+        """
+        Execute something, this is since we overload self.take_action()
+        in order to format the data
+
+        This method __NEEDS__ to be overloaded!
+
+        :param parsed_args: The parsed args that are given by take_action()
+        """
+
+    def post_execute(self, data):
+        """
+        Format the results locally if needed, by default we just return data
+
+        :param data: Whatever is returned by self.execute()
+        """
+        return data
+
+    def take_action(self, parsed_args):
+        # TODO: Common Exception Handling Here
+        results = self.execute(parsed_args)
+        return self.post_execute(results)
+
+
+class ListCommand(Command, Lister):
+    def post_execute(self, results):
+        if len(results) > 0:
+            column_names = results[0].keys()
+            data = [r.values() for r in results]
+
+            return column_names, data
+        else:
+            return [], ()
+
+
+class GetCommand(Command, ShowOne):
+    def post_execute(self, results):
+        return results.keys(), results.values()
+
+
+class CreateCommand(Command, ShowOne):
+    def post_execute(self, results):
+        return results.keys(), results.values()
+
+
+class UpdateCommand(Command, ShowOne):
+    def post_execute(self, results):
+        return results.keys(), results.values()
+
+
+class DeleteCommand(Command):
+    pass
diff --git a/monikerclient/cli/domains.py b/monikerclient/cli/domains.py
new file mode 100644
index 00000000..83df27c6
--- /dev/null
+++ b/monikerclient/cli/domains.py
@@ -0,0 +1,98 @@
+# Copyright 2012 Managed I.T.
+#
+# Author: Kiall Mac Innes <kiall@managedit.ie>
+#
+# 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 logging
+from monikerclient.cli import base
+from monikerclient.v1.domains import Domain
+
+LOG = logging.getLogger(__name__)
+
+
+class ListDomainsCommand(base.ListCommand):
+    """ List Domains """
+
+    def execute(self, parsed_args):
+        return self.client.domains.list()
+
+
+class GetDomainCommand(base.GetCommand):
+    """ Get Domain """
+
+    def get_parser(self, prog_name):
+        parser = super(GetDomainCommand, self).get_parser(prog_name)
+
+        parser.add_argument('--domain-id', help="Domain ID", required=True)
+
+        return parser
+
+    def execute(self, parsed_args):
+        return self.client.domains.get(parsed_args.domain_id)
+
+
+class CreateDomainCommand(base.CreateCommand):
+    """ Create Domain """
+
+    def get_parser(self, prog_name):
+        parser = super(CreateDomainCommand, self).get_parser(prog_name)
+
+        parser.add_argument('--domain-name', help="Domain Name", required=True)
+        parser.add_argument('--domain-email', help="Domain Email",
+                            required=True)
+
+        return parser
+
+    def execute(self, parsed_args):
+        domain = Domain(
+            name=parsed_args.domain_name,
+            email=parsed_args.domain_email
+        )
+
+        return self.client.domains.create(domain)
+
+
+class UpdateDomainCommand(base.UpdateCommand):
+    """ Update Domain """
+
+    def get_parser(self, prog_name):
+        parser = super(UpdateDomainCommand, self).get_parser(prog_name)
+
+        parser.add_argument('--domain-id', help="Domain ID", required=True)
+        parser.add_argument('--domain-name', help="Domain Name")
+        parser.add_argument('--domain-email', help="Domain Email")
+
+        return parser
+
+    def execute(self, parsed_args):
+        # TODO: API needs updating.. this get is silly
+        domain = self.client.domains.get(parsed_args.domain_id)
+
+        # TODO: How do we tell if an arg was supplied or intentionally set to
+        #       None?
+
+        return self.client.domains.update(domain)
+
+
+class DeleteDomainCommand(base.DeleteCommand):
+    """ Delete Domain """
+
+    def get_parser(self, prog_name):
+        parser = super(DeleteDomainCommand, self).get_parser(prog_name)
+
+        parser.add_argument('--domain-id', help="Domain ID")
+
+        return parser
+
+    def execute(self, parsed_args):
+        return self.client.domains.delete(parsed_args.domain_id)
diff --git a/monikerclient/exceptions.py b/monikerclient/exceptions.py
index 09ebc14d..bf0ea252 100644
--- a/monikerclient/exceptions.py
+++ b/monikerclient/exceptions.py
@@ -21,3 +21,23 @@ class Base(Exception):
 
 class ResourceNotFound(Base):
     pass
+
+
+class RemoteError(Base):
+    pass
+
+
+class Unknown(RemoteError):
+    pass
+
+
+class Forbidden(RemoteError):
+    pass
+
+
+class Conflict(RemoteError):
+    pass
+
+
+class NotFound(RemoteError):
+    pass
diff --git a/monikerclient/shell.py b/monikerclient/shell.py
index b8710cb9..c5ddaadf 100644
--- a/monikerclient/shell.py
+++ b/monikerclient/shell.py
@@ -13,7 +13,7 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations
 # under the License.
-
+import os
 from cliff.app import App
 from cliff.commandmanager import CommandManager
 
@@ -25,3 +25,41 @@ class MonikerShell(App):
             version='0.1',
             command_manager=CommandManager('moniker.cli'),
         )
+
+    def build_option_parser(self, description, version, argparse_kwargs=None):
+        parser = super(MonikerShell, self).build_option_parser(
+            description, version, argparse_kwargs)
+
+        parser.add_argument('--os-endpoint',
+                            default=os.environ.get('OS_SERVICE_ENDPOINT'),
+                            help="Defaults to env[OS_SERVICE_ENDPOINT]")
+
+        parser.add_argument('--os-auth-url',
+                            default=os.environ.get('OS_AUTH_URL'),
+                            help="Defaults to env[OS_AUTH_URL]")
+
+        parser.add_argument('--os-username',
+                            default=os.environ.get('OS_USERNAME'),
+                            help="Defaults to env[OS_USERNAME]")
+
+        parser.add_argument('--os-password',
+                            default=os.environ.get('OS_PASSWORD'),
+                            help="Defaults to env[OS_PASSWORD]")
+
+        parser.add_argument('--os-tenant-id',
+                            default=os.environ.get('OS_TENANT_ID'),
+                            help="Defaults to env[OS_TENANT_ID]")
+
+        parser.add_argument('--os-tenant-name',
+                            default=os.environ.get('OS_TENANT_NAME'),
+                            help="Defaults to env[OS_TENANT_NAME]")
+
+        parser.add_argument('--os-token',
+                            default=os.environ.get('OS_SERVICE_TOKEN'),
+                            help="Defaults to env[OS_SERVICE_TOKEN]")
+
+        parser.add_argument('--os-region-name',
+                            default=os.environ.get('OS_REGION_NAME'),
+                            help="Defaults to env[OS_REGION_NAME]")
+
+        return parser
diff --git a/monikerclient/tests/__init__.py b/monikerclient/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/monikerclient/v1/__init__.py b/monikerclient/v1/__init__.py
index e69de29b..70a7617e 100644
--- a/monikerclient/v1/__init__.py
+++ b/monikerclient/v1/__init__.py
@@ -0,0 +1,98 @@
+# Copyright 2012 Managed I.T.
+#
+# Author: Kiall Mac Innes <kiall@managedit.ie>
+#
+# 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 requests
+from urlparse import urlparse
+from monikerclient import exceptions
+from monikerclient.auth import KeystoneAuth
+from monikerclient.v1 import domains
+from monikerclient.v1 import records
+from monikerclient.v1 import servers
+
+
+class Client(object):
+    """ Client for the Moniker v1 API """
+
+    def __init__(self, endpoint=None, auth_url=None, username=None,
+                 password=None, tenant_id=None, tenant_name=None, token=None,
+                 region_name=None, endpoint_type='publicURL'):
+        """
+        :param endpoint: Endpoint URL
+        :param auth_url: Keystone auth_url
+        :param username: The username to auth with
+        :param password: The password to auth with
+        :param tenant_id: The tenant ID
+        :param tenant_name: The tenant name
+        :param token: A token instead of username / password
+        :param region_name: The region name
+        :param endpoint_type: The endpoint type (publicURL for example)
+        """
+        if auth_url:
+            auth = KeystoneAuth(auth_url, username, password, tenant_id,
+                                tenant_name, token, 'dns', endpoint_type)
+            endpoint = auth.get_url()
+        elif endpoint:
+            auth = None
+        else:
+            raise ValueError('Either an auth_url or endpoint must be supplied')
+
+        headers = {'Content-Type': 'application/json'}
+
+        def _ensure_url_hook(args):
+            url_ = urlparse(args['url'])
+            if not url_.scheme:
+                args['url'] = endpoint + url_.path
+
+        hooks = {'args': _ensure_url_hook}
+
+        self.requests = requests.session(
+            auth=auth,
+            headers=headers,
+            hooks=hooks)
+
+        self.domains = domains.DomainsController(client=self)
+        self.records = records.RecordsController(client=self)
+        self.servers = servers.ServersController(client=self)
+
+    def wrap_api_call(self, func, *args, **kw):
+        """
+        Wrap a self.<rest function> with exception handling
+
+        :param func: The function to wrap
+        """
+        response = func(*args, **kw)
+
+        if response.status_code in (401, 403):
+            raise exceptions.Forbidden()
+        elif response.status_code == 404:
+            raise exceptions.NotFound()
+        elif response.status_code == 409:
+            raise exceptions.Conflict()
+        elif response.status_code == 500:
+            raise exceptions.Unknown()
+        else:
+            return response
+
+    def get(self, path, **kw):
+        return self.wrap_api_call(self.requests.get, path, **kw)
+
+    def post(self, path, **kw):
+        return self.wrap_api_call(self.requests.post, path, **kw)
+
+    def put(self, path, **kw):
+        return self.wrap_api_call(self.requests.put, path, **kw)
+
+    def delete(self, path, **kw):
+        return self.wrap_api_call(self.requests.delete, path, **kw)
diff --git a/monikerclient/v1/base.py b/monikerclient/v1/base.py
new file mode 100644
index 00000000..09d624bd
--- /dev/null
+++ b/monikerclient/v1/base.py
@@ -0,0 +1,53 @@
+# Copyright 2012 Managed I.T.
+#
+# Author: Kiall Mac Innes <kiall@managedit.ie>
+#
+# 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 abc
+
+
+class Controller(object):
+    __metaclass__ = abc.ABCMeta
+
+    def __init__(self, client):
+        self.client = client
+
+    def list(self, *args, **kw):
+        """
+        List something
+        """
+        raise NotImplementedError
+
+    def get(self, *args, **kw):
+        """
+        Get something
+        """
+        raise NotImplementedError
+
+    def create(self, *args, **kw):
+        """
+        Create something
+        """
+        raise NotImplementedError
+
+    def update(self, *args, **kw):
+        """
+        Update something
+        """
+        raise NotImplementedError
+
+    def delete(self, *args, **kw):
+        """
+        Delete something
+        """
+        raise NotImplementedError
diff --git a/monikerclient/v1/client.py b/monikerclient/v1/client.py
deleted file mode 100644
index e40f4a97..00000000
--- a/monikerclient/v1/client.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright 2012 Managed I.T.
-#
-# Author: Kiall Mac Innes <kiall@managedit.ie>
-#
-# 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 monikerclient.v1 import domains
-from monikerclient.v1 import records
-from monikerclient.v1 import servers
-
-
-class Client(object):
-    """ Client for the Moniker v1 API """
-
-    def __init__(self):
-        self.domains = domains.Controller()
-        self.records = records.Controller()
-        self.servers = servers.Controller()
diff --git a/monikerclient/v1/domains.py b/monikerclient/v1/domains.py
index c88de493..b5551056 100644
--- a/monikerclient/v1/domains.py
+++ b/monikerclient/v1/domains.py
@@ -13,20 +13,25 @@
 # 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 monikerclient import warlock
 from monikerclient import utils
+from monikerclient.v1.base import Controller
 
 
 Domain = warlock.model_factory(utils.load_schema('v1', 'domain'))
 
 
-class Controller(object):
+class DomainsController(Controller):
     def list(self):
         """
         Retrieve a list of domains
 
         :returns: A list of :class:`Domain`s
         """
+        response = self.client.get('/domains')
+
+        return [Domain(i) for i in response.json['domains']]
 
     def get(self, domain_id):
         """
@@ -35,6 +40,9 @@ class Controller(object):
         :param domain_id: Domain Identifier
         :returns: :class:`Domain`
         """
+        response = self.client.get('/domains/%s' % domain_id)
+
+        return Domain(response.json)
 
     def create(self, domain):
         """
@@ -43,6 +51,9 @@ class Controller(object):
         :param domain: A :class:`Domain` to create
         :returns: :class:`Domain`
         """
+        response = self.client.post('/domains', data=json.dumps(domain))
+
+        return Domain(response.json)
 
     def update(self, domain):
         """
@@ -51,6 +62,10 @@ class Controller(object):
         :param domain: A :class:`Domain` to update
         :returns: :class:`Domain`
         """
+        response = self.client.put('/domains/%s' % domain.id,
+                                   data=json.dumps(domain.changes))
+
+        return Domain(response.json)
 
     def delete(self, domain):
         """
@@ -58,3 +73,7 @@ class Controller(object):
 
         :param domain: A :class:`Domain`, or Domain Identifier to delete
         """
+        if isinstance(domain, Domain):
+            self.client.delete('/domains/%s' % domain.id)
+        else:
+            self.client.delete('/domains/%s' % domain)
diff --git a/monikerclient/v1/records.py b/monikerclient/v1/records.py
index d7b56a11..57a4cbba 100644
--- a/monikerclient/v1/records.py
+++ b/monikerclient/v1/records.py
@@ -13,20 +13,25 @@
 # 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 monikerclient import warlock
 from monikerclient import utils
+from monikerclient.v1.base import Controller
 
 
 Record = warlock.model_factory(utils.load_schema('v1', 'record'))
 
 
-class Controller(object):
+class RecordsController(Controller):
     def list(self):
         """
         Retrieve a list of records
 
         :returns: A list of :class:`Record`s
         """
+        response = self.client.get('/records')
+
+        return [Record(i) for i in response.json['records']]
 
     def get(self, record_id):
         """
@@ -35,6 +40,9 @@ class Controller(object):
         :param record_id: Record Identifier
         :returns: :class:`Record`
         """
+        response = self.client.get('/records/%s' % record_id)
+
+        return Record(response.json)
 
     def create(self, record):
         """
@@ -43,6 +51,9 @@ class Controller(object):
         :param record: A :class:`Record` to create
         :returns: :class:`Record`
         """
+        response = self.client.post('/records', data=json.dumps(record))
+
+        return record.update(response.json)
 
     def update(self, record):
         """
@@ -51,6 +62,10 @@ class Controller(object):
         :param record: A :class:`Record` to update
         :returns: :class:`Record`
         """
+        response = self.client.put('/records/%s' % record.id,
+                                   data=json.dumps(record))
+
+        return record.update(response.json)
 
     def delete(self, record):
         """
@@ -58,3 +73,7 @@ class Controller(object):
 
         :param record: A :class:`Record`, or Record Identifier to delete
         """
+        if isinstance(record, Record):
+            self.client.delete('/records/%s' % record.id)
+        else:
+            self.client.delete('/records/%s' % record)
diff --git a/monikerclient/v1/servers.py b/monikerclient/v1/servers.py
index ec2923e5..3c98b121 100644
--- a/monikerclient/v1/servers.py
+++ b/monikerclient/v1/servers.py
@@ -13,20 +13,25 @@
 # 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 monikerclient import warlock
 from monikerclient import utils
+from monikerclient.v1.base import Controller
 
 
 Server = warlock.model_factory(utils.load_schema('v1', 'server'))
 
 
-class Controller(object):
+class ServersController(Controller):
     def list(self):
         """
         Retrieve a list of servers
 
         :returns: A list of :class:`Server`s
         """
+        response = self.client.get('/servers')
+
+        return [Server(i) for i in response.json['servers']]
 
     def get(self, server_id):
         """
@@ -35,6 +40,9 @@ class Controller(object):
         :param server_id: Server Identifier
         :returns: :class:`Server`
         """
+        response = self.client.get('/servers/%s' % server_id)
+
+        return Server(response.json)
 
     def create(self, server):
         """
@@ -43,6 +51,9 @@ class Controller(object):
         :param server: A :class:`Server` to create
         :returns: :class:`Server`
         """
+        response = self.client.post('/servers', data=json.dumps(server))
+
+        return server.update(response.json)
 
     def update(self, server):
         """
@@ -51,6 +62,10 @@ class Controller(object):
         :param server: A :class:`Server` to update
         :returns: :class:`Server`
         """
+        response = self.client.put('/servers/%s' % server.id,
+                                   data=json.dumps(server))
+
+        return server.update(response.json)
 
     def delete(self, server):
         """
@@ -58,3 +73,7 @@ class Controller(object):
 
         :param server: A :class:`Server`, or Server Identifier to delete
         """
+        if isinstance(server, Server):
+            self.client.delete('/servers/%s' % server.id)
+        else:
+            self.client.delete('/servers/%s' % server)
diff --git a/monikerclient/warlock.py b/monikerclient/warlock.py
index e6db38ee..7fa0616e 100644
--- a/monikerclient/warlock.py
+++ b/monikerclient/warlock.py
@@ -113,6 +113,9 @@ def model_factory(schema):
         def itervalues(self):
             return copy.deepcopy(dict(self)).itervalues()
 
+        def keys(self):
+            return copy.deepcopy(dict(self)).keys()
+
         def values(self):
             return copy.deepcopy(dict(self)).values()
 
diff --git a/setup.cfg b/setup.cfg
index f17eadfa..f0188f04 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,3 +6,11 @@ cover-inclusive=true
 verbosity=2
 detailed-errors=1
 where=monikerclient/tests
+
+[build_sphinx]
+source-dir = doc/source
+build-dir  = doc/build
+all_files  = 1
+
+[upload_docs]
+upload-dir = doc/build/html
diff --git a/setup.py b/setup.py
index bd6e49d4..1e76ba9d 100755
--- a/setup.py
+++ b/setup.py
@@ -14,6 +14,7 @@
 # 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 textwrap
 from setuptools import setup, find_packages
 from monikerclient.openstack.common import setup as common_setup
 from monikerclient.version import version_info as version
@@ -46,6 +47,14 @@ setup(
         'bin/moniker',
     ],
     cmdclass=common_setup.get_cmdclass(),
+    entry_points=textwrap.dedent("""
+        [moniker.cli]
+        domain-list = monikerclient.cli.domains:ListDomainsCommand
+        domain-get = monikerclient.cli.domains:GetDomainCommand
+        domain-create = monikerclient.cli.domains:CreateDomainCommand
+        domain-update = monikerclient.cli.domains:UpdateDomainCommand
+        domain-delete = monikerclient.cli.domains:DeleteDomainCommand
+        """),
     classifiers=[
         'Development Status :: 3 - Alpha',
         'Topic :: Internet :: Name Service (DNS)',
diff --git a/tools/pip-requires b/tools/pip-requires
index 877b6394..a23c8bd4 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -1,2 +1,4 @@
 cliff
 jsonschema>=0.7
+requests
+python-keystoneclient>=0.2.0
diff --git a/tools/test-requires b/tools/test-requires
index 4828b5d9..daf9e637 100644
--- a/tools/test-requires
+++ b/tools/test-requires
@@ -1,3 +1,4 @@
 nose
 mox
 openstack.nose_plugin
+sphinx