Merge "Set smoke/gate attributes for tests in "compute""
diff --git a/tempest/README.rst b/tempest/README.rst
index 63f3ad0..d506bc6 100644
--- a/tempest/README.rst
+++ b/tempest/README.rst
@@ -38,8 +38,6 @@
 projects themselves, possibly as functional tests in their unit test
 frameworks.
 
-TODO: The bulk of tempest/tests should move to tempest/api
-
 
 cli
 ------------
@@ -50,8 +48,6 @@
 instantiate. Tempest seems like a logical place for this, as it
 prereqs having a running OpenStack cloud.
 
-TODO: the top level cli directory moves to tempest/cli
-
 
 scenario
 ------------
@@ -62,9 +58,6 @@
 
 Scenario tests can and should use the OpenStack python clients.
 
-TODO: tests/network/test_network_basic_ops.py,
-tests/compute/servers/*_ops.py should move to tempest/scenario (others)
-
 
 stress
 -----------
@@ -85,8 +78,6 @@
 but those should be kept seperate from the normal OpenStack
 validation.
 
-TODO: tempest/tests/boto should become tempest/3rdparty/boto
-
 
 whitebox
 ----------
@@ -94,5 +85,3 @@
 Whitebox tests are tests which require access to the database of the
 target OpenStack machine to verify internal state after opperations
 are made. White box tests are allowed to use the python clients.
-
-TODO: collect out whitebox tests to this location.
diff --git a/tempest/api/compute/admin/test_simple_tenant_usage.py b/tempest/api/compute/admin/test_simple_tenant_usage.py
new file mode 100644
index 0000000..9c4c4b9
--- /dev/null
+++ b/tempest/api/compute/admin/test_simple_tenant_usage.py
@@ -0,0 +1,115 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC 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.
+
+import datetime
+
+from tempest.api.compute import base
+from tempest import exceptions
+from tempest.test import attr
+import time
+
+
+class TenantUsagesTestJSON(base.BaseComputeAdminTest):
+
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(TenantUsagesTestJSON, cls).setUpClass()
+        cls.adm_client = cls.os_adm.tenant_usages_client
+        cls.client = cls.os.tenant_usages_client
+        cls.identity_client = cls._get_identity_admin_client()
+
+        resp, tenants = cls.identity_client.list_tenants()
+        cls.tenant_id = [tnt['id'] for tnt in tenants if tnt['name'] ==
+                         cls.client.tenant_name][0]
+
+        # Create a server in the demo tenant
+        resp, server = cls.create_server(wait_until='ACTIVE')
+        time.sleep(2)
+
+        now = datetime.datetime.now()
+        cls.start = cls._parse_strtime(now - datetime.timedelta(days=1))
+        cls.end = cls._parse_strtime(now + datetime.timedelta(days=1))
+
+    @classmethod
+    def _parse_strtime(cls, at):
+        # Returns formatted datetime
+        return at.strftime('%Y-%m-%dT%H:%M:%S.%f')
+
+    @attr('positive')
+    def test_list_usage_all_tenants(self):
+        # Get usage for all tenants
+        params = {'start': self.start,
+                  'end': self.end,
+                  'detailed': int(bool(True))}
+        resp, tenant_usage = self.adm_client.list_tenant_usages(params)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(len(tenant_usage), 8)
+
+    @attr('positive')
+    def test_get_usage_tenant(self):
+        # Get usage for a specific tenant
+        params = {'start': self.start,
+                  'end': self.end}
+        resp, tenant_usage = self.adm_client.get_tenant_usage(
+            self.tenant_id, params)
+
+        self.assertEqual(200, resp.status)
+        self.assertEqual(len(tenant_usage), 8)
+
+    @attr('positive')
+    def test_get_usage_tenant_with_non_admin_user(self):
+        # Get usage for a specific tenant with non admin user
+        params = {'start': self.start,
+                  'end': self.end}
+        resp, tenant_usage = self.client.get_tenant_usage(
+            self.tenant_id, params)
+
+        self.assertEqual(200, resp.status)
+        self.assertEqual(len(tenant_usage), 8)
+
+    @attr('negative')
+    def test_get_usage_tenant_with_empty_tenant_id(self):
+        # Get usage for a specific tenant empty
+        params = {'start': self.start,
+                  'end': self.end}
+        self.assertRaises(exceptions.NotFound,
+                          self.adm_client.get_tenant_usage,
+                          '', params)
+
+    @attr('negative')
+    def test_get_usage_tenant_with_invalid_date(self):
+        # Get usage for tenant with invalid date
+        params = {'start': self.end,
+                  'end': self.start}
+        self.assertRaises(exceptions.BadRequest,
+                          self.adm_client.get_tenant_usage,
+                          self.tenant_id, params)
+
+    @attr('negative')
+    def test_list_usage_all_tenants_with_non_admin_user(self):
+        # Get usage for all tenants with non admin user
+        params = {'start': self.start,
+                  'end': self.end,
+                  'detailed': int(bool(True))}
+        self.assertRaises(exceptions.Unauthorized,
+                          self.client.list_tenant_usages, params)
+
+
+class TenantUsagesTestXML(TenantUsagesTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/limits/test_absolute_limits.py b/tempest/api/compute/limits/test_absolute_limits.py
index 4cb9272..beae122 100644
--- a/tempest/api/compute/limits/test_absolute_limits.py
+++ b/tempest/api/compute/limits/test_absolute_limits.py
@@ -64,8 +64,9 @@
 
         self.assertRaises(exceptions.OverLimit,
                           self.server_client.create_server,
-                          name='test', meta=meta_data, flavor_ref='84',
-                          image_ref='9e6a2e3b-1601-42a5-985f-c3a2f93a5ec3')
+                          name='test', meta=meta_data,
+                          flavor_ref=self.flavor_ref,
+                          image_ref=self.image_ref)
 
 
 class AbsoluteLimitsTestXML(AbsoluteLimitsTestJSON):
diff --git a/tools/__init__.py b/tempest/api/orchestration/__init__.py
similarity index 100%
copy from tools/__init__.py
copy to tempest/api/orchestration/__init__.py
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
new file mode 100644
index 0000000..544558e
--- /dev/null
+++ b/tempest/api/orchestration/base.py
@@ -0,0 +1,110 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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
+import time
+
+from tempest import clients
+from tempest.common.utils.data_utils import rand_name
+import tempest.test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseOrchestrationTest(tempest.test.BaseTestCase):
+    """Base test case class for all Orchestration API tests."""
+
+    @classmethod
+    def setUpClass(cls):
+
+        os = clients.OrchestrationManager()
+        cls.orchestration_cfg = os.config.orchestration
+        if not cls.orchestration_cfg.heat_available:
+            raise cls.skipException("Heat support is required")
+
+        cls.os = os
+        cls.orchestration_client = os.orchestration_client
+        cls.keypairs_client = os.keypairs_client
+        cls.stacks = []
+
+    @classmethod
+    def _get_identity_admin_client(cls):
+        """
+        Returns an instance of the Identity Admin API client
+        """
+        os = clients.AdminManager(interface=cls._interface)
+        admin_client = os.identity_client
+        return admin_client
+
+    @classmethod
+    def _get_client_args(cls):
+
+        return (
+            cls.config,
+            cls.config.identity.admin_username,
+            cls.config.identity.admin_password,
+            cls.config.identity.uri
+        )
+
+    def create_stack(self, stack_name, template_data, parameters={}):
+        resp, body = self.client.create_stack(
+            stack_name,
+            template=template_data,
+            parameters=parameters)
+        self.assertEqual('201', resp['status'])
+        stack_id = resp['location'].split('/')[-1]
+        stack_identifier = '%s/%s' % (stack_name, stack_id)
+        self.stacks.append(stack_identifier)
+        return stack_identifier
+
+    @classmethod
+    def clear_stacks(cls):
+        for stack_identifier in cls.stacks:
+            try:
+                cls.orchestration_client.delete_stack(stack_identifier)
+            except Exception:
+                pass
+
+        for stack_identifier in cls.stacks:
+            try:
+                cls.orchestration_client.wait_for_stack_status(
+                    stack_identifier, 'DELETE_COMPLETE')
+            except Exception:
+                pass
+
+    def _create_keypair(self, namestart='keypair-heat-'):
+        kp_name = rand_name(namestart)
+        resp, body = self.keypairs_client.create_keypair(kp_name)
+        self.assertEqual(body['name'], kp_name)
+        return body
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.clear_stacks()
+
+    def wait_for(self, condition):
+        """Repeatedly calls condition() until a timeout."""
+        start_time = int(time.time())
+        while True:
+            try:
+                condition()
+            except Exception:
+                pass
+            else:
+                return
+            if int(time.time()) - start_time >= self.build_timeout:
+                condition()
+                return
+            time.sleep(self.build_interval)
diff --git a/tools/__init__.py b/tempest/api/orchestration/stacks/__init__.py
similarity index 100%
rename from tools/__init__.py
rename to tempest/api/orchestration/stacks/__init__.py
diff --git a/tempest/api/orchestration/stacks/test_stacks.py b/tempest/api/orchestration/stacks/test_stacks.py
new file mode 100644
index 0000000..8847c08
--- /dev/null
+++ b/tempest/api/orchestration/stacks/test_stacks.py
@@ -0,0 +1,77 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 tempest.api.orchestration import base
+from tempest.common.utils.data_utils import rand_name
+from tempest.test import attr
+
+
+LOG = logging.getLogger(__name__)
+
+
+class StacksTestJSON(base.BaseOrchestrationTest):
+    _interface = 'json'
+
+    empty_template = "HeatTemplateFormatVersion: '2012-12-12'\n"
+
+    @classmethod
+    def setUpClass(cls):
+        super(StacksTestJSON, cls).setUpClass()
+        cls.client = cls.orchestration_client
+
+    @attr(type='smoke')
+    def test_stack_list_responds(self):
+        resp, body = self.client.list_stacks()
+        stacks = body['stacks']
+        self.assertEqual('200', resp['status'])
+        self.assertIsInstance(stacks, list)
+
+    @attr(type='smoke')
+    def test_stack_crud_no_resources(self):
+        stack_name = rand_name('heat')
+
+        # count how many stacks to start with
+        resp, body = self.client.list_stacks()
+        stack_count = len(body['stacks'])
+
+        # create the stack
+        stack_identifier = self.create_stack(
+            stack_name, self.empty_template)
+
+        # wait for create complete (with no resources it should be instant)
+        self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+
+        # stack count will increment by 1
+        resp, body = self.client.list_stacks()
+        self.assertEqual(stack_count + 1, len(body['stacks']),
+                         'Expected stack count to increment by 1')
+
+        # fetch the stack
+        resp, body = self.client.get_stack(stack_identifier)
+        self.assertEqual('CREATE_COMPLETE', body['stack_status'])
+
+        # fetch the stack by name
+        resp, body = self.client.get_stack(stack_name)
+        self.assertEqual('CREATE_COMPLETE', body['stack_status'])
+
+        # fetch the stack by id
+        stack_id = stack_identifier.split('/')[1]
+        resp, body = self.client.get_stack(stack_id)
+        self.assertEqual('CREATE_COMPLETE', body['stack_status'])
+
+        # delete the stack
+        resp = self.client.delete_stack(stack_identifier)
+        self.assertEqual('204', resp[0]['status'])
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index c05a6d1..c013ae4 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -13,8 +13,10 @@
 #    under the License.
 
 import logging
+import testtools
 
 from tempest.api.volume import base
+from tempest.common.utils.data_utils import rand_name
 from tempest.test import attr
 
 LOG = logging.getLogger(__name__)
@@ -37,27 +39,39 @@
     def tearDownClass(cls):
         super(VolumesSnapshotTest, cls).tearDownClass()
 
-    @attr(type=['smoke'])
-    def test_snapshot_create_get_delete(self):
-        # Create a snapshot, get some of the details and then deletes it
-        resp, snapshot = self.snapshots_client.create_snapshot(
-            self.volume_origin['id'])
-        self.assertEqual(200, resp.status)
-        self.snapshots_client.wait_for_snapshot_status(snapshot['id'],
-                                                       'available')
-        errmsg = "Referred volume origin ID mismatch"
-        self.assertEqual(self.volume_origin['id'],
-                         snapshot['volume_id'],
-                         errmsg)
-        self.snapshots_client.delete_snapshot(snapshot['id'])
-        self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
+    @attr(type='gate')
+    def test_snapshot_create_get_list_delete(self):
+        # Create a snapshot
+        s_name = rand_name('snap')
+        snapshot = self.create_snapshot(self.volume_origin['id'],
+                                        display_name=s_name)
 
-    @attr(type=['smoke'])
+        # Get the snap and check for some of its details
+        resp, snap_get = self.snapshots_client.get_snapshot(snapshot['id'])
+        self.assertEqual(200, resp.status)
+        self.assertEqual(self.volume_origin['id'],
+                         snap_get['volume_id'],
+                         "Referred volume origin mismatch")
+
+        # Compare also with the output from the list action
+        tracking_data = (snapshot['id'], snapshot['display_name'])
+        resp, snaps_list = self.snapshots_client.list_snapshots()
+        self.assertEqual(200, resp.status)
+        snaps_data = [(f['id'], f['display_name']) for f in snaps_list]
+        self.assertIn(tracking_data, snaps_data)
+
+        # Delete the snapshot
+        self.snapshots_client.delete_snapshot(snapshot['id'])
+        self.assertEqual(200, resp.status)
+        self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
+        self.snapshots.remove(snapshot)
+
+    @attr(type='gate')
     def test_volume_from_snapshot(self):
         # Create a temporary snap using wrapper method from base, then
         # create a snap based volume, check resp code and deletes it
         snapshot = self.create_snapshot(self.volume_origin['id'])
-        # NOTE: size is required also when passing snapshot_id
+        # NOTE(gfidente): size is required also when passing snapshot_id
         resp, volume = self.volumes_client.create_volume(
             size=1,
             snapshot_id=snapshot['id'])
@@ -68,5 +82,6 @@
         self.clear_snapshots()
 
 
+@testtools.skip("Until Bug #1177610 is resolved.")
 class VolumesSnapshotTestXML(VolumesSnapshotTest):
     _interface = "xml"
diff --git a/tempest/clients.py b/tempest/clients.py
index 037a1c4..f284f16 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -41,6 +41,8 @@
     SecurityGroupsClientJSON
 from tempest.services.compute.json.servers_client import ServersClientJSON
 from tempest.services.compute.json.services_client import ServicesClientJSON
+from tempest.services.compute.json.tenant_usages_client import \
+    TenantUsagesClientJSON
 from tempest.services.compute.json.volumes_extensions_client import \
     VolumesExtensionsClientJSON
 from tempest.services.compute.xml.aggregates_client import AggregatesClientXML
@@ -61,6 +63,8 @@
     import SecurityGroupsClientXML
 from tempest.services.compute.xml.servers_client import ServersClientXML
 from tempest.services.compute.xml.services_client import ServicesClientXML
+from tempest.services.compute.xml.tenant_usages_client import \
+    TenantUsagesClientXML
 from tempest.services.compute.xml.volumes_extensions_client import \
     VolumesExtensionsClientXML
 from tempest.services.identity.json.identity_client import IdentityClientJSON
@@ -216,6 +220,11 @@
     "xml": ServicesClientXML,
 }
 
+TENANT_USAGES_CLIENT = {
+    "json": TenantUsagesClientJSON,
+    "xml": TenantUsagesClientXML,
+}
+
 
 class Manager(object):
 
@@ -287,6 +296,8 @@
             self.service_client = SERVICE_CLIENT[interface](*client_args)
             self.aggregates_client = AGGREGATES_CLIENT[interface](*client_args)
             self.services_client = SERVICES_CLIENT[interface](*client_args)
+            self.tenant_usages_client = \
+                TENANT_USAGES_CLIENT[interface](*client_args)
         except KeyError:
             msg = "Unsupported interface type `%s'" % interface
             raise exceptions.InvalidConfiguration(msg)
diff --git a/tools/hacking/__init__.py b/tempest/hacking/__init__.py
similarity index 100%
rename from tools/hacking/__init__.py
rename to tempest/hacking/__init__.py
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
new file mode 100644
index 0000000..353a9ac
--- /dev/null
+++ b/tempest/hacking/checks.py
@@ -0,0 +1,58 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corp.
+#
+#   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 re
+
+
+PYTHON_CLIENTS = ['cinder', 'glance', 'keystone', 'nova', 'swift', 'quantum']
+
+SKIP_DECORATOR_RE = re.compile(r'\s*@testtools.skip\((.*)\)')
+SKIP_STR_RE = re.compile(r'.*Bug #\d+.*')
+PYTHON_CLIENT_RE = re.compile('import (%s)client' % '|'.join(PYTHON_CLIENTS))
+
+
+def skip_bugs(physical_line):
+    """Check skip lines for proper bug entries
+
+    T101: skips must contain "Bug #<bug_number>"
+    """
+
+    res = SKIP_DECORATOR_RE.match(physical_line)
+    if res:
+        content = res.group(1)
+        res = SKIP_STR_RE.match(content)
+        if not res:
+            return (physical_line.find(content),
+                    'T101: skips must contain "Bug #<bug_number>"')
+
+
+def import_no_clients_in_api(physical_line, filename):
+    """Check for client imports from tempest/api tests
+
+    T102: Cannot import OpenStack python clients
+    """
+
+    if "tempest/api" in filename:
+        res = PYTHON_CLIENT_RE.match(physical_line)
+        if res:
+            return (physical_line.find(res.group(1)),
+                    ("T102: python clients import not allowed"
+                     " in tempest/api/* tests"))
+
+
+def factory(register):
+    register(skip_bugs)
+    register(import_no_clients_in_api)
diff --git a/tempest/services/compute/json/tenant_usages_client.py b/tempest/services/compute/json/tenant_usages_client.py
new file mode 100644
index 0000000..4dd6964
--- /dev/null
+++ b/tempest/services/compute/json/tenant_usages_client.py
@@ -0,0 +1,47 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC 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.
+
+import json
+import urllib
+
+from tempest.common.rest_client import RestClient
+
+
+class TenantUsagesClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(TenantUsagesClientJSON, self).__init__(
+            config, username, password, auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def list_tenant_usages(self, params=None):
+        url = 'os-simple-tenant-usage'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['tenant_usages'][0]
+
+    def get_tenant_usage(self, tenant_id, params=None):
+        url = 'os-simple-tenant-usage/%s' % tenant_id
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['tenant_usage']
diff --git a/tempest/services/compute/xml/tenant_usages_client.py b/tempest/services/compute/xml/tenant_usages_client.py
new file mode 100644
index 0000000..cb92324
--- /dev/null
+++ b/tempest/services/compute/xml/tenant_usages_client.py
@@ -0,0 +1,54 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC 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.
+
+import urllib
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import xml_to_json
+
+
+class TenantUsagesClientXML(RestClientXML):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(TenantUsagesClientXML, self).__init__(config, username,
+                                                    password, auth_url,
+                                                    tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def _parse_array(self, node):
+        json = xml_to_json(node)
+        return json
+
+    def list_tenant_usages(self, params=None):
+        url = 'os-simple-tenant-usage'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url, self.headers)
+        tenant_usage = self._parse_array(etree.fromstring(body))
+        return resp, tenant_usage['tenant_usage']
+
+    def get_tenant_usage(self, tenant_id, params=None):
+        url = 'os-simple-tenant-usage/%s' % tenant_id
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url, self.headers)
+        tenant_usage = self._parse_array(etree.fromstring(body))
+        return resp, tenant_usage
diff --git a/tools/hacking/tempest.py b/tools/hacking/tempest.py
deleted file mode 100644
index 1db8419..0000000
--- a/tools/hacking/tempest.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2013 IBM Corp.
-#
-#   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 re
-
-
-SKIP_DECORATOR = '@testtools.skip('
-
-
-def skip_bugs(physical_line):
-    """Check skip lines for proper bug entries
-
-    T101: Bug not in skip line
-    T102: Bug in message formatted incorrectly
-    """
-
-    pos = physical_line.find(SKIP_DECORATOR)
-
-    skip_re = re.compile(r'^\s*@testtools.skip.*')
-
-    if pos != -1 and skip_re.match(physical_line):
-        bug = re.compile(r'^.*\bbug\b.*', re.IGNORECASE)
-        if bug.match(physical_line) is None:
-            return (pos, 'T101: skips must have an associated bug')
-
-        bug_re = re.compile(r'.*skip\(.*Bug\s\#\d+', re.IGNORECASE)
-
-        if bug_re.match(physical_line) is None:
-            return (pos, 'T102: Bug number formatted incorrectly')
diff --git a/tox.ini b/tox.ini
index d6c2c41..924e844 100644
--- a/tox.ini
+++ b/tox.ini
@@ -60,7 +60,7 @@
        -r{toxinidir}/tools/test-requires
 
 [hacking]
-local-check = tools.hacking.tempest.skip_bugs
+local-check-factory = tempest.hacking.checks.factory
 
 [flake8]
 ignore = E125,H302,H404