Tests for nova unified quotas

This adds a sequence of unified limit updates and verifications of
enforcement for servers, vcpu, ram, and disk. This is safe to run in
parallel with other tests because only project scoped limits are being
tested in this scenario.

Related to blueprint unified-limits-nova

Change-Id: I37d3896a037e2d4d1004abc52f6e93fd0025f981
diff --git a/tempest/scenario/test_compute_unified_limits.py b/tempest/scenario/test_compute_unified_limits.py
new file mode 100644
index 0000000..bacf526
--- /dev/null
+++ b/tempest/scenario/test_compute_unified_limits.py
@@ -0,0 +1,166 @@
+# Copyright 2021 Red Hat, 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 testtools
+
+from tempest.common import utils
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+from tempest.scenario import manager
+
+CONF = config.CONF
+
+
+@testtools.skipUnless(CONF.compute_feature_enabled.unified_limits,
+                      'Compute unified limits are not enabled')
+class ComputeProjectQuotaTest(manager.ScenarioTest):
+    """The test base class for compute unified limits tests.
+
+    Dynamic credentials (unique tenants) are created on a per-class basis, so
+    we test different quota limits in separate test classes to prevent a quota
+    limit update in one test class from affecting a test running in another
+    test class in parallel.
+
+    https://docs.openstack.org/tempest/latest/configuration.html#dynamic-credentials
+    """
+    credentials = ['primary', 'system_admin']
+    force_tenant_isolation = True
+
+    @classmethod
+    def resource_setup(cls):
+        super(ComputeProjectQuotaTest, cls).resource_setup()
+
+        # Figure out and record the nova service id
+        services = cls.os_system_admin.identity_services_v3_client.\
+            list_services()
+        nova_services = [x for x in services['services']
+                         if x['name'] == 'nova']
+        cls.nova_service_id = nova_services[0]['id']
+
+        # Pre-create quota limits in subclasses and record their IDs so we can
+        # update them in-place without needing to know which ones have been
+        # created and in which order.
+        cls.limit_ids = {}
+
+    @classmethod
+    def _create_limit(cls, name, value):
+        return cls.os_system_admin.identity_limits_client.create_limit(
+            CONF.identity.region, cls.nova_service_id,
+            cls.servers_client.tenant_id, name, value)['limits'][0]['id']
+
+    def _update_limit(self, name, value):
+        self.os_system_admin.identity_limits_client.update_limit(
+            self.limit_ids[name], value)
+
+
+@testtools.skipUnless(CONF.compute_feature_enabled.unified_limits,
+                      'Compute unified limits are not enabled')
+class ServersQuotaTest(ComputeProjectQuotaTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(ServersQuotaTest, cls).resource_setup()
+
+        try:
+            cls.limit_ids['servers'] = cls._create_limit(
+                'servers', 5)
+            cls.limit_ids['class:VCPU'] = cls._create_limit(
+                'class:VCPU', 10)
+            cls.limit_ids['class:MEMORY_MB'] = cls._create_limit(
+                'class:MEMORY_MB', 25 * 1024)
+            cls.limit_ids['class:DISK_GB'] = cls._create_limit(
+                'class:DISK_GB', 10)
+        except lib_exc.Forbidden:
+            raise cls.skipException('Target system is not configured with '
+                                    'compute unified limits')
+
+    @decorators.idempotent_id('555d8bbf-d2ed-4e39-858c-4235899402d9')
+    @utils.services('compute')
+    def test_server_count_vcpu_memory_disk_quota(self):
+        # Set a quota on the number of servers for our tenant to one.
+        self._update_limit('servers', 1)
+
+        # Create one server.
+        first = self.create_server(name='first')
+
+        # Second server would put us over quota, so expect failure.
+        # NOTE: In nova, quota exceeded raises 403 Forbidden.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='second')
+
+        # Update our limit to two.
+        self._update_limit('servers', 2)
+
+        # Now the same create should succeed.
+        second = self.create_server(name='second')
+
+        # Third server would put us over quota, so expect failure.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='third')
+
+        # Delete the first server to put us under quota.
+        self.servers_client.delete_server(first['id'])
+        waiters.wait_for_server_termination(self.servers_client, first['id'])
+
+        # Now the same create should succeed.
+        third = self.create_server(name='third')
+
+        # Set the servers limit back to 10 to test other resources.
+        self._update_limit('servers', 10)
+
+        # Default flavor has: VCPU=1, MEMORY_MB=512, DISK_GB=1
+        # We are currently using 2 VCPU, set the limit to 2.
+        self._update_limit('class:VCPU', 2)
+
+        # Server create should fail as it would go over quota.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='fourth')
+
+        # Delete the second server to put us under quota.
+        self.servers_client.delete_server(second['id'])
+        waiters.wait_for_server_termination(self.servers_client, second['id'])
+
+        # Same create should now succeed.
+        fourth = self.create_server(name='fourth')
+
+        # We are currently using 2 DISK_GB. Set limit to 1.
+        self._update_limit('class:DISK_GB', 1)
+
+        # Server create should fail because we're already over (new) quota.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='fifth')
+
+        # Delete the third server.
+        self.servers_client.delete_server(third['id'])
+        waiters.wait_for_server_termination(self.servers_client, third['id'])
+
+        # Server create should fail again because it would still put us over
+        # quota.
+        self.assertRaises(lib_exc.Forbidden,
+                          self.create_server,
+                          name='fifth')
+
+        # Delete the fourth server.
+        self.servers_client.delete_server(fourth['id'])
+        waiters.wait_for_server_termination(self.servers_client, fourth['id'])
+
+        # Server create should succeed now.
+        self.create_server(name='fifth')