From da6929777d2ae49dcebe1bbe730b1661b3153424 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Mon, 27 Apr 2015 00:03:39 +0100 Subject: [PATCH] Add Keystone service resource methods This change adds keystone service resource methods to OperatorClouds. Only Keystone v2 API is currently supported Change-Id: I5d0b44664a6839502d86fed8d68717b086c34a81 --- shade/__init__.py | 100 ++++++++++++++++++ shade/_tasks.py | 15 +++ shade/tests/fakes.py | 8 ++ shade/tests/functional/test_services.py | 108 +++++++++++++++++++ shade/tests/unit/test_services.py | 132 ++++++++++++++++++++++++ 5 files changed, 363 insertions(+) create mode 100644 shade/tests/functional/test_services.py create mode 100644 shade/tests/unit/test_services.py diff --git a/shade/__init__.py b/shade/__init__.py index cd71efd78..5c9b7f8e5 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2724,3 +2724,103 @@ class OperatorCloud(OpenStackCloud): self.log.debug( "Failed to delete instance_info", exc_info=True) raise OpenStackCloudException(str(e)) + + def create_service(self, name, service_type, description=None): + """Create a service. + + :param name: Service name. + :param service_type: Service type. + :param description: Service description (optional). + + :returns: a dict containing the services description, i.e. the + following attributes:: + - id: + - name: + - service_type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + + """ + try: + service = self.manager.submitTask(_tasks.ServiceCreate( + name=name, service_type=service_type, + description=description)) + except Exception as e: + self.log.debug( + "Failed to create service {name}".format(name=name), + exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(service) + + def list_services(self): + """List all Keystone services. + + :returns: a list of dict containing the services description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + try: + services = self.manager.submitTask(_tasks.ServiceList()) + except Exception as e: + self.log.debug("Failed to list services", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(services) + + def search_services(self, name_or_id=None, filters=None): + """Search Keystone services. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'service_type': 'network'}. + + :returns: a list of dict containing the services description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + services = self.list_services() + return _utils._filter_list(services, name_or_id, filters) + + def get_service(self, name_or_id, filters=None): + """Get exactly one Keystone service. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'service_type': 'network'} + + :returns: a dict containing the services description, i.e. the + following attributes:: + - id: + - name: + - service_type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call or if multiple matches are found. + """ + return _utils._get_entity(self.search_services, name_or_id, filters) + + def delete_service(self, name_or_id): + """Delete a Keystone service. + + :param name_or_id: Service name or id. + + :returns: None + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + service = self.get_service(name_or_id=name_or_id) + if service is None: + return + + try: + self.manager.submitTask(_tasks.ServiceDelete(id=service['id'])) + except Exception as e: + self.log.debug( + "Failed to delete service {id}".format(id=service['id']), + exc_info=True) + raise OpenStackCloudException(str(e)) diff --git a/shade/_tasks.py b/shade/_tasks.py index 3dc051c0f..5033fbae3 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -316,3 +316,18 @@ class MachineSetPower(task_manager.Task): class MachineSetProvision(task_manager.Task): def main(self, client): return client.ironic_client.node.set_provision_state(**self.args) + + +class ServiceCreate(task_manager.Task): + def main(self, client): + return client.keystone_client.services.create(**self.args) + + +class ServiceList(task_manager.Task): + def main(self, client): + return client.keystone_client.services.list() + + +class ServiceDelete(task_manager.Task): + def main(self, client): + return client.keystone_client.services.delete(**self.args) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index d7f585b2b..67f38e111 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -51,6 +51,14 @@ class FakeServer(object): self.status = status +class FakeService(object): + def __init__(self, id, name, type, description=''): + self.id = id + self.name = name + self.type = type + self.description = description + + class FakeUser(object): def __init__(self, id, email, name): self.id = id diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py new file mode 100644 index 000000000..57137f9aa --- /dev/null +++ b/shade/tests/functional/test_services.py @@ -0,0 +1,108 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# 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. + +""" +test_services +---------------------------------- + +Functional tests for `shade` service resource. +""" + +import string +import random + +from shade import operator_cloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestServices(base.TestCase): + + service_attributes = ['id', 'name', 'type', 'description'] + + def setUp(self): + super(TestServices, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.operator_cloud = operator_cloud() + + # Generate a random name for services in this test + self.new_service_name = 'test_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + + self.addCleanup(self._cleanup_services) + + def _cleanup_services(self): + exception_list = list() + for s in self.operator_cloud.list_services(): + if s['name'].startswith(self.new_service_name): + try: + self.operator_cloud.delete_service(name_or_id=s['id']) + except Exception as e: + # We were unable to delete a service, let's try with next + exception_list.append(e) + continue + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_service(self): + service = self.operator_cloud.create_service( + name=self.new_service_name + '_create', service_type='test_type', + description='this is a test description') + self.assertIsNotNone(service.get('id')) + + def test_list_services(self): + service = self.operator_cloud.create_service( + name=self.new_service_name + '_list', service_type='test_type') + observed_services = self.operator_cloud.list_services() + self.assertIsInstance(observed_services, list) + found = False + for s in observed_services: + # Test all attributes are returned + if s['id'] == service['id']: + self.assertEqual(self.new_service_name + '_list', + s.get('name')) + self.assertEqual('test_type', s.get('type')) + found = True + self.assertTrue(found, msg='new service not found in service list!') + + def test_delete_service_by_name(self): + # Test delete by name + service = self.operator_cloud.create_service( + name=self.new_service_name + '_delete_by_name', + service_type='test_type') + self.operator_cloud.delete_service(name_or_id=service['name']) + observed_services = self.operator_cloud.list_services() + found = False + for s in observed_services: + if s['id'] == service['id']: + found = True + break + self.failUnlessEqual(False, found, message='service was not deleted!') + + def test_delete_service_by_id(self): + # Test delete by id + service = self.operator_cloud.create_service( + name=self.new_service_name + '_delete_by_id', + service_type='test_type') + self.operator_cloud.delete_service(name_or_id=service['id']) + observed_services = self.operator_cloud.list_services() + found = False + for s in observed_services: + if s['id'] == service['id']: + found = True + self.failUnlessEqual(False, found, message='service was not deleted!') diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py new file mode 100644 index 000000000..1056d6708 --- /dev/null +++ b/shade/tests/unit/test_services.py @@ -0,0 +1,132 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# 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. + +""" +test_cloud_services +---------------------------------- + +Tests Keystone services commands. +""" + +from mock import patch +from shade import OpenStackCloudException +from shade import OperatorCloud +from shade.tests.fakes import FakeService +from shade.tests.unit import base + + +class CloudServices(base.TestCase): + mock_services = [ + {'id': 'id1', 'name': 'service1', 'type': 'type1', + 'description': 'desc1'}, + {'id': 'id2', 'name': 'service2', 'type': 'type2', + 'description': 'desc2'}, + {'id': 'id3', 'name': 'service3', 'type': 'type2', + 'description': 'desc3'}, + {'id': 'id4', 'name': 'service4', 'type': 'type3', + 'description': 'desc4'} + ] + + def setUp(self): + super(CloudServices, self).setUp() + self.client = OperatorCloud("op_cloud", {}) + self.mock_ks_services = [FakeService(**kwa) for kwa in + self.mock_services] + + @patch.object(OperatorCloud, 'keystone_client') + def test_create_service(self, mock_keystone_client): + kwargs = { + 'name': 'a service', + 'service_type': 'network', + 'description': 'This is a test service' + } + + self.client.create_service(**kwargs) + mock_keystone_client.services.create.assert_called_with(**kwargs) + + @patch.object(OperatorCloud, 'keystone_client') + def test_list_services(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + services = self.client.list_services() + mock_keystone_client.services.list.assert_called_with() + + self.assertItemsEqual(self.mock_services, services) + + @patch.object(OperatorCloud, 'keystone_client') + def test_get_service(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + # Search by id + service = self.client.get_service(name_or_id='id4') + # test we are getting exactly 1 element + self.assertEqual(service, self.mock_services[3]) + + # Search by name + service = self.client.get_service(name_or_id='service2') + # test we are getting exactly 1 element + self.assertEqual(service, self.mock_services[1]) + + # Not found + service = self.client.get_service(name_or_id='blah!') + self.assertIs(None, service) + + # Multiple matches + # test we are getting an Exception + self.assertRaises(OpenStackCloudException, self.client.get_service, + name_or_id=None, filters={'type': 'type2'}) + + @patch.object(OperatorCloud, 'keystone_client') + def test_search_services(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + # Search by id + services = self.client.search_services(name_or_id='id4') + # test we are getting exactly 1 element + self.assertEqual(1, len(services)) + self.assertEqual(services, [self.mock_services[3]]) + + # Search by name + services = self.client.search_services(name_or_id='service2') + # test we are getting exactly 1 element + self.assertEqual(1, len(services)) + self.assertEqual(services, [self.mock_services[1]]) + + # Not found + services = self.client.search_services(name_or_id='blah!') + self.assertEqual(0, len(services)) + + # Multiple matches + services = self.client.search_services( + filters={'type': 'type2'}) + # test we are getting exactly 2 elements + self.assertEqual(2, len(services)) + self.assertEqual(services, [self.mock_services[1], + self.mock_services[2]]) + + @patch.object(OperatorCloud, 'keystone_client') + def test_delete_service(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + # Delete by name + self.client.delete_service(name_or_id='service3') + mock_keystone_client.services.delete.assert_called_with(id='id3') + + # Delete by id + self.client.delete_service('id1') + mock_keystone_client.services.delete.assert_called_with(id='id1')