Start instances using fixed network when possible

Uses the fixed_network_name option to select which network
to boot test instances on.  This allows Tempest to be run
in environments where there are multiple networks available.

This problem does not apply to nova-network environments because
it will happily start instances with multiple networks attached,
so the behavior is left as before in that case.

Co-Authored-By: Ben Nemec <bnemec@redhat.com>
Co-Authored-By: Vincent Untz <vuntz@suse.com>
Co-Authored-By: git-harry <git-harry@live.co.uk>

Change-Id: Ib5b84b59e3d182d8b9cc83954537c32f3eb9e388
Closes-Bug: #1250866
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index c872184..ef3a029 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -16,6 +16,7 @@
 from tempest_lib import decorators
 
 from tempest.api.compute import base
+from tempest.common import fixed_network
 from tempest import test
 
 
@@ -112,7 +113,10 @@
         name = data_utils.rand_name('server')
         flavor = self.flavor_ref
         image_id = self.image_ref
-        test_server = self.client.create_server(name, image_id, flavor)
+        network = self.get_tenant_network()
+        network_kwargs = fixed_network.set_networks_kwarg(network)
+        test_server = self.client.create_server(name, image_id, flavor,
+                                                **network_kwargs)
         self.addCleanup(self.client.delete_server, test_server['id'])
         self.client.wait_for_server_status(test_server['id'], 'ACTIVE')
         server = self.client.get_server(test_server['id'])
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index ddfe6de..b794101 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -22,6 +22,7 @@
 
 from tempest import clients
 from tempest.common import credentials
+from tempest.common import fixed_network
 from tempest import config
 from tempest import exceptions
 import tempest.test
@@ -212,6 +213,8 @@
         flavor = kwargs.get('flavor', cls.flavor_ref)
         image_id = kwargs.get('image_id', cls.image_ref)
 
+        kwargs = fixed_network.set_networks_kwarg(
+            cls.get_tenant_network(), kwargs) or {}
         body = cls.servers_client.create_server(
             name, image_id, flavor, **kwargs)
 
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 0702f3f..42a61da 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -13,13 +13,15 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import time
+
+from tempest_lib import exceptions as lib_exc
+
 from tempest.api.compute import base
 from tempest import config
 from tempest import exceptions
 from tempest import test
 
-import time
-
 CONF = config.CONF
 
 
@@ -125,8 +127,15 @@
         self.assertTrue(interface_count > 0)
         self._check_interface(ifs[0])
 
-        iface = self._test_create_interface(server)
-        ifs.append(iface)
+        try:
+            iface = self._test_create_interface(server)
+        except lib_exc.BadRequest as e:
+            msg = ('Multiple possible networks found, use a Network ID to be '
+                   'more specific.')
+            if not CONF.compute.fixed_network_name and e.message == msg:
+                raise
+        else:
+            ifs.append(iface)
 
         iface = self._test_create_interface_by_network_id(server, ifs)
         ifs.append(iface)
diff --git a/tempest/api/compute/servers/test_list_server_filters.py b/tempest/api/compute/servers/test_list_server_filters.py
index 5c10f30..70e4dff 100644
--- a/tempest/api/compute/servers/test_list_server_filters.py
+++ b/tempest/api/compute/servers/test_list_server_filters.py
@@ -19,6 +19,7 @@
 
 from tempest.api.compute import base
 from tempest.api import utils
+from tempest.common import fixed_network
 from tempest import config
 from tempest import test
 
@@ -66,9 +67,13 @@
             raise RuntimeError("Image %s (image_ref_alt) was not found!" %
                                cls.image_ref_alt)
 
+        network = cls.get_tenant_network()
+        cls.fixed_network_name = network['name']
+        network_kwargs = fixed_network.set_networks_kwarg(network)
         cls.s1_name = data_utils.rand_name(cls.__name__ + '-instance')
         cls.s1 = cls.create_test_server(name=cls.s1_name,
-                                        wait_until='ACTIVE')
+                                        wait_until='ACTIVE',
+                                        **network_kwargs)
 
         cls.s2_name = data_utils.rand_name(cls.__name__ + '-instance')
         cls.s2 = cls.create_test_server(name=cls.s2_name,
@@ -80,12 +85,6 @@
                                         flavor=cls.flavor_ref_alt,
                                         wait_until='ACTIVE')
 
-        cls.fixed_network_name = CONF.compute.fixed_network_name
-        if CONF.service_available.neutron:
-            if hasattr(cls.isolated_creds, 'get_primary_network'):
-                network = cls.isolated_creds.get_primary_network()
-                cls.fixed_network_name = network['name']
-
     @test.idempotent_id('05e8a8e7-9659-459a-989d-92c2f501f4ba')
     @utils.skip_unless_attr('multiple_images', 'Only one image found')
     @test.attr(type='gate')
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 157bd44..1f76b1c 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -18,6 +18,7 @@
 from tempest_lib import exceptions as lib_exc
 
 from tempest import clients
+from tempest.common import fixed_network
 from tempest import config
 from tempest import exceptions
 import tempest.test
@@ -62,6 +63,7 @@
         super(BaseVolumeTest, cls).setup_clients()
 
         cls.servers_client = cls.os.servers_client
+        cls.networks_client = cls.os.networks_client
 
         if cls._api_version == 1:
             cls.snapshots_client = cls.os.snapshots_client
@@ -159,6 +161,15 @@
             except Exception:
                 pass
 
+    @classmethod
+    def create_server(cls, name, **kwargs):
+        network = cls.get_tenant_network()
+        network_kwargs = fixed_network.set_networks_kwarg(network, kwargs)
+        return cls.servers_client.create_server(name,
+                                                cls.image_ref,
+                                                cls.flavor_ref,
+                                                **network_kwargs)
+
 
 class BaseVolumeAdminTest(BaseVolumeTest):
     """Base test case class for all Volume Admin API tests."""
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index 3de5021..1872ec7 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -36,9 +36,7 @@
 
         # Create a test shared instance
         srv_name = data_utils.rand_name(cls.__name__ + '-Instance')
-        cls.server = cls.servers_client.create_server(srv_name,
-                                                      cls.image_ref,
-                                                      cls.flavor_ref)
+        cls.server = cls.create_server(srv_name)
         cls.servers_client.wait_for_server_status(cls.server['id'], 'ACTIVE')
 
         # Create a test shared volume for attach/detach tests
diff --git a/tempest/api/volume/test_volumes_negative.py b/tempest/api/volume/test_volumes_negative.py
index af6e24e..a47e964 100644
--- a/tempest/api/volume/test_volumes_negative.py
+++ b/tempest/api/volume/test_volumes_negative.py
@@ -179,9 +179,7 @@
     @test.services('compute')
     def test_attach_volumes_with_nonexistent_volume_id(self):
         srv_name = data_utils.rand_name('Instance')
-        server = self.servers_client.create_server(srv_name,
-                                                   self.image_ref,
-                                                   self.flavor_ref)
+        server = self.create_server(srv_name)
         self.addCleanup(self.servers_client.delete_server, server['id'])
         self.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
         self.assertRaises(lib_exc.NotFound,
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 2b6339a..b277390 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -69,9 +69,7 @@
         # Create a snapshot when volume status is in-use
         # Create a test instance
         server_name = data_utils.rand_name('instance')
-        server = self.servers_client.create_server(server_name,
-                                                   self.image_ref,
-                                                   self.flavor_ref)
+        server = self.create_server(server_name)
         self.addCleanup(self.servers_client.delete_server, server['id'])
         self.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
         mountpoint = '/dev/%s' % CONF.compute.volume_device_name
diff --git a/tempest/common/fixed_network.py b/tempest/common/fixed_network.py
new file mode 100644
index 0000000..83822c2
--- /dev/null
+++ b/tempest/common/fixed_network.py
@@ -0,0 +1,83 @@
+#    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 copy
+from oslo_log import log as logging
+
+from tempest_lib import exceptions as lib_exc
+
+from tempest import config
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+def get_tenant_network(creds_provider, compute_networks_client):
+    """Get a network usable by the primary tenant
+
+    :param creds_provider: instance of credential provider
+    :param compute_networks_client: compute network client. We want to have the
+           compute network client so we can have use a common approach for both
+           neutron and nova-network cases. If this is not an admin network
+           client, set_network_kwargs might fail in case fixed_network_name
+           is the network to be used, and it's not visible to the tenant
+    :return a dict with 'id' and 'name' of the network
+    """
+    fixed_network_name = CONF.compute.fixed_network_name
+    # NOTE(andreaf) get_primary_network will always be available once
+    # bp test-accounts-continued is implemented
+    if (CONF.auth.allow_tenant_isolation and
+        (CONF.service_available.neutron and
+         not CONF.service_available.ironic)):
+        network = creds_provider.get_primary_network()
+    else:
+        if fixed_network_name:
+            try:
+                resp = compute_networks_client.list_networks(
+                    name=fixed_network_name)
+                if isinstance(resp, list):
+                    networks = resp
+                elif isinstance(resp, dict):
+                    networks = resp['networks']
+                else:
+                    raise lib_exc.NotFound()
+                network = networks[0]
+                # To be consistent with network isolation, add name is only
+                # label is available
+                network['name'] = network.get('name', network.get('label'))
+            except lib_exc.NotFound:
+                # In case of nova network, if the fixed_network_name is not
+                # owned by the tenant, and the network client is not an admin
+                # one, list_networks will not find it
+                LOG.info('Unable to find network %s. '
+                         'Starting instance without specifying a network.' %
+                         fixed_network_name)
+                network = {'name': fixed_network_name}
+    LOG.info('Found network %s available for tenant' % network)
+    return network
+
+
+def set_networks_kwarg(network, kwargs=None):
+    """Set 'networks' kwargs for a server create if missing
+
+    :param network: dict of network to be used with 'id' and 'name'
+    :param kwargs: server create kwargs to be enhanced
+    :return: new dict of kwargs updated to include networks
+    """
+    params = copy.copy(kwargs) or {}
+    if kwargs and 'networks' in kwargs:
+        return params
+
+    if network:
+        params.update({"networks": [{'uuid': network['id']}]})
+    return params
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index f8cc17c..bae8296 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -24,6 +24,7 @@
 
 from tempest import clients
 from tempest.common import credentials
+from tempest.common import fixed_network
 from tempest.common.utils.linux import remote_client
 from tempest import config
 from tempest import exceptions
@@ -184,6 +185,8 @@
             flavor = CONF.compute.flavor_ref
         if create_kwargs is None:
             create_kwargs = {}
+        network = self.get_tenant_network()
+        fixed_network.set_networks_kwarg(network, create_kwargs)
 
         LOG.debug("Creating a server (name: %s, image: %s, flavor: %s)",
                   name, image, flavor)
diff --git a/tempest/services/compute/json/networks_client.py b/tempest/services/compute/json/networks_client.py
index ef1c058..0ae0920 100644
--- a/tempest/services/compute/json/networks_client.py
+++ b/tempest/services/compute/json/networks_client.py
@@ -20,11 +20,15 @@
 
 class NetworksClientJSON(service_client.ServiceClient):
 
-    def list_networks(self):
+    def list_networks(self, name=None):
         resp, body = self.get("os-networks")
         body = json.loads(body)
         self.expected_success(200, resp.status)
-        return service_client.ResponseBodyList(resp, body['networks'])
+        if name:
+            networks = [n for n in body['networks'] if n['label'] == name]
+        else:
+            networks = body['networks']
+        return service_client.ResponseBodyList(resp, networks)
 
     def get_network(self, network_id):
         resp, body = self.get("os-networks/%s" % str(network_id))
diff --git a/tempest/test.py b/tempest/test.py
index 19bae74..da936b4 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -32,6 +32,7 @@
 
 from tempest import clients
 from tempest.common import credentials
+from tempest.common import fixed_network
 import tempest.common.generator.valid_generator as valid
 from tempest import config
 from tempest import exceptions
@@ -435,6 +436,21 @@
                 'subnet': subnet,
                 'dhcp': dhcp}
 
+    @classmethod
+    def get_tenant_network(cls):
+        """Get the network to be used in testing
+
+        :return: network dict including 'id' and 'name'
+        """
+        # Make sure isolated_creds exists and get a network client
+        networks_client = cls.get_client_manager().networks_client
+        isolated_creds = getattr(cls, 'isolated_creds', None)
+        if credentials.is_admin_available():
+            admin_creds = isolated_creds.get_admin_creds()
+            networks_client = clients.Manager(admin_creds).networks_client
+        return fixed_network.get_tenant_network(isolated_creds,
+                                                networks_client)
+
     def assertEmpty(self, list, msg=None):
         self.assertTrue(len(list) == 0, msg)