diff --git a/releasenotes/notes/add_update_service-28e590a7a7524053.yaml b/releasenotes/notes/add_update_service-28e590a7a7524053.yaml new file mode 100644 index 000000000..ff3e7befa --- /dev/null +++ b/releasenotes/notes/add_update_service-28e590a7a7524053.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add the ability to update a keystone service information. This feature is + not available on keystone v2.0. The new function, update_service(), allows + the user to update description, name of service, service type, and enabled + status. diff --git a/shade/_tasks.py b/shade/_tasks.py index c50b3fb2b..014cc5c69 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -607,6 +607,11 @@ class ServiceList(task_manager.Task): return client.keystone_client.services.list() +class ServiceUpdate(task_manager.Task): + def main(self, client): + return client.keystone_client.services.update(**self.args) + + class ServiceDelete(task_manager.Task): def main(self, client): return client.keystone_client.services.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 53114a144..31f5d87af 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -747,13 +747,14 @@ class OperatorCloud(openstackcloud.OpenStackCloud): _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) @_utils.valid_kwargs('type', 'service_type', 'description') - def create_service(self, name, **kwargs): + def create_service(self, name, enabled=True, **kwargs): """Create a service. :param name: Service name. :param type: Service type. (type or service_type required.) :param service_type: Service type. (type or service_type required.) :param description: Service description (optional). + :param enabled: Whether the service is enabled (v3 only) :returns: a dict containing the services description, i.e. the following attributes:: @@ -767,17 +768,47 @@ class OperatorCloud(openstackcloud.OpenStackCloud): openstack API call. """ - service_type = kwargs.get('type', kwargs.get('service_type')) - description = kwargs.get('description', None) - with _utils.shade_exceptions("Failed to create service {name}".format( - name=name)): - if self.cloud_config.get_api_version('identity').startswith('2'): - service_kwargs = {'service_type': service_type} - else: - service_kwargs = {'type': service_type} + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) - service = self.manager.submitTask(_tasks.ServiceCreate( - name=name, description=description, **service_kwargs)) + if self.cloud_config.get_api_version('identity').startswith('2'): + kwargs['service_type'] = type_ or service_type + else: + kwargs['type'] = type_ or service_type + kwargs['enabled'] = enabled + + with _utils.shade_exceptions( + "Failed to create service {name}".format(name=name) + ): + service = self.manager.submitTask( + _tasks.ServiceCreate(name=name, **kwargs) + ) + + return _utils.normalize_keystone_services([service])[0] + + @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', + 'description') + def update_service(self, name_or_id, **kwargs): + # NOTE(SamYaple): Service updates are only available on v3 api + if self.cloud_config.get_api_version('identity').startswith('2'): + raise OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Service update requires Identity v3' + ) + + # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts + # both 'type' and 'service_type' with a preference + # towards 'type' + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) + if type_ or service_type: + kwargs['type'] = type_ or service_type + + with _utils.shade_exceptions( + "Error in updating service {service}".format(service=name_or_id) + ): + service = self.manager.submitTask( + _tasks.ServiceUpdate(service=name_or_id, **kwargs) + ) return _utils.normalize_keystone_services([service])[0] diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 48faf10d3..87d050293 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -26,6 +26,7 @@ import random from shade import operator_cloud from shade.exc import OpenStackCloudException +from shade.exc import OpenStackCloudUnavailableFeature from shade.tests import base @@ -65,6 +66,30 @@ class TestServices(base.TestCase): description='this is a test description') self.assertIsNotNone(service.get('id')) + def test_update_service(self): + if self.operator_cloud.cloud_config.get_api_version( + 'identity').startswith('2'): + # NOTE(SamYaple): Update service only works with v3 api + self.assertRaises(OpenStackCloudUnavailableFeature, + self.operator_cloud.update_service, + 'service_id', name='new name') + else: + service = self.operator_cloud.create_service( + name=self.new_service_name + '_create', type='test_type', + description='this is a test description', enabled=True) + new_service = self.operator_cloud.update_service( + service.id, + name=self.new_service_name + '_update', + description='this is an updated description', + enabled=False + ) + self.assertEqual(new_service.name, + self.new_service_name + '_update') + self.assertEqual(new_service.description, + 'this is an updated description') + self.assertFalse(new_service.enabled) + self.assertEqual(service.id, new_service.id) + def test_list_services(self): service = self.operator_cloud.create_service( name=self.new_service_name + '_list', type='test_type') diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index f5fd9d965..0c4dd0087 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -22,7 +22,9 @@ Tests Keystone services commands. from mock import patch import os_client_config from shade import _utils +from shade import meta from shade import OpenStackCloudException +from shade.exc import OpenStackCloudUnavailableFeature from shade import OperatorCloud from shade.tests.fakes import FakeService from shade.tests.unit import base @@ -50,7 +52,10 @@ class CloudServices(base.TestCase): @patch.object(_utils, 'normalize_keystone_services') @patch.object(OperatorCloud, 'keystone_client') - def test_create_service(self, mock_keystone_client, mock_norm): + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_create_service_v2(self, mock_api_version, mock_keystone_client, + mock_norm): + mock_api_version.return_value = '2.0' kwargs = { 'name': 'a service', 'type': 'network', @@ -62,6 +67,55 @@ class CloudServices(base.TestCase): mock_keystone_client.services.create.assert_called_with(**kwargs) self.assertTrue(mock_norm.called) + @patch.object(_utils, 'normalize_keystone_services') + @patch.object(OperatorCloud, 'keystone_client') + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_create_service_v3(self, mock_api_version, mock_keystone_client, + mock_norm): + mock_api_version.return_value = '3' + kwargs = { + 'name': 'a v3 service', + 'type': 'cinderv2', + 'description': 'This is a test service', + 'enabled': False + } + + self.client.create_service(**kwargs) + mock_keystone_client.services.create.assert_called_with(**kwargs) + self.assertTrue(mock_norm.called) + + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_update_service_v2(self, mock_api_version): + mock_api_version.return_value = '2.0' + # NOTE(SamYaple): Update service only works with v3 api + self.assertRaises(OpenStackCloudUnavailableFeature, + self.client.update_service, + 'service_id', name='new name') + + @patch.object(_utils, 'normalize_keystone_services') + @patch.object(OperatorCloud, 'keystone_client') + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_update_service_v3(self, mock_api_version, mock_keystone_client, + mock_norm): + mock_api_version.return_value = '3' + kwargs = { + 'name': 'updated_name', + 'type': 'updated_type', + 'service_type': 'updated_type', + 'description': 'updated_name', + 'enabled': False + } + + service_obj = FakeService(id='id1', **kwargs) + mock_keystone_client.services.update.return_value = service_obj + + self.client.update_service('id1', **kwargs) + del kwargs['service_type'] + mock_keystone_client.services.update.assert_called_once_with( + service='id1', **kwargs + ) + mock_norm.assert_called_once_with([meta.obj_to_dict(service_obj)]) + @patch.object(OperatorCloud, 'keystone_client') def test_list_services(self, mock_keystone_client): mock_keystone_client.services.list.return_value = \