diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py
new file mode 100644
index 0000000000..4cea5a06cc
--- /dev/null
+++ b/openstackclient/common/restapi.py
@@ -0,0 +1,188 @@
+#   Copyright 2013 Nebula 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.
+#
+
+"""REST API bits"""
+
+import json
+import logging
+import requests
+
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+_logger = logging.getLogger(__name__)
+
+
+class RESTApi(object):
+    """A REST api client that handles the interface from us to the server
+
+    RESTApi is an extension of a requests.Session that knows
+    how to do:
+    * JSON serialization/deserialization
+    * log requests in 'curl' format
+    * basic API boilerplate for create/delete/list/set/show verbs
+
+    * authentication is handled elsewhere and a token is passed in
+
+    The expectation that there will be a RESTApi object per authentication
+    token in use, i.e. project/username/auth_endpoint
+
+    On the other hand, a Client knows details about the specific REST Api that
+    it communicates with, such as the available endpoints, API versions, etc.
+    """
+
+    USER_AGENT = 'RAPI'
+
+    def __init__(
+        self,
+        os_auth=None,
+        user_agent=USER_AGENT,
+        debug=None,
+        **kwargs
+    ):
+        self.set_auth(os_auth)
+        self.debug = debug
+        self.session = requests.Session(**kwargs)
+
+        self.set_header('User-Agent', user_agent)
+        self.set_header('Content-Type', 'application/json')
+
+    def set_auth(self, os_auth):
+        """Sets the current auth blob"""
+        self.os_auth = os_auth
+
+    def set_header(self, header, content):
+        """Sets passed in headers into the session headers
+
+        Replaces existing headers!!
+        """
+        if content is None:
+            del self.session.headers[header]
+        else:
+            self.session.headers[header] = content
+
+    def request(self, method, url, **kwargs):
+        if self.os_auth:
+            self.session.headers.setdefault('X-Auth-Token', self.os_auth)
+        if 'data' in kwargs and isinstance(kwargs['data'], type({})):
+            kwargs['data'] = json.dumps(kwargs['data'])
+        log_request(method, url, headers=self.session.headers, **kwargs)
+        response = self.session.request(method, url, **kwargs)
+        log_response(response)
+        return self._error_handler(response)
+
+    def create(self, url, data=None, response_key=None, **kwargs):
+        response = self.request('POST', url, data=data, **kwargs)
+        if response_key:
+            return response.json()[response_key]
+        else:
+            return response.json()
+
+        #with self.completion_cache('human_id', self.resource_class, mode="a"):
+        #    with self.completion_cache('uuid', self.resource_class, mode="a"):
+        #        return self.resource_class(self, body[response_key])
+
+    def delete(self, url):
+        self.request('DELETE', url)
+
+    def list(self, url, data=None, response_key=None, **kwargs):
+        if data:
+            response = self.request('POST', url, data=data, **kwargs)
+        else:
+            kwargs.setdefault('allow_redirects', True)
+            response = self.request('GET', url, **kwargs)
+
+        return response.json()[response_key]
+
+        ###hack this for keystone!!!
+        #data = body[response_key]
+        # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
+        #           unlike other services which just return the list...
+        #if isinstance(data, dict):
+        #    try:
+        #        data = data['values']
+        #    except KeyError:
+        #        pass
+
+        #with self.completion_cache('human_id', obj_class, mode="w"):
+        #    with self.completion_cache('uuid', obj_class, mode="w"):
+        #        return [obj_class(self, res, loaded=True)
+        #                for res in data if res]
+
+    def set(self, url, data=None, response_key=None, **kwargs):
+        response = self.request('PUT', url, data=data)
+        if data:
+            if response_key:
+                return response.json()[response_key]
+            else:
+                return response.json()
+        else:
+            return None
+
+    def show(self, url, response_key=None, **kwargs):
+        response = self.request('GET', url, **kwargs)
+        if response_key:
+            return response.json()[response_key]
+        else:
+            return response.json()
+
+    def _error_handler(self, response):
+        if response.status_code < 200 or response.status_code >= 300:
+            _logger.debug(
+                "ERROR: %s",
+                response.text,
+            )
+            response.raise_for_status()
+        return response
+
+
+def log_request(method, url, **kwargs):
+    # put in an early exit if debugging is not enabled?
+    if 'params' in kwargs and kwargs['params'] != {}:
+        url += '?' + urlencode(kwargs['params'])
+
+    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 log_response(response):
+    _logger.debug(
+        "RESP: [%s] %s\n",
+        response.status_code,
+        response.headers,
+    )
+    if response._content_consumed:
+        _logger.debug(
+            "RESP BODY: %s\n",
+            response.text,
+        )
+    _logger.debug(
+        "encoding: %s",
+        response.encoding,
+    )
diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py
index f72bb505a5..91a20895b2 100644
--- a/openstackclient/common/utils.py
+++ b/openstackclient/common/utils.py
@@ -115,6 +115,30 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
     return tuple(row)
 
 
+def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}):
+    """Return a tuple containing the item properties.
+
+    :param item: a single dict resource
+    :param fields: tuple of strings with the desired field names
+    :param mixed_case_fields: tuple of field names to preserve case
+    :param formatters: dictionary mapping field names to callables
+       to format the values
+    """
+    row = []
+
+    for field in fields:
+        if field in mixed_case_fields:
+            field_name = field.replace(' ', '_')
+        else:
+            field_name = field.lower().replace(' ', '_')
+        data = item[field_name] if field_name in item else ''
+        if field in formatters:
+            row.append(formatters[field](data))
+        else:
+            row.append(data)
+    return tuple(row)
+
+
 def string_to_bool(arg):
     return arg.strip().lower() in ('t', 'true', 'yes', '1')
 
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index b66611b163..91b02a2b0a 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -29,6 +29,7 @@ from openstackclient.common import clientmanager
 from openstackclient.common import commandmanager
 from openstackclient.common import exceptions as exc
 from openstackclient.common import openstackkeyring
+from openstackclient.common import restapi
 from openstackclient.common import utils
 
 
@@ -368,6 +369,9 @@ class OpenStackShell(app.App):
         if self.options.deferred_help:
             self.DeferredHelpAction(self.parser, self.parser, None, None)
 
+        # Set up common client session
+        self.restapi = restapi.RESTApi()
+
         # If the user is not asking for help, make sure they
         # have given us auth.
         cmd_name = None
@@ -376,6 +380,7 @@ class OpenStackShell(app.App):
             cmd_factory, cmd_name, sub_argv = cmd_info
         if self.interactive_mode or cmd_name != 'help':
             self.authenticate_user()
+            self.restapi.set_auth(self.client_manager.identity.auth_token)
 
     def prepare_to_run_command(self, cmd):
         """Set up auth and API versions"""
diff --git a/openstackclient/tests/common/__init__.py b/openstackclient/tests/common/__init__.py
new file mode 100644
index 0000000000..c534c012e8
--- /dev/null
+++ b/openstackclient/tests/common/__init__.py
@@ -0,0 +1,14 @@
+#   Copyright 2013 OpenStack Foundation
+#
+#   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.
+#
diff --git a/openstackclient/tests/common/test_restapi.py b/openstackclient/tests/common/test_restapi.py
new file mode 100644
index 0000000000..4b83ffa460
--- /dev/null
+++ b/openstackclient/tests/common/test_restapi.py
@@ -0,0 +1,320 @@
+#   Copyright 2013 Nebula 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.
+#
+
+"""Test rest module"""
+
+import json
+import mock
+
+import requests
+
+from openstackclient.common import restapi
+from openstackclient.tests import utils
+
+fake_auth = '11223344556677889900'
+fake_url = 'http://gopher.com'
+fake_key = 'gopher'
+fake_keys = 'gophers'
+fake_gopher_mac = {
+    'id': 'g1',
+    'name': 'mac',
+    'actor': 'Mel Blanc',
+}
+fake_gopher_tosh = {
+    'id': 'g2',
+    'name': 'tosh',
+    'actor': 'Stan Freeberg',
+}
+fake_gopher_single = {
+    fake_key: fake_gopher_mac,
+}
+fake_gopher_list = {
+    fake_keys:
+        [
+            fake_gopher_mac,
+            fake_gopher_tosh,
+        ]
+}
+
+
+class FakeResponse(requests.Response):
+    def __init__(self, headers={}, status_code=None, data=None, encoding=None):
+        super(FakeResponse, self).__init__()
+
+        self.status_code = status_code
+
+        self.headers.update(headers)
+        self._content = json.dumps(data)
+
+
+@mock.patch('openstackclient.common.restapi.requests.Session')
+class TestRESTApi(utils.TestCase):
+
+    def test_request_get(self, session_mock):
+        resp = FakeResponse(status_code=200, data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+        gopher = api.request('GET', fake_url)
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+        )
+        self.assertEqual(gopher.status_code, 200)
+        self.assertEqual(gopher.json(), fake_gopher_single)
+
+    def test_request_get_return_300(self, session_mock):
+        resp = FakeResponse(status_code=300, data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+        gopher = api.request('GET', fake_url)
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+        )
+        self.assertEqual(gopher.status_code, 300)
+        self.assertEqual(gopher.json(), fake_gopher_single)
+
+    def test_request_get_fail_404(self, session_mock):
+        resp = FakeResponse(status_code=404, data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+        self.assertRaises(requests.HTTPError, api.request, 'GET', fake_url)
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+        )
+
+    def test_request_get_auth(self, session_mock):
+        resp = FakeResponse(data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+            headers=mock.MagicMock(return_value={}),
+        )
+
+        api = restapi.RESTApi(os_auth=fake_auth)
+        gopher = api.request('GET', fake_url)
+        session_mock.return_value.headers.setdefault.assert_called_with(
+            'X-Auth-Token',
+            fake_auth,
+        )
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+        )
+        self.assertEqual(gopher.json(), fake_gopher_single)
+
+    def test_request_get_header(self, session_mock):
+        resp = FakeResponse(data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+            headers=mock.MagicMock(return_value={}),
+        )
+
+        api = restapi.RESTApi(user_agent='fake_agent')
+        api.set_header('X-Fake-Header', 'wb')
+        gopher = api.request('GET', fake_url)
+        session_mock.return_value.headers.__setitem__.assert_any_call(
+            'Content-Type',
+            'application/json',
+        )
+        session_mock.return_value.headers.__setitem__.assert_any_call(
+            'User-Agent',
+            'fake_agent',
+        )
+        session_mock.return_value.headers.__setitem__.assert_any_call(
+            'X-Fake-Header',
+            'wb',
+        )
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+        )
+        self.assertEqual(gopher.json(), fake_gopher_single)
+
+        api.set_header('X-Fake-Header', None)
+        session_mock.return_value.headers.__delitem__.assert_any_call(
+            'X-Fake-Header',
+        )
+
+    def test_request_post(self, session_mock):
+        resp = FakeResponse(data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+        data = fake_gopher_tosh
+        gopher = api.request('POST', fake_url, data=data)
+        session_mock.return_value.request.assert_called_with(
+            'POST',
+            fake_url,
+            data=json.dumps(data),
+        )
+        self.assertEqual(gopher.json(), fake_gopher_single)
+
+    def test_create(self, session_mock):
+        resp = FakeResponse(data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+        data = fake_gopher_mac
+
+        # Test no key
+        gopher = api.create(fake_url, data=data)
+        session_mock.return_value.request.assert_called_with(
+            'POST',
+            fake_url,
+            data=json.dumps(data),
+        )
+        self.assertEqual(gopher, fake_gopher_single)
+
+        # Test with key
+        gopher = api.create(fake_url, data=data, response_key=fake_key)
+        session_mock.return_value.request.assert_called_with(
+            'POST',
+            fake_url,
+            data=json.dumps(data),
+        )
+        self.assertEqual(gopher, fake_gopher_mac)
+
+    def test_delete(self, session_mock):
+        resp = FakeResponse(data=None)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+        gopher = api.delete(fake_url)
+        session_mock.return_value.request.assert_called_with(
+            'DELETE',
+            fake_url,
+        )
+        self.assertEqual(gopher, None)
+
+    def test_list(self, session_mock):
+        resp = FakeResponse(data=fake_gopher_list)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        # test base
+        api = restapi.RESTApi()
+        gopher = api.list(fake_url, response_key=fake_keys)
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+            allow_redirects=True,
+        )
+        self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh])
+
+        # test body
+        api = restapi.RESTApi()
+        data = {'qwerty': 1}
+        gopher = api.list(fake_url, response_key=fake_keys, data=data)
+        session_mock.return_value.request.assert_called_with(
+            'POST',
+            fake_url,
+            data=json.dumps(data),
+        )
+        self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh])
+
+        # test query params
+        api = restapi.RESTApi()
+        params = {'qaz': '123'}
+        gophers = api.list(fake_url, response_key=fake_keys, params=params)
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+            allow_redirects=True,
+            params=params,
+        )
+        self.assertEqual(gophers, [fake_gopher_mac, fake_gopher_tosh])
+
+    def test_set(self, session_mock):
+        new_gopher = fake_gopher_single
+        new_gopher[fake_key]['name'] = 'Chip'
+        resp = FakeResponse(data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+        data = fake_gopher_mac
+        data['name'] = 'Chip'
+
+        # Test no data, no key
+        gopher = api.set(fake_url)
+        session_mock.return_value.request.assert_called_with(
+            'PUT',
+            fake_url,
+            data=None,
+        )
+        self.assertEqual(gopher, None)
+
+        # Test data, no key
+        gopher = api.set(fake_url, data=data)
+        session_mock.return_value.request.assert_called_with(
+            'PUT',
+            fake_url,
+            data=json.dumps(data),
+        )
+        self.assertEqual(gopher, fake_gopher_single)
+
+        # NOTE:(dtroyer): Key and no data is not tested as without data
+        #                 the response_key is moot
+
+        # Test data and key
+        gopher = api.set(fake_url, data=data, response_key=fake_key)
+        session_mock.return_value.request.assert_called_with(
+            'PUT',
+            fake_url,
+            data=json.dumps(data),
+        )
+        self.assertEqual(gopher, fake_gopher_mac)
+
+    def test_show(self, session_mock):
+        resp = FakeResponse(data=fake_gopher_single)
+        session_mock.return_value = mock.MagicMock(
+            request=mock.MagicMock(return_value=resp),
+        )
+
+        api = restapi.RESTApi()
+
+        # Test no key
+        gopher = api.show(fake_url)
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+        )
+        self.assertEqual(gopher, fake_gopher_single)
+
+        # Test with key
+        gopher = api.show(fake_url, response_key=fake_key)
+        session_mock.return_value.request.assert_called_with(
+            'GET',
+            fake_url,
+        )
+        self.assertEqual(gopher, fake_gopher_mac)
diff --git a/tox.ini b/tox.ini
index 61bdc9dec4..e2c61af489 100644
--- a/tox.ini
+++ b/tox.ini
@@ -23,6 +23,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}'
 downloadcache = ~/cache/pip
 
 [flake8]
-ignore = E126,E202,W602,H402
+ignore = E126,E202,W602,H302,H402
 show-source = True
 exclude =  .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools