Merge "Add identity v3 project tags client"
diff --git a/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
new file mode 100644
index 0000000..dfbcc7d
--- /dev/null
+++ b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ Add ``project_tags_client`` to the identity v3 library. This feature
+ enables the possibility of invoking the following API actions:
+
+ * update_project_tag
+ * list_project_tags
+ * update_all_project_tags
+ * check_project_tag_existence
+ * delete_project_tag
+ * delete_all_project_tags
diff --git a/tempest/api/identity/admin/v3/test_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
new file mode 100644
index 0000000..d05173b
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -0,0 +1,66 @@
+# Copyright 2018 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.
+
+import testtools
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+
+ @decorators.idempotent_id('7c123aac-999d-416a-a0fb-84b915ab10de')
+ @testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
+ 'Project tags not available.')
+ def test_list_update_delete_project_tags(self):
+ project = self.setup_test_project()
+
+ # Create a tag for testing.
+ tag = data_utils.rand_name('tag')
+ # NOTE(felipemonteiro): The response body for create is empty.
+ self.project_tags_client.update_project_tag(project['id'], tag)
+
+ # Verify that the tag was created.
+ self.project_tags_client.check_project_tag_existence(
+ project['id'], tag)
+
+ # Verify that updating the project tags works.
+ tags_to_update = [data_utils.rand_name('tag') for _ in range(3)]
+ updated_tags = self.project_tags_client.update_all_project_tags(
+ project['id'], tags_to_update)['tags']
+ self.assertEqual(sorted(tags_to_update), sorted(updated_tags))
+
+ # Verify that listing project tags works.
+ retrieved_tags = self.project_tags_client.list_project_tags(
+ project['id'])['tags']
+ self.assertEqual(sorted(tags_to_update), sorted(retrieved_tags))
+
+ # Verify that deleting a project tag works.
+ self.project_tags_client.delete_project_tag(
+ project['id'], tags_to_update[0])
+ self.assertRaises(lib_exc.NotFound,
+ self.project_tags_client.check_project_tag_existence,
+ project['id'], tags_to_update[0])
+
+ # Verify that deleting all project tags works.
+ self.project_tags_client.delete_all_project_tags(project['id'])
+ retrieved_tags = self.project_tags_client.list_project_tags(
+ project['id'])['tags']
+ self.assertEmpty(retrieved_tags)
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 9edccbb..6edb8f3 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -228,6 +228,7 @@
cls.domain_config_client = cls.os_admin.domain_config_client
cls.endpoint_filter_client = cls.os_admin.endpoint_filter_client
cls.endpoint_groups_client = cls.os_admin.endpoint_groups_client
+ cls.project_tags_client = cls.os_admin.project_tags_client
if CONF.identity.admin_domain_scope:
# NOTE(andreaf) When keystone policy requires it, the identity
diff --git a/tempest/clients.py b/tempest/clients.py
index b06eafb..d75a712 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -197,6 +197,8 @@
self.endpoint_groups_client = self.identity_v3.EndPointGroupsClient(
**params_v3)
self.catalog_client = self.identity_v3.CatalogClient(**params_v3)
+ self.project_tags_client = self.identity_v3.ProjectTagsClient(
+ **params_v3)
# Token clients do not use the catalog. They only need default_params.
# They read auth_url, so they should only be set if the corresponding
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index a539d08..f302455 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -33,6 +33,8 @@
from tempest.lib.services.identity.v3.oauth_token_client import \
OAUTHTokenClient
from tempest.lib.services.identity.v3.policies_client import PoliciesClient
+from tempest.lib.services.identity.v3.project_tags_client import \
+ ProjectTagsClient
from tempest.lib.services.identity.v3.projects_client import ProjectsClient
from tempest.lib.services.identity.v3.regions_client import RegionsClient
from tempest.lib.services.identity.v3.role_assignments_client import \
@@ -49,6 +51,6 @@
'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient',
'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient',
'OAUTHTokenClient', 'PoliciesClient', 'ProjectsClient',
- 'RegionsClient', 'RoleAssignmentsClient', 'RolesClient',
- 'ServicesClient', 'V3TokenClient', 'TrustsClient', 'UsersClient',
- 'VersionsClient']
+ 'ProjectTagsClient', 'RegionsClient', 'RoleAssignmentsClient',
+ 'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient',
+ 'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/project_tags_client.py b/tempest/lib/services/identity/v3/project_tags_client.py
new file mode 100644
index 0000000..dd1a2a5
--- /dev/null
+++ b/tempest/lib/services/identity/v3/project_tags_client.py
@@ -0,0 +1,80 @@
+# Copyright 2018 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
+
+
+class ProjectTagsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def update_project_tag(self, project_id, tag):
+ """Updates the specified tag and adds it to the project's list of tags.
+
+ """
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.put(url, '{}')
+ # NOTE(felipemonteiro): This API endpoint returns 201 AND an empty
+ # response body, which is consistent with the spec:
+ # https://specs.openstack.org/openstack/api-wg/guidelines/tags.html#addressing-individual-tags
+ self.expected_success(201, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_project_tags(self, project_id):
+ """List tags for a project."""
+ url = "projects/%s/tags" % project_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_all_project_tags(self, project_id, tags, **kwargs):
+ """Updates all the tags for a project.
+
+ Any existing tags not specified will be deleted.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/#modify-tag-list-for-a-project
+ """
+ body = {'tags': tags}
+ if kwargs:
+ body.update(kwargs)
+ put_body = json.dumps(body)
+ resp, body = self.put('projects/%s/tags' % project_id, put_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_project_tag_existence(self, project_id, tag):
+ """Check if a project contains a tag."""
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.get(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_project_tag(self, project_id, tag):
+ """Delete a project tag."""
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_all_project_tags(self, project_id):
+ """Delete all tags from a project."""
+ resp, body = self.delete('projects/%s/tags' % project_id)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/tests/lib/services/identity/v3/test_project_tags_client.py b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
new file mode 100644
index 0000000..2d65a29
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
@@ -0,0 +1,104 @@
+# Copyright 2018 AT&T Corporation.
+#
+# 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.identity.v3 import project_tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestProjectTagsClient(base.BaseServiceTest):
+
+ FAKE_PROJECT_ID = "0c4e939acacf4376bdcd1129f1a054ad"
+
+ FAKE_PROJECT_TAG = "foo"
+
+ FAKE_PROJECT_TAGS = ["foo", "bar"]
+
+ def setUp(self):
+ super(TestProjectTagsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = project_tags_client.ProjectTagsClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_update_project_tag(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_project_tag,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=201)
+
+ def _test_list_project_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"tags": self.FAKE_PROJECT_TAGS},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID)
+
+ def _test_update_all_project_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_all_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {"tags": self.FAKE_PROJECT_TAGS},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID,
+ tags=self.FAKE_PROJECT_TAGS)
+
+ def test_update_project_tag_with_str_body(self):
+ self._test_update_project_tag()
+
+ def test_update_project_tag_with_bytes_body(self):
+ self._test_update_project_tag(bytes_body=True)
+
+ def test_list_project_tags_with_str_body(self):
+ self._test_list_project_tags()
+
+ def test_list_project_tags_with_bytes_body(self):
+ self._test_list_project_tags(bytes_body=True)
+
+ def test_update_all_project_tags_with_str_body(self):
+ self._test_update_all_project_tags()
+
+ def test_update_all_project_tags_with_bytes_body(self):
+ self._test_update_all_project_tags(bytes_body=True)
+
+ def test_check_project_project_tag_existence(self):
+ self.check_service_client_function(
+ self.client.check_project_tag_existence,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=204)
+
+ def test_delete_project_tag(self):
+ self.check_service_client_function(
+ self.client.delete_project_tag,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=204)
+
+ def test_delete_all_project_tags(self):
+ self.check_service_client_function(
+ self.client.delete_all_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ status=204)