Merge "Fix compute api ref links"
diff --git a/.zuul.yaml b/.zuul.yaml
index b51babb..2c066ae 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -32,6 +32,22 @@
         ENABLE_FILE_INJECTION: True
 
 - job:
+    name: tempest-full-py3
+    parent: tempest-full
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: True
+        FORCE_CONFIG_DRIVE: True
+        ENABLE_FILE_INJECTION: False
+      devstack_services:
+        s-account: false
+        s-container: false
+        s-object: false
+        s-proxy: false
+        # without Swift, c-bak cannot run (in the Gate at least)
+        c-bak: false
+
+- job:
     name: tempest-tox-plugin-sanity-check
     parent: tox
     description: |
@@ -116,4 +132,15 @@
               - ^playbooks/
               - ^roles/
               - ^.zuul.yaml$
+        - tempest-full-py3:
+            voting: false
+            irrelevant-files:
+              - ^(test-|)requirements.txt$
+              - ^.*\.rst$
+              - ^doc/.*$
+              - ^etc/.*$
+              - ^releasenotes/.*$
+              - ^setup.cfg$
+              - ^tempest/hacking/.*$
+              - ^tempest/tests/.*$
         - tempest-tox-plugin-sanity-check
diff --git a/HACKING.rst b/HACKING.rst
index a3e9c26..f961884 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -385,7 +385,7 @@
 
 Otherwise the bug fix won't be able to land in the project.
 
-Handily, `Zuul’s cross-repository dependencies
+Handily, `Zuul's cross-repository dependencies
 <https://docs.openstack.org/infra/zuul/user/gating.html#cross-project-dependencies>`_.
 can be leveraged to do without step 2 and to have steps 3 and 4 happen
 "atomically". To do that, make the patch written in step 1 to depend (refer to
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index e5f70d2..d0d7320 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -400,7 +400,7 @@
     Examples:
 
     * Good - ``http://example.com:1234/v2.0``
-    * Wouldn’t work -  ``http://example.com:1234/xyz/v2.0/``
+    * Wouldn't work -  ``http://example.com:1234/xyz/v2.0/``
       (adding prefix/suffix around version etc)
 
 Service Feature Configuration
diff --git a/releasenotes/notes/add-group-type-specs-apis-to-v3-group-types-client-10390b52dedede54.yaml b/releasenotes/notes/add-group-type-specs-apis-to-v3-group-types-client-10390b52dedede54.yaml
new file mode 100644
index 0000000..404319d
--- /dev/null
+++ b/releasenotes/notes/add-group-type-specs-apis-to-v3-group-types-client-10390b52dedede54.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    Add group type specs APIs to v3 group_types_client library.
+
+    * create_or_update_group_type_specs
+    * list_group_type_specs
+    * show_group_type_specs_item
+    * update_group_type_specs_item
+    * delete_group_type_specs_item
diff --git a/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml b/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml
index 775a383..a002fb8 100644
--- a/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml
+++ b/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml
@@ -1,6 +1,6 @@
 ---
 fixes:
   - |
-    Fix list_group_snapshots API in v3 group_snapshots_client: Bug#1715786.
+    Fix list_group_snapshots API in v3 group_snapshots_client: Bug#1715786.
     The url path for list group snapshots with details API is changed from
     ``?detail=True`` to ``/detail``.
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index dcd7b9b..9e897e3 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -230,8 +230,8 @@
             while data not in console_output and t <= 120.0:
                 try:
                     ws.send_frame(data)
-                    recieved = ws.receive_frame()
-                    console_output += recieved
+                    received = ws.receive_frame()
+                    console_output += received
                 except Exception:
                     # In case we had an issue with send/receive on the
                     # websocket connection, we create a new one.
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 83fabdb..9759be7 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -185,11 +185,12 @@
             cls.request_microversion)
         v2_37_version = api_version_request.APIVersionRequest('2.37')
 
+        tenant_network = cls.get_tenant_network()
         # NOTE(snikitin): since microversion v2.37 'networks' field is required
-        if request_version >= v2_37_version and 'networks' not in kwargs:
+        if (request_version >= v2_37_version and 'networks' not in kwargs and
+            not tenant_network):
             kwargs['networks'] = 'none'
 
-        tenant_network = cls.get_tenant_network()
         body, servers = compute.create_test_server(
             cls.os_primary,
             validatable,
diff --git a/tempest/api/volume/admin/test_group_type_specs.py b/tempest/api/volume/admin/test_group_type_specs.py
new file mode 100644
index 0000000..c5e6d1a
--- /dev/null
+++ b/tempest/api/volume/admin/test_group_type_specs.py
@@ -0,0 +1,80 @@
+# Copyright 2017 FiberHome Telecommunication Technologies CO.,LTD
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.volume import base
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class GroupTypeSpecsTest(base.BaseVolumeAdminTest):
+    _api_version = 3
+    min_microversion = '3.11'
+    max_microversion = 'latest'
+
+    @decorators.idempotent_id('bb4e30d0-de6e-4f4d-866c-dcc48d023b4e')
+    def test_group_type_specs_create_show_update_list_delete(self):
+        # Create new group type
+        group_type = self.create_group_type()
+
+        # Create new group type specs
+        create_specs = {
+            "key1": "value1",
+            "key2": "value2"
+        }
+        body = self.admin_group_types_client.create_or_update_group_type_specs(
+            group_type['id'], create_specs)['group_specs']
+        self.assertEqual(create_specs, body)
+
+        # Create a new group type spec and update an existing group type spec
+        update_specs = {
+            "key2": "value2-updated",
+            "key3": "value3"
+        }
+        body = self.admin_group_types_client.create_or_update_group_type_specs(
+            group_type['id'], update_specs)['group_specs']
+        self.assertEqual(update_specs, body)
+
+        # Show specified item of group type specs
+        spec_keys = ['key2', 'key3']
+        for key in spec_keys:
+            body = self.admin_group_types_client.show_group_type_specs_item(
+                group_type['id'], key)
+            self.assertIn(key, body)
+            self.assertEqual(update_specs[key], body[key])
+
+        # Update specified item of group type specs
+        update_key = 'key3'
+        update_spec = {update_key: "value3-updated"}
+        body = self.admin_group_types_client.update_group_type_specs_item(
+            group_type['id'], update_key, update_spec)
+        self.assertEqual(update_spec, body)
+
+        # List all group type specs that created or updated above
+        list_specs = {}
+        list_specs.update(create_specs)
+        list_specs.update(update_specs)
+        list_specs.update(update_spec)
+        body = self.admin_group_types_client.list_group_type_specs(
+            group_type['id'])['group_specs']
+        self.assertEqual(list_specs, body)
+
+        # Delete specified item of group type specs
+        delete_key = 'key1'
+        self.admin_group_types_client.delete_group_type_specs_item(
+            group_type['id'], delete_key)
+        self.assertRaises(
+            lib_exc.NotFound,
+            self.admin_group_types_client.show_group_type_specs_item,
+            group_type['id'], delete_key)
diff --git a/tempest/lib/common/utils/test_utils.py b/tempest/lib/common/utils/test_utils.py
index c2e93ee..2a9f3a9 100644
--- a/tempest/lib/common/utils/test_utils.py
+++ b/tempest/lib/common/utils/test_utils.py
@@ -102,13 +102,13 @@
     now = time.time()
     begin_time = now
     timeout = now + duration
+    func_name = getattr(func, '__name__', getattr(func.__class__, '__name__'))
     while now < timeout:
         if func(*args, **kwargs):
             LOG.debug("Call %s returns true in %f seconds",
-                      getattr(func, '__name__'), time.time() - begin_time)
+                      func_name, time.time() - begin_time)
             return True
         time.sleep(sleep_for)
         now = time.time()
-    LOG.debug("Call %s returns false in %f seconds",
-              getattr(func, '__name__'), duration)
+    LOG.debug("Call %s returns false in %f seconds", func_name, duration)
     return False
diff --git a/tempest/lib/services/compute/quota_classes_client.py b/tempest/lib/services/compute/quota_classes_client.py
index 0fe9868..64e06f4 100644
--- a/tempest/lib/services/compute/quota_classes_client.py
+++ b/tempest/lib/services/compute/quota_classes_client.py
@@ -35,8 +35,9 @@
     def update_quota_class_set(self, quota_class_id, **kwargs):
         """Update the quota class's limits for one or more resources.
 
-        # NOTE: Current api-site doesn't contain this API description.
-        # LP: https://bugs.launchpad.net/nova/+bug/1602400
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/compute/#create-or-update-quotas-for-quota-class
         """
         post_body = json.dumps({'quota_class_set': kwargs})
 
diff --git a/tempest/lib/services/volume/v2/quota_classes_client.py b/tempest/lib/services/volume/v2/quota_classes_client.py
index d40d2d9..733b1ac 100644
--- a/tempest/lib/services/volume/v2/quota_classes_client.py
+++ b/tempest/lib/services/volume/v2/quota_classes_client.py
@@ -26,8 +26,9 @@
     def show_quota_class_set(self, quota_class_id):
         """List quotas for a quota class.
 
-        TODO: Current api-site doesn't contain this API description.
-        LP: https://bugs.launchpad.net/nova/+bug/1602400
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/index.html#show-quota-classes
         """
         url = 'os-quota-class-sets/%s' % quota_class_id
         resp, body = self.get(url)
@@ -38,8 +39,9 @@
     def update_quota_class_set(self, quota_class_id, **kwargs):
         """Update quotas for a quota class.
 
-        TODO: Current api-site doesn't contain this API description.
-        LP: https://bugs.launchpad.net/nova/+bug/1602400
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/index.html#update-quota-classes
         """
         url = 'os-quota-class-sets/%s' % quota_class_id
         put_body = json.dumps({'quota_class_set': kwargs})
diff --git a/tempest/lib/services/volume/v3/group_types_client.py b/tempest/lib/services/volume/v3/group_types_client.py
index 6181472..1b47201 100644
--- a/tempest/lib/services/volume/v3/group_types_client.py
+++ b/tempest/lib/services/volume/v3/group_types_client.py
@@ -88,3 +88,54 @@
         self.expected_success(200, resp.status)
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
+
+    def create_or_update_group_type_specs(self, group_type_id, group_specs):
+        """Creates new group specs or updates existing group specs.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#create-group-specs-for-a-group-type
+        """
+        url = "group_types/%s/group_specs" % group_type_id
+        post_body = json.dumps({'group_specs': group_specs})
+        resp, body = self.post(url, post_body)
+        body = json.loads(body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_group_type_specs(self, group_type_id):
+        """Lists all group specs for a given group type."""
+        url = 'group_types/%s/group_specs' % group_type_id
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def show_group_type_specs_item(self, group_type_id, spec_id):
+        """Shows specified item of group specs for a given group type."""
+        url = "group_types/%s/group_specs/%s" % (group_type_id, spec_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def update_group_type_specs_item(self, group_type_id, spec_id, spec):
+        """Updates specified item of group specs for a given group type.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#update-one-specific-group-spec-for-a-group-type
+        """
+        url = "group_types/%s/group_specs/%s" % (group_type_id, spec_id)
+        put_body = json.dumps(spec)
+        resp, body = self.put(url, put_body)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_group_type_specs_item(self, group_type_id, spec_id):
+        """Deletes specified item of group specs for a given group type."""
+        resp, body = self.delete("group_types/%s/group_specs/%s" % (
+            group_type_id, spec_id))
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/tests/lib/services/volume/v3/test_group_types_client.py b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
index e86594e..c60cc36 100644
--- a/tempest/tests/lib/services/volume/v3/test_group_types_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
@@ -69,6 +69,28 @@
         ]
     }
 
+    FAKE_CREATE_GROUP_TYPE_SPECS = {
+        "group_specs": {
+            "key1": "value1",
+            "key2": "value2"
+        }
+    }
+
+    FAKE_LIST_GROUP_TYPE_SPECS = {
+        "group_specs": {
+            "key1": "value1",
+            "key2": "value2"
+        }
+    }
+
+    FAKE_SHOW_GROUP_TYPE_SPECS_ITEM = {
+        "key1": "value1"
+    }
+
+    FAKE_UPDATE_GROUP_TYPE_SPECS_ITEM = {
+        "key2": "value2-updated"
+    }
+
     def setUp(self):
         super(TestGroupTypesClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -111,6 +133,45 @@
             group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
             name='updated-group-type-name')
 
+    def _test_create_or_update_group_type_specs(self, bytes_body=False):
+        group_specs = self.FAKE_CREATE_GROUP_TYPE_SPECS['group_specs']
+        self.check_service_client_function(
+            self.client.create_or_update_group_type_specs,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_GROUP_TYPE_SPECS,
+            bytes_body,
+            group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+            group_specs=group_specs,
+            status=202)
+
+    def _test_list_group_type_specs(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_group_type_specs,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_GROUP_TYPE_SPECS,
+            bytes_body,
+            group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+    def _test_show_group_type_specs_item(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_group_type_specs_item,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_SHOW_GROUP_TYPE_SPECS_ITEM,
+            bytes_body,
+            group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+            spec_id="key1")
+
+    def _test_update_group_type_specs_item(self, bytes_body=False):
+        spec = self.FAKE_UPDATE_GROUP_TYPE_SPECS_ITEM
+        self.check_service_client_function(
+            self.client.update_group_type_specs_item,
+            'tempest.lib.common.rest_client.RestClient.put',
+            self.FAKE_UPDATE_GROUP_TYPE_SPECS_ITEM,
+            bytes_body,
+            group_type_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5",
+            spec_id="key2",
+            spec=spec)
+
     def test_create_group_type_with_str_body(self):
         self._test_create_group_type()
 
@@ -142,3 +203,36 @@
 
     def test_update_group_types_with_bytes_body(self):
         self._test_update_group_types(bytes_body=True)
+
+    def test_create_or_update_group_type_specs_with_str_body(self):
+        self._test_create_or_update_group_type_specs()
+
+    def test_create_or_update_group_type_specs_with_bytes_body(self):
+        self._test_create_or_update_group_type_specs(bytes_body=True)
+
+    def test_list_group_type_specs_with_str_body(self):
+        self._test_list_group_type_specs()
+
+    def test_list_group_type_specs_with_bytes_body(self):
+        self._test_list_group_type_specs(bytes_body=True)
+
+    def test_show_group_type_specs_item_with_str_body(self):
+        self._test_show_group_type_specs_item()
+
+    def test_show_group_type_specs_item_with_bytes_body(self):
+        self._test_show_group_type_specs_item(bytes_body=True)
+
+    def test_update_group_type_specs_item_with_str_body(self):
+        self._test_update_group_type_specs_item()
+
+    def test_update_group_type_specs_item_with_bytes_body(self):
+        self._test_update_group_type_specs_item(bytes_body=True)
+
+    def test_delete_group_type_specs_item(self):
+        self.check_service_client_function(
+            self.client.delete_group_type_specs_item,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            group_type_id='0e58433f-d108-4bf3-a22c-34e6b71ef86b',
+            spec_id='key1',
+            status=202)
diff --git a/tox.ini b/tox.ini
index c0df8a6..892b6f4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -123,6 +123,10 @@
     tempest run --serial --regex '\[.*\bsmoke\b.*\]' {posargs}
 
 [testenv:venv]
+deps =
+  -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
+  -r{toxinidir}/requirements.txt
+  -r{toxinidir}/doc/requirements.txt
 commands = {posargs}
 
 [testenv:venv-tempest]