Merge "Bulk creation of SecurityGroups"
diff --git a/neutron/tests/tempest/api/base.py b/neutron/tests/tempest/api/base.py
index b308a31..0d77064 100644
--- a/neutron/tests/tempest/api/base.py
+++ b/neutron/tests/tempest/api/base.py
@@ -109,6 +109,7 @@
         cls.admin_address_scopes = []
         cls.subnetpools = []
         cls.admin_subnetpools = []
+        cls.security_groups = []
 
     @classmethod
     def resource_cleanup(cls):
@@ -167,6 +168,11 @@
                 cls._try_delete_resource(cls.admin_client.delete_network,
                                          network['id'])
 
+            # Clean up security groups
+            for secgroup in cls.security_groups:
+                cls._try_delete_resource(cls.client.delete_security_group,
+                                         secgroup['id'])
+
             for subnetpool in cls.subnetpools:
                 cls._try_delete_resource(cls.client.delete_subnetpool,
                                          subnetpool['id'])
@@ -343,6 +349,11 @@
         return interface
 
     @classmethod
+    def get_supported_qos_rule_types(cls):
+        body = cls.client.list_qos_rule_types()
+        return [rule_type['type'] for rule_type in body['rule_types']]
+
+    @classmethod
     def create_qos_policy(cls, name, description=None, shared=False,
                           tenant_id=None):
         """Wrapper utility that returns a test QoS policy."""
@@ -486,6 +497,18 @@
         raise exceptions.InvalidConfiguration(message)
 
 
+def require_qos_rule_type(rule_type):
+    def decorator(f):
+        @functools.wraps(f)
+        def wrapper(self, *func_args, **func_kwargs):
+            if rule_type not in self.get_supported_qos_rule_types():
+                raise self.skipException(
+                    "%s rule type is required." % rule_type)
+            return f(self, *func_args, **func_kwargs)
+        return wrapper
+    return decorator
+
+
 def _require_sorting(f):
     @functools.wraps(f)
     def inner(self, *args, **kwargs):
diff --git a/neutron/tests/tempest/api/test_floating_ips.py b/neutron/tests/tempest/api/test_floating_ips.py
index bafa54c..6e722db 100644
--- a/neutron/tests/tempest/api/test_floating_ips.py
+++ b/neutron/tests/tempest/api/test_floating_ips.py
@@ -100,4 +100,4 @@
         # disassociate
         body = self.client.update_floatingip(body['floatingip']['id'],
                                              port_id=None)
-        self.assertEqual(None, body['floatingip']['port_id'])
+        self.assertIsNone(body['floatingip']['port_id'])
diff --git a/neutron/tests/tempest/api/test_metering_extensions.py b/neutron/tests/tempest/api/test_metering_extensions.py
index 7b03386..9bbcdce 100644
--- a/neutron/tests/tempest/api/test_metering_extensions.py
+++ b/neutron/tests/tempest/api/test_metering_extensions.py
@@ -12,13 +12,13 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+from neutron_lib.db import constants as db_const
 from tempest.lib.common.utils import data_utils
 from tempest import test
 
-from neutron.api.v2 import attributes as attr
 from neutron.tests.tempest.api import base
 
-LONG_NAME_OK = 'x' * (attr.NAME_MAX_LEN)
+LONG_NAME_OK = 'x' * db_const.NAME_FIELD_SIZE
 
 
 class MeteringTestJSON(base.BaseAdminNetworkTest):
diff --git a/neutron/tests/tempest/api/test_metering_negative.py b/neutron/tests/tempest/api/test_metering_negative.py
index 39fdae8..dece9e4 100644
--- a/neutron/tests/tempest/api/test_metering_negative.py
+++ b/neutron/tests/tempest/api/test_metering_negative.py
@@ -12,13 +12,13 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from neutron_lib.db import constants as db_const
 from tempest.lib import exceptions as lib_exc
 from tempest import test
 
-from neutron.api.v2 import attributes as attr
 from neutron.tests.tempest.api import base
 
-LONG_NAME_NG = 'x' * (attr.NAME_MAX_LEN + 1)
+LONG_NAME_NG = 'x' * (db_const.NAME_FIELD_SIZE + 1)
 
 
 class MeteringNegativeTestJSON(base.BaseAdminNetworkTest):
diff --git a/neutron/tests/tempest/api/test_qos.py b/neutron/tests/tempest/api/test_qos.py
index 2f1c75a..395bda0 100644
--- a/neutron/tests/tempest/api/test_qos.py
+++ b/neutron/tests/tempest/api/test_qos.py
@@ -361,6 +361,7 @@
 class QosBandwidthLimitRuleTestJSON(base.BaseAdminNetworkTest):
     @classmethod
     @test.requires_ext(extension="qos", service="network")
+    @base.require_qos_rule_type(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
     def resource_setup(cls):
         super(QosBandwidthLimitRuleTestJSON, cls).resource_setup()
 
@@ -772,6 +773,7 @@
 
     @classmethod
     @test.requires_ext(extension="qos", service="network")
+    @base.require_qos_rule_type(qos_consts.RULE_TYPE_DSCP_MARKING)
     def resource_setup(cls):
         super(QosDscpMarkingRuleTestJSON, cls).resource_setup()
 
@@ -905,6 +907,7 @@
 
     @classmethod
     @test.requires_ext(extension="qos", service="network")
+    @base.require_qos_rule_type(qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH)
     def resource_setup(cls):
         super(QosMinimumBandwidthRuleTestJSON, cls).resource_setup()
 
diff --git a/neutron/tests/tempest/api/test_qos_negative.py b/neutron/tests/tempest/api/test_qos_negative.py
new file mode 100644
index 0000000..5057c72
--- /dev/null
+++ b/neutron/tests/tempest/api/test_qos_negative.py
@@ -0,0 +1,50 @@
+#    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 neutron_lib.db import constants as db_const
+from tempest.lib import exceptions as lib_exc
+from tempest import test
+
+from neutron.tests.tempest.api import base
+
+LONG_NAME_NG = 'z' * (db_const.NAME_FIELD_SIZE + 1)
+LONG_DESCRIPTION_NG = 'z' * (db_const.LONG_DESCRIPTION_FIELD_SIZE + 1)
+LONG_TENANT_ID_NG = 'z' * (db_const.PROJECT_ID_FIELD_SIZE + 1)
+
+
+class QosNegativeTestJSON(base.BaseAdminNetworkTest):
+    @classmethod
+    @test.requires_ext(extension="qos", service="network")
+    def resource_setup(cls):
+        super(QosNegativeTestJSON, cls).resource_setup()
+
+    @test.attr(type='negative')
+    @test.idempotent_id('b9dce555-d3b3-11e5-950a-54ee757c77da')
+    def test_add_policy_with_too_long_name(self):
+        self.assertRaises(lib_exc.BadRequest,
+                          self.client.create_qos_policy,
+                          LONG_NAME_NG, 'test policy desc1', False)
+
+    @test.attr(type='negative')
+    @test.idempotent_id('b9dce444-d3b3-11e5-950a-54ee747c99db')
+    def test_add_policy_with_too_long_description(self):
+        self.assertRaises(lib_exc.BadRequest,
+                          self.client.create_qos_policy,
+                          'test-policy', LONG_DESCRIPTION_NG, False)
+
+    @test.attr(type='negative')
+    @test.idempotent_id('b9dce444-d3b3-11e5-950a-54ee757c77dc')
+    def test_add_policy_with_too_long_tenant_id(self):
+        self.assertRaises(lib_exc.BadRequest,
+                          self.client.create_qos_policy,
+                          'test-policy', 'test policy desc1',
+                          False, LONG_TENANT_ID_NG)
diff --git a/neutron/tests/tempest/api/test_revisions.py b/neutron/tests/tempest/api/test_revisions.py
index 6a2ff88..cdcb367 100644
--- a/neutron/tests/tempest/api/test_revisions.py
+++ b/neutron/tests/tempest/api/test_revisions.py
@@ -261,3 +261,54 @@
                            body['revision_number'])
         # disassociate
         self.client.update_floatingip(b2['floatingip']['id'], port_id=None)
+
+    @test.idempotent_id('afb6486c-41b5-483e-a500-3c506f4deb49')
+    @test.requires_ext(extension="router", service="network")
+    @test.requires_ext(extension="dvr", service="network")
+    def test_update_router_extra_attributes_bumps_revision(self):
+        router = self.create_router(router_name='r1')
+        self.assertIn('revision_number', router)
+        rev1 = router['revision_number']
+        router = self.admin_client.update_router(
+            router['id'], admin_state_up=False)['router']
+        self.assertGreater(router['revision_number'], rev1)
+        self.admin_client.update_router(router['id'],
+                                        distributed=True)['router']
+        updated = self.client.show_router(router['id'])['router']
+        self.assertGreater(updated['revision_number'],
+                           router['revision_number'])
+
+    @test.idempotent_id('90743b00-b0e2-40e4-9524-1c884fe3ef23')
+    @test.requires_ext(extension="external-network", service="network")
+    @test.requires_ext(extension="auto-allocated-topology", service="network")
+    @test.requires_ext(extension="subnet_allocation", service="network")
+    @test.requires_ext(extension="router", service="network")
+    def test_update_external_network_bumps_revision(self):
+        net = self.create_network()
+        self.assertIn('revision_number', net)
+        updated = self.admin_client.update_network(net['id'],
+                                                   **{'router:external': True})
+        self.assertGreater(updated['network']['revision_number'],
+                           net['revision_number'])
+
+    @test.idempotent_id('5af6450a-0f61-49c3-b628-38db77c7b856')
+    @test.requires_ext(extension="qos", service="network")
+    def test_update_qos_port_policy_binding_bumps_revision(self):
+        policy = self.create_qos_policy(name='port-policy', shared=False)
+        port = self.create_port(self.create_network())
+        self.addCleanup(self.client.delete_port, port['id'])
+        updated = self.admin_client.update_port(
+            port['id'], qos_policy_id=policy['id'])
+        self.assertGreater(updated['port']['revision_number'],
+                           port['revision_number'])
+
+    @test.idempotent_id('817da343-c6e4-445c-9519-a621f124dfbe')
+    @test.requires_ext(extension="qos", service="network")
+    def test_update_qos_network_policy_binding_bumps_revision(self):
+        policy = self.create_qos_policy(name='network-policy', shared=False)
+        network = self.create_network()
+        self.addCleanup(self.client.delete_network, network['id'])
+        updated = self.admin_client.update_network(
+            network['id'], qos_policy_id=policy['id'])
+        self.assertGreater(updated['network']['revision_number'],
+                           network['revision_number'])
diff --git a/neutron/tests/tempest/api/test_tag.py b/neutron/tests/tempest/api/test_tag.py
new file mode 100644
index 0000000..5cf6e23
--- /dev/null
+++ b/neutron/tests/tempest/api/test_tag.py
@@ -0,0 +1,174 @@
+#    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 TagTestJSON(base.BaseAdminNetworkTest):
+
+    @classmethod
+    @test.requires_ext(extension="tag", service="network")
+    def resource_setup(cls):
+        super(TagTestJSON, cls).resource_setup()
+        cls.res_id = cls._create_resource()
+
+    def _get_and_compare_tags(self, tags):
+        res_body = self.client.get_tags(self.resource, self.res_id)
+        self.assertItemsEqual(tags, res_body['tags'])
+
+    def _test_tag_operations(self):
+        # create and get tags
+        tags = ['red', 'blue']
+        res_body = self.client.update_tags(self.resource, self.res_id, tags)
+        self.assertItemsEqual(tags, res_body['tags'])
+        self._get_and_compare_tags(tags)
+
+        # add a tag
+        self.client.update_tag(self.resource, self.res_id, 'green')
+        self._get_and_compare_tags(['red', 'blue', 'green'])
+
+        # update tag exist
+        self.client.update_tag(self.resource, self.res_id, 'red')
+        self._get_and_compare_tags(['red', 'blue', 'green'])
+
+        # replace tags
+        tags = ['red', 'yellow', 'purple']
+        res_body = self.client.update_tags(self.resource, self.res_id, tags)
+        self.assertItemsEqual(tags, res_body['tags'])
+        self._get_and_compare_tags(tags)
+
+        # get tag
+        self.client.get_tag(self.resource, self.res_id, 'red')
+
+        # get tag not exist
+        self.assertRaises(lib_exc.NotFound, self.client.get_tag,
+                          self.resource, self.res_id, 'green')
+
+        # delete tag
+        self.client.delete_tag(self.resource, self.res_id, 'red')
+        self._get_and_compare_tags(['yellow', 'purple'])
+
+        # delete tag not exist
+        self.assertRaises(lib_exc.NotFound, self.client.delete_tag,
+                          self.resource, self.res_id, 'green')
+
+        # delete tags
+        self.client.delete_tags(self.resource, self.res_id)
+        self._get_and_compare_tags([])
+
+
+class TagNetworkTestJSON(TagTestJSON):
+    resource = 'networks'
+
+    @classmethod
+    def _create_resource(cls):
+        network = cls.create_network()
+        return network['id']
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('5621062d-fbfb-4437-9d69-138c78ea4188')
+    def test_network_tags(self):
+        self._test_tag_operations()
+
+
+class TagFilterTestJSON(base.BaseAdminNetworkTest):
+    credentials = ['primary', 'alt', 'admin']
+    resource = 'networks'
+
+    @classmethod
+    @test.requires_ext(extension="tag", service="network")
+    def resource_setup(cls):
+        super(TagFilterTestJSON, cls).resource_setup()
+
+        res1_id = cls._create_resource('tag-res1')
+        res2_id = cls._create_resource('tag-res2')
+        res3_id = cls._create_resource('tag-res3')
+        res4_id = cls._create_resource('tag-res4')
+        # tag-res5: a resource without tags
+        cls._create_resource('tag-res5')
+
+        cls.client.update_tags(cls.resource, res1_id, ['red'])
+        cls.client.update_tags(cls.resource, res2_id, ['red', 'blue'])
+        cls.client.update_tags(cls.resource, res3_id,
+                               ['red', 'blue', 'green'])
+        cls.client.update_tags(cls.resource, res4_id, ['green'])
+
+    @classmethod
+    def setup_clients(cls):
+        super(TagFilterTestJSON, cls).setup_clients()
+        cls.client = cls.alt_manager.network_client
+
+    def _assertEqualResources(self, expected, res):
+        actual = [n['name'] for n in res if n['name'].startswith('tag-res')]
+        self.assertEqual(set(expected), set(actual))
+
+    def _test_filter_tags(self):
+        # tags single
+        filters = {'tags': 'red'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res1', 'tag-res2', 'tag-res3'], res)
+
+        # tags multi
+        filters = {'tags': 'red,blue'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res2', 'tag-res3'], res)
+
+        # tags-any single
+        filters = {'tags-any': 'blue'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res2', 'tag-res3'], res)
+
+        # tags-any multi
+        filters = {'tags-any': 'red,blue'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res1', 'tag-res2', 'tag-res3'], res)
+
+        # not-tags single
+        filters = {'not-tags': 'red'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res4', 'tag-res5'], res)
+
+        # not-tags multi
+        filters = {'not-tags': 'red,blue'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res1', 'tag-res4', 'tag-res5'], res)
+
+        # not-tags-any single
+        filters = {'not-tags-any': 'blue'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res1', 'tag-res4', 'tag-res5'], res)
+
+        # not-tags-any multi
+        filters = {'not-tags-any': 'red,blue'}
+        res = self._list_resource(filters)
+        self._assertEqualResources(['tag-res4', 'tag-res5'], res)
+
+
+class TagFilterNetworkTestJSON(TagFilterTestJSON):
+    resource = 'networks'
+
+    @classmethod
+    def _create_resource(cls, name):
+        res = cls.create_network(network_name=name)
+        return res['id']
+
+    def _list_resource(self, filters):
+        res = self.client.list_networks(**filters)
+        return res['networks']
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('a66b5cca-7db2-40f5-a33d-8ac9f864e53e')
+    def test_filter_network_tags(self):
+        self._test_filter_tags()
diff --git a/neutron/tests/tempest/scenario/base.py b/neutron/tests/tempest/scenario/base.py
index 8c45f4b..56221e5 100644
--- a/neutron/tests/tempest/scenario/base.py
+++ b/neutron/tests/tempest/scenario/base.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from oslo_log import log
+
 from tempest.common import waiters
 from tempest.lib.common import ssh
 from tempest.lib.common.utils import data_utils
@@ -23,6 +25,8 @@
 
 CONF = config.CONF
 
+LOG = log.getLogger(__name__)
+
 
 class BaseTempestTestCase(base_api.BaseNetworkTest):
     @classmethod
@@ -47,13 +51,37 @@
 
     @classmethod
     def create_server(cls, flavor_ref, image_ref, key_name, networks,
-                      name=None):
+                      name=None, security_groups=None):
+        """Create a server using tempest lib
+        All the parameters are the ones used in Compute API
+
+        Args:
+           flavor_ref(str): The flavor of the server to be provisioned.
+           image_ref(str):  The image of the server to be provisioned.
+           key_name(str): SSH key to to be used to connect to the
+                            provisioned server.
+           networks(list): List of dictionaries where each represent
+               an interface to be attached to the server. For network
+               it should be {'uuid': network_uuid} and for port it should
+               be {'port': port_uuid}
+           name(str): Name of the server to be provisioned.
+           security_groups(list): List of dictionaries where
+                the keys is 'name' and the value is the name of
+                the security group. If it's not passed the default
+                security group will be used.
+        """
+
         name = name or data_utils.rand_name('server-test')
+        if not security_groups:
+            security_groups = [{'name': 'default'}]
+
         server = cls.manager.servers_client.create_server(
-            name=name, flavorRef=flavor_ref,
+            name=name,
+            flavorRef=flavor_ref,
             imageRef=image_ref,
             key_name=key_name,
-            networks=networks)
+            networks=networks,
+            security_groups=security_groups)
         cls.servers.append(server['server']['id'])
         return server
 
@@ -104,6 +132,7 @@
         router = cls.create_router(
             data_utils.rand_name('router'), admin_state_up=True,
             external_network_id=CONF.network.public_network_id)
+        LOG.debug("Created router %s", router['name'])
         cls.create_router_interface(router['id'], subnet_id)
         cls.routers.append(router)
         return router
@@ -123,17 +152,32 @@
 
     @classmethod
     def setup_network_and_server(cls):
+        """Creating network resources and a server.
+
+        Creating a network, subnet, router, keypair, security group
+        and a server.
+        """
         cls.network = cls.create_network()
+        LOG.debug("Created network %s", cls.network['name'])
         cls.subnet = cls.create_subnet(cls.network)
+        LOG.debug("Created subnet %s", cls.subnet['id'])
+
+        secgroup = cls.manager.network_client.create_security_group(
+            name=data_utils.rand_name('secgroup-'))
+        LOG.debug("Created security group %s",
+                  secgroup['security_group']['name'])
+        cls.security_groups.append(secgroup['security_group'])
 
         cls.create_router_and_interface(cls.subnet['id'])
         cls.keypair = cls.create_keypair()
-        cls.create_loginable_secgroup_rule()
+        cls.create_loginable_secgroup_rule(
+            secgroup_id=secgroup['security_group']['id'])
         cls.server = cls.create_server(
             flavor_ref=CONF.compute.flavor_ref,
             image_ref=CONF.compute.image_ref,
             key_name=cls.keypair['name'],
-            networks=[{'uuid': cls.network['id']}])
+            networks=[{'uuid': cls.network['id']}],
+            security_groups=[{'name': secgroup['security_group']['name']}])
         waiters.wait_for_server_status(cls.manager.servers_client,
                                        cls.server['server']['id'],
                                        constants.SERVER_STATUS_ACTIVE)
diff --git a/neutron/tests/tempest/scenario/test_qos.py b/neutron/tests/tempest/scenario/test_qos.py
index 89b31a7..b558438 100644
--- a/neutron/tests/tempest/scenario/test_qos.py
+++ b/neutron/tests/tempest/scenario/test_qos.py
@@ -22,6 +22,8 @@
 from tempest import test
 
 from neutron.common import utils
+from neutron.services.qos import qos_consts
+from neutron.tests.tempest.api import base as base_api
 from neutron.tests.tempest import config
 from neutron.tests.tempest.scenario import base
 from neutron.tests.tempest.scenario import constants
@@ -79,6 +81,7 @@
 
     @classmethod
     @test.requires_ext(extension="qos", service="network")
+    @base_api.require_qos_rule_type(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
     def resource_setup(cls):
         super(QoSTest, cls).resource_setup()
 
@@ -158,7 +161,9 @@
                      'port_range_min': NC_PORT,
                      'port_range_max': NC_PORT,
                      'remote_ip_prefix': '0.0.0.0/0'}]
-        self.create_secgroup_rules(rulesets)
+        self.create_secgroup_rules(rulesets,
+                                   self.security_groups[-1]['id'])
+
         ssh_client = ssh.Client(self.fip['floating_ip_address'],
                                 CONF.validation.image_ssh_user,
                                 pkey=self.keypair['private_key'])
diff --git a/neutron/tests/tempest/scenario/test_trunk.py b/neutron/tests/tempest/scenario/test_trunk.py
index 30d6022..b350392 100644
--- a/neutron/tests/tempest/scenario/test_trunk.py
+++ b/neutron/tests/tempest/scenario/test_trunk.py
@@ -14,6 +14,7 @@
 
 from oslo_log import log as logging
 from tempest.common import waiters
+from tempest.lib.common.utils import data_utils
 from tempest import test
 
 from neutron.common import utils
@@ -38,17 +39,24 @@
         cls.subnet = cls.create_subnet(cls.network)
         cls.create_router_and_interface(cls.subnet['id'])
         cls.keypair = cls.create_keypair()
-        cls.create_loginable_secgroup_rule()
+        cls.secgroup = cls.manager.network_client.create_security_group(
+            name=data_utils.rand_name('secgroup-'))
+        cls.security_groups.append(cls.secgroup['security_group'])
+        cls.create_loginable_secgroup_rule(
+            secgroup_id=cls.secgroup['security_group']['id'])
 
     def _create_server_with_trunk_port(self):
-        port = self.create_port(self.network)
+        port = self.create_port(self.network, security_groups=[
+            self.secgroup['security_group']['id']])
         trunk = self.client.create_trunk(port['id'], subports=[])['trunk']
         fip = self.create_and_associate_floatingip(port['id'])
         server = self.create_server(
             flavor_ref=CONF.compute.flavor_ref,
             image_ref=CONF.compute.image_ref,
             key_name=self.keypair['name'],
-            networks=[{'port': port['id']}])['server']
+            networks=[{'port': port['id']}],
+            security_groups=[{'name': self.secgroup[
+                'security_group']['name']}])['server']
         self.addCleanup(self._detach_and_delete_trunk, server, trunk)
         return {'port': port, 'trunk': trunk, 'fip': fip,
                 'server': server}
diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py
index 93e31ea..610b049 100644
--- a/neutron/tests/tempest/services/network/json/network_client.py
+++ b/neutron/tests/tempest/services/network/json/network_client.py
@@ -907,3 +907,44 @@
         body = {'extensions': self.deserialize_list(body)}
         self.expected_success(200, resp.status)
         return service_client.ResponseBody(resp, body)
+
+    def get_tags(self, resource_type, resource_id):
+        uri = '%s/%s/%s/tags' % (
+            self.uri_prefix, resource_type, resource_id)
+        resp, body = self.get(uri)
+        self.expected_success(200, resp.status)
+        body = jsonutils.loads(body)
+        return service_client.ResponseBody(resp, body)
+
+    def get_tag(self, resource_type, resource_id, tag):
+        uri = '%s/%s/%s/tags/%s' % (
+            self.uri_prefix, resource_type, resource_id, tag)
+        resp, body = self.get(uri)
+        self.expected_success(204, resp.status)
+
+    def update_tag(self, resource_type, resource_id, tag):
+        uri = '%s/%s/%s/tags/%s' % (
+            self.uri_prefix, resource_type, resource_id, tag)
+        resp, body = self.put(uri, None)
+        self.expected_success(201, resp.status)
+
+    def update_tags(self, resource_type, resource_id, tags):
+        uri = '%s/%s/%s/tags' % (
+            self.uri_prefix, resource_type, resource_id)
+        req_body = jsonutils.dumps({'tags': tags})
+        resp, body = self.put(uri, req_body)
+        self.expected_success(200, resp.status)
+        body = jsonutils.loads(body)
+        return service_client.ResponseBody(resp, body)
+
+    def delete_tags(self, resource_type, resource_id):
+        uri = '%s/%s/%s/tags' % (
+            self.uri_prefix, resource_type, resource_id)
+        resp, body = self.delete(uri)
+        self.expected_success(204, resp.status)
+
+    def delete_tag(self, resource_type, resource_id, tag):
+        uri = '%s/%s/%s/tags/%s' % (
+            self.uri_prefix, resource_type, resource_id, tag)
+        resp, body = self.delete(uri)
+        self.expected_success(204, resp.status)