Merge "Add compute API tests for 'get-me-a-network'"
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 5263fdd..bff18f8 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -221,3 +221,7 @@
  * `2.25`_
 
  .. _2.25: http://docs.openstack.org/developer/nova/api_microversion_history.html#maximum-in-mitaka
+
+ * `2.37`_
+
+ .. _2.37: http://docs.openstack.org/developer/nova/api_microversion_history.html#id34
diff --git a/tempest/api/compute/admin/test_auto_allocate_network.py b/tempest/api/compute/admin/test_auto_allocate_network.py
new file mode 100644
index 0000000..052c17d
--- /dev/null
+++ b/tempest/api/compute/admin/test_auto_allocate_network.py
@@ -0,0 +1,205 @@
+# Copyright 2016 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.
+
+from oslo_log import log
+
+from tempest.api.compute import base
+from tempest.common import compute
+from tempest.common import waiters
+from tempest import config
+from tempest import exceptions
+from tempest.lib.common.utils import test_utils
+from tempest import test
+
+CONF = config.CONF
+LOG = log.getLogger(__name__)
+
+
+# NOTE(mriedem): This is in the admin directory only because it requires
+# force_tenant_isolation=True, but doesn't extend BaseV2ComputeAdminTest
+# because it doesn't actually use any admin credentials in the tests.
+class AutoAllocateNetworkTest(base.BaseV2ComputeTest):
+    """Tests auto-allocating networks with the v2.37 microversion.
+
+    These tests rely on Neutron being enabled. Also, the tenant must not have
+    any network resources available to it so we can make sure that Nova
+    calls to Neutron to automatically allocate the network topology.
+    """
+
+    force_tenant_isolation = True
+
+    min_microversion = '2.37'
+    max_microversion = 'latest'
+
+    @classmethod
+    def skip_checks(cls):
+        super(AutoAllocateNetworkTest, cls).skip_checks()
+        if not CONF.service_available.neutron:
+            raise cls.skipException('Neutron is required')
+        if not test.is_extension_enabled('auto-allocated-topology', 'network'):
+            raise cls.skipException(
+                'auto-allocated-topology extension is not available')
+
+    @classmethod
+    def setup_credentials(cls):
+        # Do not create network resources for these tests.
+        cls.set_network_resources()
+        super(AutoAllocateNetworkTest, cls).setup_credentials()
+
+    @classmethod
+    def setup_clients(cls):
+        super(AutoAllocateNetworkTest, cls).setup_clients()
+        cls.servers_client = cls.servers_client
+        cls.networks_client = cls.os.networks_client
+        cls.routers_client = cls.os.routers_client
+        cls.subnets_client = cls.os.subnets_client
+        cls.ports_client = cls.os.ports_client
+
+    @classmethod
+    def resource_setup(cls):
+        super(AutoAllocateNetworkTest, cls).resource_setup()
+        # Sanity check that there are no networks available to the tenant.
+        # This is essentially what Nova does for getting available networks.
+        tenant_id = cls.networks_client.tenant_id
+        # (1) Retrieve non-public network list owned by the tenant.
+        search_opts = {'tenant_id': tenant_id, 'shared': False}
+        nets = cls.networks_client.list_networks(
+            **search_opts).get('networks', [])
+        if nets:
+            raise exceptions.TempestException(
+                'Found tenant networks: %s' % nets)
+        # (2) Retrieve shared network list.
+        search_opts = {'shared': True}
+        nets = cls.networks_client.list_networks(
+            **search_opts).get('networks', [])
+        if nets:
+            raise exceptions.TempestException(
+                'Found shared networks: %s' % nets)
+
+    @classmethod
+    def resource_cleanup(cls):
+        """Deletes any auto_allocated_network and it's associated resources."""
+
+        # Find the auto-allocated router for the tenant.
+        # This is a bit hacky since we don't have a great way to find the
+        # auto-allocated router given the private tenant network we have.
+        routers = cls.routers_client.list_routers().get('routers', [])
+        if len(routers) > 1:
+            # This indicates a race where nova is concurrently calling the
+            # neutron auto-allocated-topology API for multiple server builds
+            # at the same time (it's called from nova-compute when setting up
+            # networking for a server). Neutron will detect duplicates and
+            # automatically clean them up, but there is a window where the API
+            # can return multiple and we don't have a good way to filter those
+            # out right now, so we'll just handle them.
+            LOG.info('(%s) Found more than one router for tenant.',
+                     test_utils.find_test_caller())
+
+        # Let's just blindly remove any networks, duplicate or otherwise, that
+        # the test might have created even though Neutron will cleanup
+        # duplicate resources automatically (so ignore 404s).
+        networks = cls.networks_client.list_networks().get('networks', [])
+
+        for router in routers:
+            # Disassociate the subnets from the router. Because of the race
+            # mentioned above the subnets might not be associated with the
+            # router so ignore any 404.
+            for network in networks:
+                for subnet_id in network['subnets']:
+                    test_utils.call_and_ignore_notfound_exc(
+                        cls.routers_client.remove_router_interface,
+                        router['id'], subnet_id=subnet_id)
+
+            # Delete the router.
+            cls.routers_client.delete_router(router['id'])
+
+        for network in networks:
+            # Get and delete the ports for the given network.
+            ports = cls.ports_client.list_ports(
+                network_id=network['id']).get('ports', [])
+            for port in ports:
+                test_utils.call_and_ignore_notfound_exc(
+                    cls.ports_client.delete_port, port['id'])
+
+            # Delete the subnets.
+            for subnet_id in network['subnets']:
+                test_utils.call_and_ignore_notfound_exc(
+                    cls.subnets_client.delete_subnet, subnet_id)
+
+            # Delete the network.
+            test_utils.call_and_ignore_notfound_exc(
+                cls.networks_client.delete_network, network['id'])
+
+    @test.idempotent_id('5eb7b8fa-9c23-47a2-9d7d-02ed5809dd34')
+    def test_server_create_no_allocate(self):
+        """Tests that no networking is allocated for the server."""
+        # create the server with no networking
+        server, _ = compute.create_test_server(
+            self.os, networks='none', wait_until='ACTIVE')
+        self.addCleanup(waiters.wait_for_server_termination,
+                        self.servers_client, server['id'])
+        self.addCleanup(self.servers_client.delete_server, server['id'])
+        # get the server ips
+        addresses = self.servers_client.list_addresses(
+            server['id'])['addresses']
+        # assert that there is no networking
+        self.assertEqual({}, addresses)
+
+    @test.idempotent_id('2e6cf129-9e28-4e8a-aaaa-045ea826b2a6')
+    def test_server_multi_create_auto_allocate(self):
+        """Tests that networking is auto-allocated for multiple servers."""
+
+        # Create multiple servers with auto networking to make sure the
+        # automatic network allocation is atomic. Using a minimum of three
+        # servers is essential for this scenario because:
+        #
+        # - First request sees no networks for the tenant so it auto-allocates
+        #   one from Neutron, let's call that net1.
+        # - Second request sees no networks for the tenant so it auto-allocates
+        #   one from Neutron. Neutron creates net2 but sees it's a duplicate
+        #   so it queues net2 for deletion and returns net1 from the API and
+        #   Nova uses that for the second server request.
+        # - Third request sees net1 and net2 for the tenant and fails with a
+        #   NetworkAmbiguous 400 error.
+        _, servers = compute.create_test_server(
+            self.os, networks='auto', wait_until='ACTIVE',
+            min_count=3)
+        server_nets = set()
+        for server in servers:
+            self.addCleanup(waiters.wait_for_server_termination,
+                            self.servers_client, server['id'])
+            self.addCleanup(self.servers_client.delete_server, server['id'])
+            # get the server ips
+            addresses = self.servers_client.list_addresses(
+                server['id'])['addresses']
+            # assert that there is networking (should only be one)
+            self.assertEqual(1, len(addresses))
+            server_nets.add(list(addresses.keys())[0])
+        # all servers should be on the same network
+        self.assertEqual(1, len(server_nets))
+
+        # List the networks for the tenant; we filter on admin_state_up=True
+        # because the auto-allocated-topology code in Neutron won't set that
+        # to True until the network is ready and is returned from the API.
+        # Duplicate networks created from a race should have
+        # admin_state_up=False.
+        search_opts = {'tenant_id': self.networks_client.tenant_id,
+                       'shared': False,
+                       'admin_state_up': True}
+        nets = self.networks_client.list_networks(
+            **search_opts).get('networks', [])
+        self.assertEqual(1, len(nets))
+        # verify the single private tenant network is the one that the servers
+        # are using also
+        self.assertIn(nets[0]['name'], server_nets)
diff --git a/tempest/lib/api_schema/response/compute/v2_26/__init__.py b/tempest/lib/api_schema/response/compute/v2_26/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_26/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_26/servers.py b/tempest/lib/api_schema/response/compute/v2_26/servers.py
new file mode 100644
index 0000000..bc5d18e
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_26/servers.py
@@ -0,0 +1,47 @@
+# Copyright 2016 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import servers as servers21
+from tempest.lib.api_schema.response.compute.v2_19 import servers as servers219
+
+# The 2.26 microversion changes the server GET and (detailed) LIST responses to
+# include the server 'tags' which is just a list of strings.
+
+tag_items = {
+    'type': 'array',
+    'maxItems': 50,
+    'items': {
+        'type': 'string',
+        'pattern': '^[^,/]*$',
+        'maxLength': 60
+    }
+}
+
+get_server = copy.deepcopy(servers219.get_server)
+get_server['response_body']['properties']['server'][
+    'properties'].update({'tags': tag_items})
+get_server['response_body']['properties']['server'][
+    'required'].append('tags')
+
+list_servers_detail = copy.deepcopy(servers219.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+    'properties'].update({'tags': tag_items})
+list_servers_detail['response_body']['properties']['servers']['items'][
+    'required'].append('tags')
+
+# list response schema wasn't changed for v2.26 so use v2.1
+
+list_servers = copy.deepcopy(servers21.list_servers)
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 24c0be9..8b22be0 100755
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -22,6 +22,7 @@
 from tempest.lib.api_schema.response.compute.v2_1 import servers as schema
 from tempest.lib.api_schema.response.compute.v2_16 import servers as schemav216
 from tempest.lib.api_schema.response.compute.v2_19 import servers as schemav219
+from tempest.lib.api_schema.response.compute.v2_26 import servers as schemav226
 from tempest.lib.api_schema.response.compute.v2_3 import servers as schemav23
 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
 from tempest.lib.common import rest_client
@@ -34,7 +35,8 @@
         {'min': '2.3', 'max': '2.8', 'schema': schemav23},
         {'min': '2.9', 'max': '2.15', 'schema': schemav29},
         {'min': '2.16', 'max': '2.18', 'schema': schemav216},
-        {'min': '2.19', 'max': None, 'schema': schemav219}]
+        {'min': '2.19', 'max': '2.25', 'schema': schemav219},
+        {'min': '2.26', 'max': None, 'schema': schemav226}]
 
     def __init__(self, auth_provider, service, region,
                  enable_instance_password=True, **kwargs):