Enable CRUD for trunk ports

This patch enables basic CRUD operations on trunk ports and defines
related API extensions. Trunk ports and sub-ports can be persisted
in the Neutron model and are made visible through the API, but the
L2 agent is not notified and no trunk ports or subports are actually
instantiated on compute hosts.

This one of the main patches in the series that implement the end
to end functionality.

Partially-implements: blueprint vlan-aware-vms

Co-Authored-By: Armando Migliaccio <armamig@gmail.com>
Change-Id: I26453eb9a1b25e116193417271400994ac57e4c1
diff --git a/neutron/tests/tempest/api/test_trunk.py b/neutron/tests/tempest/api/test_trunk.py
new file mode 100644
index 0000000..589acf9
--- /dev/null
+++ b/neutron/tests/tempest/api/test_trunk.py
@@ -0,0 +1,133 @@
+# Copyright 2016 Hewlett Packard Enterprise Development Company LP
+#
+#    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.lib import exceptions as lib_exc
+from tempest import test
+
+from neutron.tests.tempest.api import base
+
+
+class TrunkTestJSONBase(base.BaseAdminNetworkTest):
+
+    def _create_trunk_with_network_and_parent(self, subports):
+        network = self.create_network()
+        parent_port = self.create_port(network)
+        return self.client.create_trunk(parent_port['id'], subports)
+
+
+class TrunkTestJSON(TrunkTestJSONBase):
+
+    @classmethod
+    @test.requires_ext(extension="trunk", service="network")
+    def resource_setup(cls):
+        super(TrunkTestJSON, cls).resource_setup()
+
+    def tearDown(self):
+        # NOTE(tidwellr) These tests create networks and ports, clean them up
+        # after each test to avoid hitting quota limits
+        self.resource_cleanup()
+        super(TrunkTestJSON, self).tearDown()
+
+    @test.idempotent_id('e1a6355c-4768-41f3-9bf8-0f1d192bd501')
+    def test_create_trunk_empty_subports_list(self):
+        trunk = self._create_trunk_with_network_and_parent([])
+        observed_trunk = self.client.show_trunk(trunk['trunk']['id'])
+        self.assertEqual(trunk, observed_trunk)
+
+    @test.idempotent_id('382dfa39-ca03-4bd3-9a1c-91e36d2e3796')
+    def test_create_trunk_subports_not_specified(self):
+        trunk = self._create_trunk_with_network_and_parent(None)
+        observed_trunk = self.client.show_trunk(trunk['trunk']['id'])
+        self.assertEqual(trunk, observed_trunk)
+
+    @test.idempotent_id('7de46c22-e2b6-4959-ac5a-0e624632ab32')
+    def test_create_show_delete_trunk(self):
+        trunk = self._create_trunk_with_network_and_parent(None)
+        trunk_id = trunk['trunk']['id']
+        parent_port_id = trunk['trunk']['port_id']
+        res = self.client.show_trunk(trunk_id)
+        self.assertEqual(trunk_id, res['trunk']['id'])
+        self.assertEqual(parent_port_id, res['trunk']['port_id'])
+        self.client.delete_trunk(trunk_id)
+        self.assertRaises(lib_exc.NotFound, self.client.show_trunk, trunk_id)
+
+    @test.idempotent_id('73365f73-bed6-42cd-960b-ec04e0c99d85')
+    def test_list_trunks(self):
+        trunk1 = self._create_trunk_with_network_and_parent(None)
+        trunk2 = self._create_trunk_with_network_and_parent(None)
+        expected_trunks = {trunk1['trunk']['id']: trunk1['trunk'],
+                           trunk2['trunk']['id']: trunk2['trunk']}
+        trunk_list = self.client.list_trunks()['trunks']
+        matched_trunks = [x for x in trunk_list if x['id'] in expected_trunks]
+        self.assertEqual(2, len(matched_trunks))
+        for trunk in matched_trunks:
+            self.assertEqual(expected_trunks[trunk['id']], trunk)
+
+    @test.idempotent_id('bb5fcead-09b5-484a-bbe6-46d1e06d6cc0')
+    def test_add_subport(self):
+        trunk = self._create_trunk_with_network_and_parent([])
+        network = self.create_network()
+        port = self.create_port(network)
+        subports = [{'port_id': port['id'],
+                     'segmentation_type': 'vlan',
+                     'segmentation_id': 2}]
+        self.client.add_subports(trunk['trunk']['id'], subports)
+        trunk = self.client.show_trunk(trunk['trunk']['id'])
+        observed_subports = trunk['trunk']['sub_ports']
+        self.assertEqual(1, len(observed_subports))
+        created_subport = observed_subports[0]
+        self.assertEqual(subports[0], created_subport)
+
+    @test.idempotent_id('96eea398-a03c-4c3e-a99e-864392c2ca53')
+    def test_remove_subport(self):
+        subport_parent1 = self.create_port(self.create_network())
+        subport_parent2 = self.create_port(self.create_network())
+        subports = [{'port_id': subport_parent1['id'],
+                     'segmentation_type': 'vlan',
+                     'segmentation_id': 2},
+                    {'port_id': subport_parent2['id'],
+                     'segmentation_type': 'vlan',
+                     'segmentation_id': 4}]
+        trunk = self._create_trunk_with_network_and_parent(subports)
+        removed_subport = trunk['trunk']['sub_ports'][0]
+        expected_subport = None
+
+        for subport in subports:
+            if subport['port_id'] != removed_subport['port_id']:
+                expected_subport = subport
+                break
+
+        # Remove the subport and validate PUT response
+        res = self.client.remove_subports(trunk['trunk']['id'],
+                                          [removed_subport])
+        self.assertEqual(1, len(res['sub_ports']))
+        self.assertEqual(expected_subport, res['sub_ports'][0])
+
+        # Validate the results of a subport list
+        trunk = self.client.show_trunk(trunk['trunk']['id'])
+        observed_subports = trunk['trunk']['sub_ports']
+        self.assertEqual(1, len(observed_subports))
+        self.assertEqual(expected_subport, observed_subports[0])
+
+    @test.idempotent_id('bb5fcaad-09b5-484a-dde6-4cd1ea6d6ff0')
+    def test_get_subports(self):
+        network = self.create_network()
+        port = self.create_port(network)
+        subports = [{'port_id': port['id'],
+                     'segmentation_type': 'vlan',
+                     'segmentation_id': 2}]
+        trunk = self._create_trunk_with_network_and_parent(subports)
+        trunk = self.client.get_subports(trunk['trunk']['id'])
+        observed_subports = trunk['sub_ports']
+        self.assertEqual(1, len(observed_subports))
diff --git a/neutron/tests/tempest/api/test_trunk_negative.py b/neutron/tests/tempest/api/test_trunk_negative.py
new file mode 100644
index 0000000..1cf37b7
--- /dev/null
+++ b/neutron/tests/tempest/api/test_trunk_negative.py
@@ -0,0 +1,190 @@
+# Copyright 2016 Hewlett Packard Enterprise Development Company LP
+#
+#    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_utils import uuidutils
+from tempest.lib import exceptions as lib_exc
+from tempest import test
+
+from neutron.tests.tempest.api import test_trunk
+
+
+class TrunkTestJSON(test_trunk.TrunkTestJSONBase):
+
+    def tearDown(self):
+        # NOTE(tidwellr) These tests create networks and ports, clean them up
+        # after each test to avoid hitting quota limits
+        self.resource_cleanup()
+        super(TrunkTestJSON, self).tearDown()
+
+    @classmethod
+    @test.requires_ext(extension="trunk", service="network")
+    def resource_setup(cls):
+        super(test_trunk.TrunkTestJSONBase, cls).resource_setup()
+
+    @test.attr(type='negative')
+    @test.idempotent_id('1b5cf87a-1d3a-4a94-ba64-647153d54f32')
+    def test_create_trunk_nonexistent_port_id(self):
+        self.assertRaises(lib_exc.NotFound, self.client.create_trunk,
+                          uuidutils.generate_uuid(), [])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('980bca3b-b0be-45ac-8067-b401e445b796')
+    def test_create_trunk_nonexistent_subport_port_id(self):
+        network = self.create_network()
+        parent_port = self.create_port(network)
+        self.assertRaises(lib_exc.NotFound, self.client.create_trunk,
+                          parent_port['id'],
+                          [{'port_id': uuidutils.generate_uuid(),
+                            'segmentation_type': 'vlan',
+                            'segmentation_id': 2}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('a5c5200a-72a0-43c5-a11a-52f808490344')
+    def test_create_subport_nonexistent_port_id(self):
+        trunk = self._create_trunk_with_network_and_parent([])
+        self.assertRaises(lib_exc.NotFound, self.client.add_subports,
+                          trunk['trunk']['id'],
+                          [{'port_id': uuidutils.generate_uuid(),
+                            'segmentation_type': 'vlan',
+                            'segmentation_id': 2}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('80deb6a9-da2a-48db-b7fd-bcef5b14edc1')
+    def test_create_subport_nonexistent_trunk(self):
+        network = self.create_network()
+        parent_port = self.create_port(network)
+        self.assertRaises(lib_exc.NotFound, self.client.add_subports,
+                          uuidutils.generate_uuid(),
+                          [{'port_id': parent_port['id'],
+                            'segmentation_type': 'vlan',
+                            'segmentation_id': 2}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('7e0f99ab-fe37-408b-a889-9e44ef300084')
+    def test_create_subport_missing_segmentation_id(self):
+        trunk = self._create_trunk_with_network_and_parent([])
+        subport_network = self.create_network()
+        parent_port = self.create_port(subport_network)
+        self.assertRaises(lib_exc.BadRequest, self.client.add_subports,
+                          trunk['trunk']['id'],
+                          [{'port_id': parent_port['id'],
+                            'segmentation_type': 'vlan'}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('a315d78b-2f43-4efa-89ae-166044c568aa')
+    def test_create_trunk_with_subport_missing_segmentation_id(self):
+        subport_network = self.create_network()
+        parent_port = self.create_port(subport_network)
+        self.assertRaises(lib_exc.BadRequest, self.client.create_trunk,
+                          parent_port['id'],
+                          [{'port_id': uuidutils.generate_uuid(),
+                            'segmentation_type': 'vlan'}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('33498618-f75a-4796-8ae6-93d4fd203fa4')
+    def test_create_trunk_with_subport_missing_segmentation_type(self):
+        subport_network = self.create_network()
+        parent_port = self.create_port(subport_network)
+        self.assertRaises(lib_exc.BadRequest, self.client.create_trunk,
+                          parent_port['id'],
+                          [{'port_id': uuidutils.generate_uuid(),
+                            'segmentation_id': 3}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('a717691c-4e07-4d81-a98d-6f1c18c5d183')
+    def test_create_trunk_with_subport_missing_port_id(self):
+        subport_network = self.create_network()
+        parent_port = self.create_port(subport_network)
+        self.assertRaises(lib_exc.BadRequest, self.client.create_trunk,
+                          parent_port['id'],
+                          [{'segmentation_type': 'vlan',
+                            'segmentation_id': 3}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('40aed9be-e976-47d0-a555-bde2c7e74e57')
+    def test_create_trunk_duplicate_subport_segmentation_ids(self):
+        trunk = self._create_trunk_with_network_and_parent([])
+        subport_network1 = self.create_network()
+        subport_network2 = self.create_network()
+        parent_port1 = self.create_port(subport_network1)
+        parent_port2 = self.create_port(subport_network2)
+        self.assertRaises(lib_exc.BadRequest, self.client.create_trunk,
+                          trunk['trunk']['id'],
+                          [{'port_id': parent_port1['id'],
+                            'segmentation_id': 2,
+                            'segmentation_type': 'vlan'},
+                           {'port_id': parent_port2['id'],
+                            'segmentation_id': 2,
+                            'segmentation_type': 'vlan'}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('6f132ccc-1380-42d8-9c44-50411612bd01')
+    def test_add_subport_port_id_uses_trunk_port_id(self):
+        trunk = self._create_trunk_with_network_and_parent(None)
+        self.assertRaises(lib_exc.Conflict, self.client.add_subports,
+                          trunk['trunk']['id'],
+                          [{'port_id': trunk['trunk']['port_id'],
+                            'segmentation_type': 'vlan',
+                            'segmentation_id': 2}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('00cb40bb-1593-44c8-808c-72b47e64252f')
+    def test_add_subport_duplicate_segmentation_details(self):
+        trunk = self._create_trunk_with_network_and_parent(None)
+        network = self.create_network()
+        parent_port1 = self.create_port(network)
+        parent_port2 = self.create_port(network)
+        self.client.add_subports(trunk['trunk']['id'],
+                                 [{'port_id': parent_port1['id'],
+                                   'segmentation_type': 'vlan',
+                                   'segmentation_id': 2}])
+        self.assertRaises(lib_exc.Conflict, self.client.add_subports,
+                          trunk['trunk']['id'],
+                          [{'port_id': parent_port2['id'],
+                            'segmentation_type': 'vlan',
+                            'segmentation_id': 2}])
+
+    @test.attr(type='negative')
+    @test.idempotent_id('4eac8c25-83ee-4051-9620-34774f565730')
+    def test_add_subport_passing_dict(self):
+        trunk = self._create_trunk_with_network_and_parent(None)
+        self.assertRaises(lib_exc.BadRequest, self.client.add_subports,
+                          trunk['trunk']['id'],
+                          {'port_id': trunk['trunk']['port_id'],
+                           'segmentation_type': 'vlan',
+                           'segmentation_id': 2})
+
+    @test.attr(type='negative')
+    @test.idempotent_id('17ca7dd7-96a8-445a-941e-53c0c86c2fe2')
+    def test_remove_subport_passing_dict(self):
+        network = self.create_network()
+        parent_port = self.create_port(network)
+        subport_data = {'port_id': parent_port['id'],
+                        'segmentation_type': 'vlan',
+                        'segmentation_id': 2}
+        trunk = self._create_trunk_with_network_and_parent([subport_data])
+        self.assertRaises(lib_exc.BadRequest, self.client.remove_subports,
+                          trunk['trunk']['id'], subport_data)
+
+    @test.attr(type='negative')
+    @test.idempotent_id('aaca7dd7-96b8-445a-931e-63f0d86d2fe2')
+    def test_remove_subport_not_found(self):
+        network = self.create_network()
+        parent_port = self.create_port(network)
+        subport_data = {'port_id': parent_port['id'],
+                        'segmentation_type': 'vlan',
+                        'segmentation_id': 2}
+        trunk = self._create_trunk_with_network_and_parent([])
+        self.assertRaises(lib_exc.NotFound, self.client.remove_subports,
+                          trunk['trunk']['id'], [subport_data])
diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py
index d9e333c..3220604 100644
--- a/neutron/tests/tempest/services/network/json/network_client.py
+++ b/neutron/tests/tempest/services/network/json/network_client.py
@@ -659,6 +659,64 @@
         body = jsonutils.loads(body)
         return service_client.ResponseBody(resp, body)
 
+    def create_trunk(self, parent_port_id, subports, tenant_id=None):
+        uri = '%s/trunks' % self.uri_prefix
+        post_data = {
+            'trunk': {
+                'port_id': parent_port_id,
+            }
+        }
+        if subports is not None:
+            post_data['trunk']['sub_ports'] = subports
+        if tenant_id is not None:
+            post_data['trunk']['tenant_id'] = tenant_id
+        resp, body = self.post(uri, self.serialize(post_data))
+        body = self.deserialize_single(body)
+        self.expected_success(201, resp.status)
+        return service_client.ResponseBody(resp, body)
+
+    def show_trunk(self, trunk_id):
+        uri = '%s/trunks/%s' % (self.uri_prefix, trunk_id)
+        resp, body = self.get(uri)
+        body = self.deserialize_single(body)
+        self.expected_success(200, resp.status)
+        return service_client.ResponseBody(resp, body)
+
+    def list_trunks(self, **kwargs):
+        uri = '%s/trunks' % self.uri_prefix
+        if kwargs:
+            uri += '?' + urlparse.urlencode(kwargs, doseq=1)
+        resp, body = self.get(uri)
+        self.expected_success(200, resp.status)
+        body = self.deserialize_single(body)
+        return service_client.ResponseBody(resp, body)
+
+    def delete_trunk(self, trunk_id):
+        uri = '%s/trunks/%s' % (self.uri_prefix, trunk_id)
+        resp, body = self.delete(uri)
+        self.expected_success(204, resp.status)
+        return service_client.ResponseBody(resp, body)
+
+    def _subports_action(self, action, trunk_id, subports):
+        uri = '%s/trunks/%s/%s' % (self.uri_prefix, trunk_id, action)
+        resp, body = self.put(uri, jsonutils.dumps(subports))
+        body = self.deserialize_single(body)
+        self.expected_success(200, resp.status)
+        return service_client.ResponseBody(resp, body)
+
+    def add_subports(self, trunk_id, subports):
+        return self._subports_action('add_subports', trunk_id, subports)
+
+    def remove_subports(self, trunk_id, subports):
+        return self._subports_action('remove_subports', trunk_id, subports)
+
+    def get_subports(self, trunk_id):
+        uri = '%s/trunks/%s/%s' % (self.uri_prefix, trunk_id, 'get_subports')
+        resp, body = self.get(uri)
+        self.expected_success(200, resp.status)
+        body = jsonutils.loads(body)
+        return service_client.ResponseBody(resp, body)
+
     def get_auto_allocated_topology(self, tenant_id=None):
         uri = '%s/auto-allocated-topology/%s' % (self.uri_prefix, tenant_id)
         resp, body = self.get(uri)