Merge "[V3] Make endpoints_client use **kwargs"
diff --git a/HACKING.rst b/HACKING.rst
index 3799046..9f7487d 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -17,6 +17,7 @@
 - [T108] Check no hyphen at the end of rand_name() argument
 - [T109] Cannot use testtools.skip decorator; instead use
          decorators.skip_because from tempest-lib
+- [T110] Check that service client names of GET should be consistent
 - [N322] Method's default argument shouldn't be mutable
 
 Test Data/Configuration
diff --git a/tempest/api/compute/admin/test_availability_zone.py b/tempest/api/compute/admin/test_availability_zone.py
index 1b36ff2..5b02761 100644
--- a/tempest/api/compute/admin/test_availability_zone.py
+++ b/tempest/api/compute/admin/test_availability_zone.py
@@ -17,11 +17,10 @@
 from tempest import test
 
 
-class AZAdminV2TestJSON(base.BaseComputeAdminTest):
+class AZAdminV2TestJSON(base.BaseV2ComputeAdminTest):
     """
     Tests Availability Zone API List
     """
-    _api_version = 2
 
     @classmethod
     def setup_clients(cls):
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index f186a7d..ecb27c2 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -28,14 +28,27 @@
     _host_key = 'OS-EXT-SRV-ATTR:host'
 
     @classmethod
+    def skip_checks(cls):
+        super(LiveBlockMigrationTestJSON, cls).skip_checks()
+
+        if not CONF.compute_feature_enabled.live_migration:
+            skip_msg = ("%s skipped as live-migration is "
+                        "not available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+        if len(cls._get_compute_hostnames()) < 2:
+            raise cls.skipTest(
+                "Less than 2 compute nodes, skipping migration test.")
+
+    @classmethod
     def setup_clients(cls):
         super(LiveBlockMigrationTestJSON, cls).setup_clients()
         cls.admin_hosts_client = cls.os_adm.hosts_client
         cls.admin_servers_client = cls.os_adm.servers_client
         cls.admin_migration_client = cls.os_adm.migrations_client
 
-    def _get_compute_hostnames(self):
-        body = self.admin_hosts_client.list_hosts()['hosts']
+    @classmethod
+    def _get_compute_hostnames(cls):
+        body = cls.admin_hosts_client.list_hosts()['hosts']
         return [
             host_record['host_name']
             for host_record in body
@@ -90,9 +103,6 @@
                               volume_backed, *block* migration is not used.
         """
         # Live migrate an instance to another host
-        if len(self._get_compute_hostnames()) < 2:
-            raise self.skipTest(
-                "Less than 2 compute nodes, skipping migration test.")
         server_id = self._create_server(volume_backed=volume_backed)
         actual_host = self._get_host_for_server(server_id)
         target_host = self._get_host_other_than(actual_host)
@@ -117,14 +127,10 @@
                          msg)
 
     @test.idempotent_id('1dce86b8-eb04-4c03-a9d8-9c1dc3ee0c7b')
-    @testtools.skipUnless(CONF.compute_feature_enabled.live_migration,
-                          'Live migration not available')
     def test_live_block_migration(self):
         self._test_live_migration()
 
     @test.idempotent_id('1e107f21-61b2-4988-8f22-b196e938ab88')
-    @testtools.skipUnless(CONF.compute_feature_enabled.live_migration,
-                          'Live migration not available')
     @testtools.skipUnless(CONF.compute_feature_enabled.pause,
                           'Pause is not available.')
     @testtools.skipUnless(CONF.compute_feature_enabled
@@ -135,24 +141,18 @@
         self._test_live_migration(state='PAUSED')
 
     @test.idempotent_id('5071cf17-3004-4257-ae61-73a84e28badd')
-    @testtools.skipUnless(CONF.compute_feature_enabled.live_migration,
-                          'Live migration not available')
     @test.services('volume')
     def test_volume_backed_live_migration(self):
         self._test_live_migration(volume_backed=True)
 
     @test.idempotent_id('e19c0cc6-6720-4ed8-be83-b6603ed5c812')
-    @testtools.skipIf(not CONF.compute_feature_enabled.live_migration or not
-                      CONF.compute_feature_enabled.
+    @testtools.skipIf(not CONF.compute_feature_enabled.
                       block_migration_for_live_migration,
                       'Block Live migration not available')
     @testtools.skipIf(not CONF.compute_feature_enabled.
                       block_migrate_cinder_iscsi,
                       'Block Live migration not configured for iSCSI')
     def test_iscsi_volume(self):
-        if len(self._get_compute_hostnames()) < 2:
-            raise self.skipTest(
-                "Less than 2 compute nodes, skipping migration test.")
         server_id = self._create_server()
         actual_host = self._get_host_for_server(server_id)
         target_host = self._get_host_other_than(actual_host)
diff --git a/tempest/api/compute/admin/test_networks.py b/tempest/api/compute/admin/test_networks.py
index 1da3f6e..c4aa81b 100644
--- a/tempest/api/compute/admin/test_networks.py
+++ b/tempest/api/compute/admin/test_networks.py
@@ -20,8 +20,7 @@
 CONF = config.CONF
 
 
-class NetworksTest(base.BaseComputeAdminTest):
-    _api_version = 2
+class NetworksTest(base.BaseV2ComputeAdminTest):
 
     """
     Tests Nova Networks API that usually requires admin privileges.
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index ec2192f..1ee8f04 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -22,7 +22,6 @@
 from tempest.common.utils import data_utils
 from tempest.common import waiters
 from tempest import config
-from tempest import exceptions
 import tempest.test
 
 CONF = config.CONF
@@ -30,10 +29,9 @@
 LOG = logging.getLogger(__name__)
 
 
-class BaseComputeTest(tempest.test.BaseTestCase):
+class BaseV2ComputeTest(tempest.test.BaseTestCase):
     """Base test case class for all Compute API tests."""
 
-    _api_version = 2
     force_tenant_isolation = False
 
     # TODO(andreaf) We should care also for the alt_manager here
@@ -42,22 +40,18 @@
 
     @classmethod
     def skip_checks(cls):
-        super(BaseComputeTest, cls).skip_checks()
+        super(BaseV2ComputeTest, cls).skip_checks()
         if not CONF.service_available.nova:
             raise cls.skipException("Nova is not available")
-        if cls._api_version != 2:
-            msg = ("Unexpected API version is specified (%s)" %
-                   cls._api_version)
-            raise exceptions.InvalidConfiguration(message=msg)
 
     @classmethod
     def setup_credentials(cls):
         cls.set_network_resources()
-        super(BaseComputeTest, cls).setup_credentials()
+        super(BaseV2ComputeTest, cls).setup_credentials()
 
     @classmethod
     def setup_clients(cls):
-        super(BaseComputeTest, cls).setup_clients()
+        super(BaseV2ComputeTest, cls).setup_clients()
         cls.servers_client = cls.os.servers_client
         cls.server_groups_client = cls.os.server_groups_client
         cls.flavors_client = cls.os.flavors_client
@@ -96,7 +90,7 @@
 
     @classmethod
     def resource_setup(cls):
-        super(BaseComputeTest, cls).resource_setup()
+        super(BaseV2ComputeTest, cls).resource_setup()
         cls.build_interval = CONF.compute.build_interval
         cls.build_timeout = CONF.compute.build_timeout
         cls.ssh_user = CONF.compute.ssh_user
@@ -117,7 +111,7 @@
         cls.clear_servers()
         cls.clear_security_groups()
         cls.clear_server_groups()
-        super(BaseComputeTest, cls).resource_cleanup()
+        super(BaseV2ComputeTest, cls).resource_cleanup()
 
     @classmethod
     def clear_servers(cls):
@@ -147,7 +141,7 @@
            Method will delete the server when it's dirty.
            The setUp method is responsible for creating a new server.
            Exceptions raised in tearDown class are fails the test case,
-           This method supposed to use only by tierDown methods, when
+           This method supposed to use only by tearDown methods, when
            the shared server_id is stored in the server_id of the class.
         """
         if getattr(cls, 'server_id', None) is not None:
@@ -356,22 +350,13 @@
         return ip_or_server
 
 
-class BaseV2ComputeTest(BaseComputeTest):
-    _api_version = 2
-
-
-class BaseComputeAdminTest(BaseComputeTest):
+class BaseV2ComputeAdminTest(BaseV2ComputeTest):
     """Base test case class for Compute Admin API tests."""
 
     credentials = ['primary', 'admin']
 
     @classmethod
     def setup_clients(cls):
-        super(BaseComputeAdminTest, cls).setup_clients()
+        super(BaseV2ComputeAdminTest, cls).setup_clients()
         cls.availability_zone_admin_client = (
             cls.os_adm.availability_zone_client)
-
-
-class BaseV2ComputeAdminTest(BaseComputeAdminTest):
-    """Base test case class for Compute Admin V2 API tests."""
-    _api_version = 2
diff --git a/tempest/api/compute/certificates/test_certificates.py b/tempest/api/compute/certificates/test_certificates.py
index 0096fc2..d5c7302 100644
--- a/tempest/api/compute/certificates/test_certificates.py
+++ b/tempest/api/compute/certificates/test_certificates.py
@@ -20,9 +20,7 @@
 CONF = config.CONF
 
 
-class CertificatesV2TestJSON(base.BaseComputeTest):
-
-    _api_version = 2
+class CertificatesV2TestJSON(base.BaseV2ComputeTest):
 
     @classmethod
     def skip_checks(cls):
diff --git a/tempest/api/compute/flavors/test_flavors.py b/tempest/api/compute/flavors/test_flavors.py
index e114c80..7e01296 100644
--- a/tempest/api/compute/flavors/test_flavors.py
+++ b/tempest/api/compute/flavors/test_flavors.py
@@ -17,9 +17,7 @@
 from tempest import test
 
 
-class FlavorsV2TestJSON(base.BaseComputeTest):
-
-    _api_version = 2
+class FlavorsV2TestJSON(base.BaseV2ComputeTest):
     _min_disk = 'minDisk'
     _min_ram = 'minRam'
 
diff --git a/tempest/api/compute/keypairs/base.py b/tempest/api/compute/keypairs/base.py
index 76e5573..15f231b 100644
--- a/tempest/api/compute/keypairs/base.py
+++ b/tempest/api/compute/keypairs/base.py
@@ -16,11 +16,9 @@
 from tempest.api.compute import base
 
 
-class BaseKeypairTest(base.BaseComputeTest):
+class BaseKeypairTest(base.BaseV2ComputeTest):
     """Base test case class for all keypair API tests."""
 
-    _api_version = 2
-
     @classmethod
     def setup_clients(cls):
         super(BaseKeypairTest, cls).setup_clients()
diff --git a/tempest/api/compute/servers/test_availability_zone.py b/tempest/api/compute/servers/test_availability_zone.py
index 080441a..8f1f360 100644
--- a/tempest/api/compute/servers/test_availability_zone.py
+++ b/tempest/api/compute/servers/test_availability_zone.py
@@ -17,11 +17,10 @@
 from tempest import test
 
 
-class AZV2TestJSON(base.BaseComputeTest):
+class AZV2TestJSON(base.BaseV2ComputeTest):
     """
     Tests Availability Zone API List
     """
-    _api_version = 2
 
     @classmethod
     def setup_clients(cls):
diff --git a/tempest/api/compute/test_versions.py b/tempest/api/compute/test_versions.py
index f94cee6..8b84a21 100644
--- a/tempest/api/compute/test_versions.py
+++ b/tempest/api/compute/test_versions.py
@@ -16,7 +16,7 @@
 from tempest import test
 
 
-class TestVersions(base.BaseComputeTest):
+class TestVersions(base.BaseV2ComputeTest):
 
     @test.idempotent_id('6c0a0990-43b6-4529-9b61-5fd8daf7c55c')
     def test_list_api_versions(self):
diff --git a/tempest/config.py b/tempest/config.py
index 0cda018..26823de 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -255,6 +255,7 @@
                     "when sshing to a guest."),
     cfg.StrOpt('ssh_auth_method',
                default='keypair',
+               choices=('keypair', 'configured', 'adminpass', 'disabled'),
                help="Auth method used for authenticate to the instance. "
                     "Valid choices are: keypair, configured, adminpass "
                     "and disabled. "
@@ -264,6 +265,7 @@
                     "Disabled: avoid using ssh when it is an option."),
     cfg.StrOpt('ssh_connect_method',
                default='floating',
+               choices=('fixed', 'floating'),
                help="How to connect to the instance? "
                     "fixed: using the first ip belongs the fixed network "
                     "floating: creating and using a floating ip."),
@@ -330,7 +332,13 @@
                help='Unallocated floating IP range, which will be used to '
                     'test the floating IP bulk feature for CRUD operation. '
                     'This block must not overlap an existing floating IP '
-                    'pool.')
+                    'pool.'),
+    cfg.IntOpt('min_compute_nodes',
+               default=1,
+               help=('The minimum number of compute nodes expected. This will '
+                     'be utilized by some multinode specific tests to ensure '
+                     'that requests match the expected size of the cluster '
+                     'you are testing with.'))
 ]
 
 compute_features_group = cfg.OptGroup(name='compute-feature-enabled',
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 06ca09b..936fbe8 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -30,6 +30,9 @@
 RAND_NAME_HYPHEN_RE = re.compile(r".*rand_name\(.+[\-\_][\"\']\)")
 mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])")
 TESTTOOLS_SKIP_DECORATOR = re.compile(r'\s*@testtools\.skip\((.*)\)')
+METHOD = re.compile(r"^    def .+")
+METHOD_GET_RESOURCE = re.compile(r"^\s*def (list|show)\_.+")
+CLASS = re.compile(r"^class .+")
 
 
 def import_no_clients_in_api_and_scenario_tests(physical_line, filename):
@@ -143,6 +146,45 @@
                "decorators.skip_because from tempest-lib")
 
 
+def get_resources_on_service_clients(logical_line, physical_line, filename,
+                                     line_number, lines):
+    """Check that service client names of GET should be consistent
+
+    T110
+    """
+    if 'tempest/services/' not in filename:
+        return
+
+    ignored_list = []
+    with open('tempest/hacking/ignored_list_T110.txt') as f:
+        for line in f:
+            ignored_list.append(line.strip())
+
+    if filename in ignored_list:
+        return
+
+    if not METHOD.match(physical_line):
+        return
+
+    if pep8.noqa(physical_line):
+        return
+
+    for line in lines[line_number:]:
+        if METHOD.match(line) or CLASS.match(line):
+            # the end of a method
+            return
+
+        if 'self.get(' not in line:
+            continue
+
+        if METHOD_GET_RESOURCE.match(logical_line):
+            return
+
+        msg = ("T110: [GET /resources] methods should be list_<resource name>s"
+               " or show_<resource name>")
+        yield (0, msg)
+
+
 def factory(register):
     register(import_no_clients_in_api_and_scenario_tests)
     register(scenario_tests_need_service_tags)
@@ -152,3 +194,4 @@
     register(no_hyphen_at_end_of_rand_name)
     register(no_mutable_default_args)
     register(no_testtools_skip_decorator)
+    register(get_resources_on_service_clients)
diff --git a/tempest/hacking/ignored_list_T110.txt b/tempest/hacking/ignored_list_T110.txt
new file mode 100644
index 0000000..987861f
--- /dev/null
+++ b/tempest/hacking/ignored_list_T110.txt
@@ -0,0 +1,14 @@
+./tempest/services/compute/json/server_groups_client.py
+./tempest/services/compute/json/servers_client.py
+./tempest/services/database/json/flavors_client.py
+./tempest/services/identity/v3/json/credentials_client.py
+./tempest/services/identity/v3/json/identity_client.py
+./tempest/services/identity/v3/json/policy_client.py
+./tempest/services/identity/v3/json/region_client.py
+./tempest/services/messaging/json/messaging_client.py
+./tempest/services/object_storage/object_client.py
+./tempest/services/telemetry/json/telemetry_client.py
+./tempest/services/volume/json/qos_client.py
+./tempest/services/volume/json/backups_client.py
+./tempest/services/image/v2/json/image_client.py
+./tempest/services/baremetal/base.py
diff --git a/tempest/scenario/test_server_multinode.py b/tempest/scenario/test_server_multinode.py
new file mode 100644
index 0000000..ef78b4a
--- /dev/null
+++ b/tempest/scenario/test_server_multinode.py
@@ -0,0 +1,88 @@
+# Copyright 2012 OpenStack Foundation
+# 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 oslo_log import log as logging
+
+from tempest import config
+from tempest import exceptions
+from tempest.scenario import manager
+from tempest import test
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class TestServerMultinode(manager.ScenarioTest):
+
+    """
+    This is a set of tests specific to multinode testing.
+
+    """
+    credentials = ['primary', 'admin']
+
+    @classmethod
+    def setup_clients(cls):
+        super(TestServerMultinode, cls).setup_clients()
+        # Use admin client by default
+        cls.manager = cls.admin_manager
+        # this is needed so that we can use the availability_zone:host
+        # scheduler hint, which is admin_only by default
+        cls.servers_client = cls.admin_manager.servers_client
+        super(TestServerMultinode, cls).resource_setup()
+
+    @test.idempotent_id('9cecbe35-b9d4-48da-a37e-7ce70aa43d30')
+    @test.attr(type='smoke')
+    @test.services('compute', 'network')
+    def test_schedule_to_all_nodes(self):
+        host_client = self.manager.hosts_client
+        hosts = host_client.list_hosts()['hosts']
+        hosts = [x for x in hosts if x['service'] == 'compute']
+
+        # ensure we have at least as many compute hosts as we expect
+        if len(hosts) < CONF.compute.min_compute_nodes:
+            raise exceptions.InvalidConfiguration(
+                "Host list %s is shorter than min_compute_nodes. "
+                "Did a compute worker not boot correctly?" % hosts)
+
+        # create 1 compute for each node, up to the min_compute_nodes
+        # threshold (so that things don't get crazy if you have 1000
+        # compute nodes but set min to 3).
+        servers = []
+
+        for host in hosts[:CONF.compute.min_compute_nodes]:
+            create_kwargs = {
+                'availability_zone': '%(zone)s:%(host_name)s' % host
+            }
+
+            # by getting to active state here, this means this has
+            # landed on the host in question.
+            inst = self.create_server(image=CONF.compute.image_ref,
+                                      flavor=CONF.compute.flavor_ref,
+                                      create_kwargs=create_kwargs)
+            server = self.servers_client.show_server(inst['id'])['server']
+            servers.append(server)
+
+        # make sure we really have the number of servers we think we should
+        self.assertEqual(
+            len(servers), CONF.compute.min_compute_nodes,
+            "Incorrect number of servers built %s" % servers)
+
+        # ensure that every server ended up on a different host
+        host_ids = [x['hostId'] for x in servers]
+        self.assertEqual(
+            len(set(host_ids)), len(servers),
+            "Incorrect number of distinct host_ids scheduled to %s" % servers)