diff --git a/shade/_tasks.py b/shade/_tasks.py index 35151db42..5f5320ef9 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -42,6 +42,21 @@ class UserGet(task_manager.Task): return client.keystone_client.users.get(**self.args) +class UserAddToGroup(task_manager.Task): + def main(self, client): + return client.keystone_client.users.add_to_group(**self.args) + + +class UserCheckInGroup(task_manager.Task): + def main(self, client): + return client.keystone_client.users.check_in_group(**self.args) + + +class UserRemoveFromGroup(task_manager.Task): + def main(self, client): + return client.keystone_client.users.remove_from_group(**self.args) + + class ProjectList(task_manager.Task): def main(self, client): return client._project_manager.list() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c6dba5cd5..01664f9b7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -554,6 +554,86 @@ class OpenStackCloud(object): self.list_users.invalidate(self) return True + def _get_user_and_group(self, user_name_or_id, group_name_or_id): + user = self.get_user(user_name_or_id) + if not user: + raise OpenStackCloudException( + 'User {user} not found'.format(user=user_name_or_id)) + + group = self.get_group(group_name_or_id) + if not group: + raise OpenStackCloudException( + 'Group {user} not found'.format(user=group_name_or_id)) + + return (user, group) + + def add_user_to_group(self, name_or_id, group_name_or_id): + """Add a user to a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + with _utils.shade_exceptions( + "Error adding user {user} to group {group}".format( + user=name_or_id, group=group_name_or_id) + ): + self.manager.submitTask( + _tasks.UserAddToGroup(user=user['id'], group=group['id']) + ) + + def is_user_in_group(self, name_or_id, group_name_or_id): + """Check to see if a user is in a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :returns: True if user is in the group, False otherwise + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + try: + return self.manager.submitTask( + _tasks.UserCheckInGroup(user=user['id'], group=group['id']) + ) + except keystoneauth1.exceptions.http.NotFound: + # Because the keystone API returns either True or raises an + # exception, which is awesome. + return False + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error adding user {user} to group {group}: {err}".format( + user=name_or_id, group=group_name_or_id, err=str(e)) + ) + + def remove_user_from_group(self, name_or_id, group_name_or_id): + """Remove a user from a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + with _utils.shade_exceptions( + "Error removing user {user} from group {group}".format( + user=name_or_id, group=group_name_or_id) + ): + self.manager.submitTask( + _tasks.UserRemoveFromGroup(user=user['id'], group=group['id']) + ) + @property def glance_client(self): if self._glance_client is None: diff --git a/shade/task_manager.py b/shade/task_manager.py index 258f7fcb2..c96e86617 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -79,9 +79,11 @@ class Task(object): if self._exception: six.reraise(type(self._exception), self._exception, self._traceback) + if type(self._result) == list: return meta.obj_list_to_dict(self._result) - elif not isinstance(self._result, types.GeneratorType): + elif type(self._result) not in (bool, int, float, str, set, + types.GeneratorType): return meta.obj_to_dict(self._result) else: return self._result diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 4b83ec875..2167121c0 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -19,9 +19,6 @@ test_users Functional tests for `shade` user methods. """ -import random -import string - from shade import operator_cloud from shade import OpenStackCloudException from shade.tests import base @@ -31,8 +28,7 @@ class TestUsers(base.TestCase): def setUp(self): super(TestUsers, self).setUp() self.cloud = operator_cloud(cloud='devstack-admin') - self.user_prefix = 'test_user' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + self.user_prefix = self.getUniqueString('user') self.addCleanup(self._cleanup_users) def _cleanup_users(self): @@ -111,3 +107,28 @@ class TestUsers(base.TestCase): self.assertEqual(user_name + '2', new_user['name']) self.assertEqual('somebody@nowhere.com', new_user['email']) self.assertFalse(new_user['enabled']) + + def test_users_and_groups(self): + if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + self.skipTest('Identity service does not support groups') + + group_name = self.getUniqueString('group') + self.addCleanup(self.cloud.delete_group, group_name) + + # Create a group + group = self.cloud.create_group(group_name, 'test group') + self.assertIsNotNone(group) + + # Create a user + user_name = self.user_prefix + '_ug' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, email=user_email) + self.assertIsNotNone(user) + + # Add the user to the group + self.cloud.add_user_to_group(user_name, group_name) + self.assertTrue(self.cloud.is_user_in_group(user_name, group_name)) + + # Remove them from the group + self.cloud.remove_user_from_group(user_name, group_name) + self.assertFalse(self.cloud.is_user_in_group(user_name, group_name)) diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py index 8c9074a85..e45db7282 100644 --- a/shade/tests/unit/test_task_manager.py +++ b/shade/tests/unit/test_task_manager.py @@ -13,10 +13,8 @@ # limitations under the License. -import mock import types -import shade from shade import task_manager from shade.tests.unit import base @@ -35,6 +33,31 @@ class TestTaskGenerator(task_manager.Task): yield 1 +class TestTaskInt(task_manager.Task): + def main(self, client): + return int(1) + + +class TestTaskFloat(task_manager.Task): + def main(self, client): + return float(2.0) + + +class TestTaskStr(task_manager.Task): + def main(self, client): + return "test" + + +class TestTaskBool(task_manager.Task): + def main(self, client): + return True + + +class TestTaskSet(task_manager.Task): + def main(self, client): + return set([1, 2]) + + class TestTaskManager(base.TestCase): def setUp(self): @@ -50,10 +73,26 @@ class TestTaskManager(base.TestCase): """ self.assertRaises(TestException, self.manager.submitTask, TestTask()) - @mock.patch.object(shade.meta, 'obj_to_dict') - @mock.patch.object(shade.meta, 'obj_list_to_dict') - def test_dont_munchify_generators(self, mock_ol2d, mock_o2d): + def test_dont_munchify_generators(self): ret = self.manager.submitTask(TestTaskGenerator()) - self.assertEqual(types.GeneratorType, type(ret)) - self.assertFalse(mock_o2d.called) - self.assertFalse(mock_ol2d.called) + self.assertIsInstance(ret, types.GeneratorType) + + def test_dont_munchify_int(self): + ret = self.manager.submitTask(TestTaskInt()) + self.assertIsInstance(ret, int) + + def test_dont_munchify_float(self): + ret = self.manager.submitTask(TestTaskFloat()) + self.assertIsInstance(ret, float) + + def test_dont_munchify_str(self): + ret = self.manager.submitTask(TestTaskStr()) + self.assertIsInstance(ret, str) + + def test_dont_munchify_bool(self): + ret = self.manager.submitTask(TestTaskBool()) + self.assertIsInstance(ret, bool) + + def test_dont_munchify_set(self): + ret = self.manager.submitTask(TestTaskSet()) + self.assertIsInstance(ret, set) diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py new file mode 100644 index 000000000..ed1ee1428 --- /dev/null +++ b/shade/tests/unit/test_users.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# 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 mock +import munch + +import shade +from shade.tests.unit import base + + +class TestUsers(base.TestCase): + + def setUp(self): + super(TestUsers, self).setUp() + self.cloud = shade.operator_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OperatorCloud, 'get_group') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_add_user_to_group(self, mock_keystone, mock_group, mock_user): + mock_user.return_value = munch.Munch(dict(id=1)) + mock_group.return_value = munch.Munch(dict(id=2)) + self.cloud.add_user_to_group("user", "group") + mock_keystone.users.add_to_group.assert_called_once_with( + user=1, group=2 + ) + + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OperatorCloud, 'get_group') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_is_user_in_group(self, mock_keystone, mock_group, mock_user): + mock_user.return_value = munch.Munch(dict(id=1)) + mock_group.return_value = munch.Munch(dict(id=2)) + mock_keystone.users.check_in_group.return_value = True + self.assertTrue(self.cloud.is_user_in_group("user", "group")) + mock_keystone.users.check_in_group.assert_called_once_with( + user=1, group=2 + ) + + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OperatorCloud, 'get_group') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_remove_user_from_group(self, mock_keystone, mock_group, + mock_user): + mock_user.return_value = munch.Munch(dict(id=1)) + mock_group.return_value = munch.Munch(dict(id=2)) + self.cloud.remove_user_from_group("user", "group") + mock_keystone.users.remove_from_group.assert_called_once_with( + user=1, group=2 + )