Merge "Add network tags client"
diff --git a/releasenotes/notes/network-tag-client-f4614029af7927f0.yaml b/releasenotes/notes/network-tag-client-f4614029af7927f0.yaml
new file mode 100644
index 0000000..9af57b1
--- /dev/null
+++ b/releasenotes/notes/network-tag-client-f4614029af7927f0.yaml
@@ -0,0 +1,8 @@
+---
+features:
+  - |
+    Define v2.0 ``tags_client`` for the network service as a library
+    interface, allowing other projects to use this module as a stable
+    library without maintenance changes.
+
+    * tags_client(v2.0)
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index 8775495..6bec0d7 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -83,6 +83,7 @@
             cls.os_primary.security_group_rules_client)
         cls.network_versions_client = cls.os_primary.network_versions_client
         cls.service_providers_client = cls.os_primary.service_providers_client
+        cls.tags_client = cls.os_primary.tags_client
 
     @classmethod
     def resource_setup(cls):
diff --git a/tempest/api/network/test_tags.py b/tempest/api/network/test_tags.py
new file mode 100644
index 0000000..1f3a7c4
--- /dev/null
+++ b/tempest/api/network/test_tags.py
@@ -0,0 +1,90 @@
+# Copyright 2017 AT&T Corporation.
+# 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 tempest.api.network import base
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+from tempest import test
+
+
+class TagsTest(base.BaseNetworkTest):
+    """Tests the following operations in the tags API:
+
+        Update all tags.
+        Delete all tags.
+        Check tag existence.
+        Create a tag.
+        List tags.
+        Remove a tag.
+
+    v2.0 of the Neutron API is assumed. The tag extension allows users to set
+    tags on their networks. The extension supports networks only.
+    """
+
+    @classmethod
+    def skip_checks(cls):
+        super(TagsTest, cls).skip_checks()
+        if not test.is_extension_enabled('tag', 'network'):
+            msg = "tag extension not enabled."
+            raise cls.skipException(msg)
+
+    @classmethod
+    def resource_setup(cls):
+        super(TagsTest, cls).resource_setup()
+        cls.network = cls.create_network()
+
+    @decorators.idempotent_id('ee76bfaf-ac94-4d74-9ecc-4bbd4c583cb1')
+    def test_create_list_show_update_delete_tags(self):
+        # Validate that creating a tag on a network resource works.
+        tag_name = data_utils.rand_name(self.__class__.__name__ + '-Tag')
+        self.tags_client.create_tag('networks', self.network['id'], tag_name)
+        self.addCleanup(self.tags_client.delete_all_tags, 'networks',
+                        self.network['id'])
+        self.tags_client.check_tag_existence('networks', self.network['id'],
+                                             tag_name)
+
+        # Validate that listing tags on a network resource works.
+        retrieved_tags = self.tags_client.list_tags(
+            'networks', self.network['id'])['tags']
+        self.assertEqual([tag_name], retrieved_tags)
+
+        # Generate 3 new tag names.
+        replace_tags = [data_utils.rand_name(
+            self.__class__.__name__ + '-Tag') for _ in range(3)]
+
+        # Replace the current tag with the 3 new tags and validate that the
+        # network resource has the 3 new tags.
+        updated_tags = self.tags_client.update_all_tags(
+            'networks', self.network['id'], replace_tags)['tags']
+        self.assertEqual(3, len(updated_tags))
+        self.assertEqual(set(replace_tags), set(updated_tags))
+
+        # Delete the first tag and check that it has been removed.
+        self.tags_client.delete_tag(
+            'networks', self.network['id'], replace_tags[0])
+        self.assertRaises(lib_exc.NotFound,
+                          self.tags_client.check_tag_existence, 'networks',
+                          self.network['id'], replace_tags[0])
+        for i in range(1, 3):
+            self.tags_client.check_tag_existence(
+                'networks', self.network['id'], replace_tags[i])
+
+        # Delete all the remaining tags and check that they have been removed.
+        self.tags_client.delete_all_tags('networks', self.network['id'])
+        for i in range(1, 3):
+            self.assertRaises(lib_exc.NotFound,
+                              self.tags_client.check_tag_existence, 'networks',
+                              self.network['id'], replace_tags[i])
diff --git a/tempest/clients.py b/tempest/clients.py
index 7b6cc19..a941301 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -79,6 +79,7 @@
         self.security_groups_client = self.network.SecurityGroupsClient()
         self.network_versions_client = self.network.NetworkVersionsClient()
         self.service_providers_client = self.network.ServiceProvidersClient()
+        self.tags_client = self.network.TagsClient()
 
     def _set_image_clients(self):
         if CONF.service_available.glance:
diff --git a/tempest/lib/services/network/__init__.py b/tempest/lib/services/network/__init__.py
index 19e5463..419e593 100644
--- a/tempest/lib/services/network/__init__.py
+++ b/tempest/lib/services/network/__init__.py
@@ -31,11 +31,12 @@
     ServiceProvidersClient
 from tempest.lib.services.network.subnetpools_client import SubnetpoolsClient
 from tempest.lib.services.network.subnets_client import SubnetsClient
+from tempest.lib.services.network.tags_client import TagsClient
 from tempest.lib.services.network.versions_client import NetworkVersionsClient
 
 __all__ = ['AgentsClient', 'ExtensionsClient', 'FloatingIPsClient',
            'MeteringLabelRulesClient', 'MeteringLabelsClient',
-           'NetworksClient', 'PortsClient', 'QuotasClient', 'RoutersClient',
-           'SecurityGroupRulesClient', 'SecurityGroupsClient',
-           'ServiceProvidersClient', 'SubnetpoolsClient', 'SubnetsClient',
-           'NetworkVersionsClient']
+           'NetworksClient', 'NetworkVersionsClient', 'PortsClient',
+           'QuotasClient', 'RoutersClient', 'SecurityGroupRulesClient',
+           'SecurityGroupsClient', 'ServiceProvidersClient',
+           'SubnetpoolsClient', 'SubnetsClient', 'TagsClient']
diff --git a/tempest/lib/services/network/tags_client.py b/tempest/lib/services/network/tags_client.py
new file mode 100644
index 0000000..20c2c11
--- /dev/null
+++ b/tempest/lib/services/network/tags_client.py
@@ -0,0 +1,88 @@
+# Copyright 2017 AT&T Corporation.
+# 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_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib.services.network import base
+
+
+class TagsClient(base.BaseNetworkClient):
+
+    def create_tag(self, resource_type, resource_id, tag):
+        """Adds a tag on the resource.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/networking/v2/index.html#add-a-tag
+        """
+        # NOTE(felipemonteiro): Cannot use ``update_resource`` method because
+        # this API requires self.put but returns 201 instead of 200 expected
+        # by ``update_resource``.
+        uri = '%s/%s/%s/tags/%s' % (
+            self.uri_prefix, resource_type, resource_id, tag)
+        resp, _ = self.put(uri, json.dumps({}))
+        self.expected_success(201, resp.status)
+        return rest_client.ResponseBody(resp)
+
+    def check_tag_existence(self, resource_type, resource_id, tag):
+        """Confirm that a given tag is set on the resource.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/networking/v2/index.html#confirm-a-tag
+        """
+        # TODO(felipemonteiro): Use the "check_resource" method in
+        # ``BaseNetworkClient`` once it has been implemented.
+        uri = '%s/%s/%s/tags/%s' % (
+            self.uri_prefix, resource_type, resource_id, tag)
+        resp, _ = self.get(uri)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp)
+
+    def update_all_tags(self, resource_type, resource_id, tags):
+        """Replace all tags on the resource.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/networking/v2/index.html#replace-all-tags
+        """
+        uri = '/%s/%s/tags' % (resource_type, resource_id)
+        put_body = {"tags": tags}
+        return self.update_resource(uri, put_body)
+
+    def delete_tag(self, resource_type, resource_id, tag):
+        """Removes a tag on the resource.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/networking/v2/index.html#remove-a-tag
+        """
+        uri = '/%s/%s/tags/%s' % (resource_type, resource_id, tag)
+        return self.delete_resource(uri)
+
+    def delete_all_tags(self, resource_type, resource_id):
+        """Removes all tags on the resource.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/networking/v2/index.html#remove-all-tags
+        """
+        uri = '/%s/%s/tags' % (resource_type, resource_id)
+        return self.delete_resource(uri)
+
+    def list_tags(self, resource_type, resource_id):
+        """Retrieves the tags for a resource.
+
+        For more information, please refer to the official API reference:
+        http://developer.openstack.org/api-ref/networking/v2/index.html#obtain-tag-list
+        """
+        uri = '/%s/%s/tags' % (resource_type, resource_id)
+        return self.list_resources(uri)
diff --git a/tempest/tests/lib/services/network/test_tags_client.py b/tempest/tests/lib/services/network/test_tags_client.py
new file mode 100644
index 0000000..dbe50a0
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_tags_client.py
@@ -0,0 +1,123 @@
+# Copyright 2017 AT&T Corporation.
+# 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 tempest.lib.services.network import tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestTagsClient(base.BaseServiceTest):
+
+    FAKE_TAGS = {
+        "tags": [
+            "red",
+            "blue"
+        ]
+    }
+
+    FAKE_RESOURCE_TYPE = 'network'
+
+    FAKE_RESOURCE_ID = '7a8f904b-c1ed-4446-a87d-60440c02934b'
+
+    def setUp(self):
+        super(TestTagsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = tags_client.TagsClient(
+            fake_auth, 'network', 'regionOne')
+
+    def _test_update_all_tags(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_all_tags,
+            'tempest.lib.common.rest_client.RestClient.put',
+            self.FAKE_TAGS,
+            bytes_body,
+            resource_type=self.FAKE_RESOURCE_TYPE,
+            resource_id=self.FAKE_RESOURCE_ID,
+            tags=self.FAKE_TAGS)
+
+    def _test_check_tag_existence(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.check_tag_existence,
+            'tempest.lib.common.rest_client.RestClient.get',
+            {},
+            bytes_body,
+            resource_type=self.FAKE_RESOURCE_TYPE,
+            resource_id=self.FAKE_RESOURCE_ID,
+            tag=self.FAKE_TAGS['tags'][0],
+            status=204)
+
+    def _test_create_tag(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_tag,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            resource_type=self.FAKE_RESOURCE_TYPE,
+            resource_id=self.FAKE_RESOURCE_ID,
+            tag=self.FAKE_TAGS['tags'][0],
+            status=201)
+
+    def _test_list_tags(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_tags,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_TAGS,
+            bytes_body,
+            resource_type=self.FAKE_RESOURCE_TYPE,
+            resource_id=self.FAKE_RESOURCE_ID)
+
+    def test_update_all_tags_with_str_body(self):
+        self._test_update_all_tags()
+
+    def test_update_all_tags_with_bytes_body(self):
+        self._test_update_all_tags(bytes_body=True)
+
+    def test_delete_all_tags(self):
+        self.check_service_client_function(
+            self.client.delete_all_tags,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            resource_type=self.FAKE_RESOURCE_TYPE,
+            resource_id=self.FAKE_RESOURCE_ID,
+            status=204)
+
+    def test_check_tag_existence_with_str_body(self):
+        self._test_check_tag_existence()
+
+    def test_check_tag_existence_with_bytes_body(self):
+        self._test_check_tag_existence(bytes_body=True)
+
+    def test_create_tag_with_str_body(self):
+        self._test_create_tag()
+
+    def test_create_tag_with_bytes_body(self):
+        self._test_create_tag(bytes_body=True)
+
+    def test_list_tags_with_str_body(self):
+        self._test_list_tags()
+
+    def test_list_tags_with_bytes_body(self):
+        self._test_list_tags(bytes_body=True)
+
+    def test_delete_tag(self):
+        self.check_service_client_function(
+            self.client.delete_tag,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            resource_type=self.FAKE_RESOURCE_TYPE,
+            resource_id=self.FAKE_RESOURCE_ID,
+            tag=self.FAKE_TAGS['tags'][0],
+            status=204)