From ac3972b0bc44fbda4d3669ae3fbb740969758de0 Mon Sep 17 00:00:00 2001 From: Jason Dillaman Date: Wed, 31 Jul 2013 13:51:17 -0400 Subject: [PATCH] Add a new compute API method for deleting retired services Services and related compute nodes cannot currently be deleted from the command-line. If a node is retired, the service list will continue to show the retired services. A delete service REST method has been added to the compute APIs to support the command-line removal of retired services. Blueprint: remove-nova-compute Change-Id: I655a7f818bb59c8971feb5841feeefafc3a4580a Flags: DocImpact --- .../all_extensions/extensions-get-resp.json | 8 +++ .../all_extensions/extensions-get-resp.xml | 3 + .../services-get-resp.json | 40 +++++++++++ .../services-get-resp.xml | 6 ++ .../os-services/services-list-get-resp.json | 6 +- .../contrib/extended_services_delete.py | 23 ++++++ .../api/openstack/compute/contrib/services.py | 18 +++++ .../openstack/compute/plugins/v3/services.py | 17 ++++- nova/cells/manager.py | 8 ++- nova/cells/messaging.py | 13 ++++ nova/cells/rpcapi.py | 7 ++ nova/compute/api.py | 4 ++ nova/compute/cells_api.py | 4 ++ .../compute/contrib/test_services.py | 72 +++++++++++++++++++ .../compute/plugins/v3/test_services.py | 30 ++++++++ nova/tests/cells/test_cells_manager.py | 12 ++++ nova/tests/cells/test_cells_messaging.py | 12 ++++ nova/tests/cells/test_cells_rpcapi.py | 10 +++ nova/tests/compute/test_host_api.py | 25 +++++++ .../extensions-get-resp.json.tpl | 8 +++ .../extensions-get-resp.xml.tpl | 3 + .../services-get-resp.json.tpl | 40 +++++++++++ .../services-get-resp.xml.tpl | 7 ++ nova/tests/integrated/test_api_samples.py | 50 ++++++++++++- .../services-list-get-resp.json.tpl | 4 ++ nova/tests/integrated/v3/test_services.py | 6 ++ 26 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 doc/api_samples/os-extended-services-delete/services-get-resp.json create mode 100644 doc/api_samples/os-extended-services-delete/services-get-resp.xml create mode 100644 nova/api/openstack/compute/contrib/extended_services_delete.py create mode 100644 nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.xml.tpl diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index fa780a5562c5..b688cb9d3000 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -623,6 +623,14 @@ "name": "Volumes", "namespace": "http://docs.openstack.org/compute/ext/volumes/api/v1.1", "updated": "2011-03-25T00:00:00+00:00" + }, + { + "alias": "os-extended-services-delete", + "description": "Services deletion support.", + "links": [], + "name": "ExtendedServicesDelete", + "namespace": "http://docs.openstack.org/compute/ext/extended_services_delete/api/v2", + "updated": "2013-12-10T00:00:00+00:00" } ] } diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index d12e74d80a56..80337434dc29 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -254,4 +254,7 @@ Volumes support. + + Services deletion support. + diff --git a/doc/api_samples/os-extended-services-delete/services-get-resp.json b/doc/api_samples/os-extended-services-delete/services-get-resp.json new file mode 100644 index 000000000000..fefb0474c294 --- /dev/null +++ b/doc/api_samples/os-extended-services-delete/services-get-resp.json @@ -0,0 +1,40 @@ +{ + "services": [ + { + "id": 1, + "binary": "nova-scheduler", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:02.000000", + "zone": "internal" + }, + { + "id": 2, + "binary": "nova-compute", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:05.000000", + "zone": "nova" + }, + { + "id": 3, + "binary": "nova-scheduler", + "host": "host2", + "state": "down", + "status": "enabled", + "updated_at": "2012-09-19T06:55:34.000000", + "zone": "internal" + }, + { + "id": 4, + "binary": "nova-compute", + "host": "host2", + "state": "down", + "status": "disabled", + "updated_at": "2012-09-18T08:03:38.000000", + "zone": "nova" + } + ] +} diff --git a/doc/api_samples/os-extended-services-delete/services-get-resp.xml b/doc/api_samples/os-extended-services-delete/services-get-resp.xml new file mode 100644 index 000000000000..bb5b46770ac5 --- /dev/null +++ b/doc/api_samples/os-extended-services-delete/services-get-resp.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/doc/v3/api_samples/os-services/services-list-get-resp.json b/doc/v3/api_samples/os-services/services-list-get-resp.json index 40b3bf2521af..1b2c5bf3438b 100644 --- a/doc/v3/api_samples/os-services/services-list-get-resp.json +++ b/doc/v3/api_samples/os-services/services-list-get-resp.json @@ -1,6 +1,7 @@ { "services": [ { + "id": 1, "binary": "nova-scheduler", "disabled_reason": "test1", "host": "host1", @@ -10,6 +11,7 @@ "zone": "internal" }, { + "id": 2, "binary": "nova-compute", "disabled_reason": "test2", "host": "host1", @@ -19,6 +21,7 @@ "zone": "nova" }, { + "id": 3, "binary": "nova-scheduler", "disabled_reason": "", "host": "host2", @@ -28,6 +31,7 @@ "zone": "internal" }, { + "id": 4, "binary": "nova-compute", "disabled_reason": "test4", "host": "host2", @@ -37,4 +41,4 @@ "zone": "nova" } ] -} \ No newline at end of file +} diff --git a/nova/api/openstack/compute/contrib/extended_services_delete.py b/nova/api/openstack/compute/contrib/extended_services_delete.py new file mode 100644 index 000000000000..a7b3bd38230a --- /dev/null +++ b/nova/api/openstack/compute/contrib/extended_services_delete.py @@ -0,0 +1,23 @@ +# 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 nova.api.openstack import extensions + + +class Extended_services_delete(extensions.ExtensionDescriptor): + """Extended services deletion support.""" + + name = "ExtendedServicesDelete" + alias = "os-extended-services-delete" + namespace = ("http://docs.openstack.org/compute/ext/" + "extended_services_delete/api/v2") + updated = "2013-12-10T00:00:00" diff --git a/nova/api/openstack/compute/contrib/services.py b/nova/api/openstack/compute/contrib/services.py index 373582763a41..601255ec7b14 100644 --- a/nova/api/openstack/compute/contrib/services.py +++ b/nova/api/openstack/compute/contrib/services.py @@ -33,6 +33,7 @@ class ServicesIndexTemplate(xmlutil.TemplateBuilder): def construct(self): root = xmlutil.TemplateElement('services') elem = xmlutil.SubTemplateElement(root, 'service', selector='services') + elem.set('id') elem.set('binary') elem.set('host') elem.set('zone') @@ -106,6 +107,8 @@ class ServiceController(object): 'zone': svc['availability_zone'], 'status': active, 'state': state, 'updated_at': svc['updated_at']} + if self.ext_mgr.is_loaded('os-extended-services-delete'): + service_detail['id'] = svc['id'] if detailed: service_detail['disabled_reason'] = svc['disabled_reason'] @@ -128,6 +131,21 @@ class ServiceController(object): return True + @wsgi.response(204) + def delete(self, req, id): + """Deletes the specified service.""" + if not self.ext_mgr.is_loaded('os-extended-services-delete'): + raise webob.exc.HTTPMethodNotAllowed() + + context = req.environ['nova.context'] + authorize(context) + + try: + self.host_api.service_delete(context, id) + except exception.ServiceNotFound: + explanation = _("Service %s not found.") % id + raise webob.exc.HTTPNotFound(explanation=explanation) + @wsgi.serializers(xml=ServicesIndexTemplate) def index(self, req): """ diff --git a/nova/api/openstack/compute/plugins/v3/services.py b/nova/api/openstack/compute/plugins/v3/services.py index 6c4ebf65f5f7..54456614d83b 100644 --- a/nova/api/openstack/compute/plugins/v3/services.py +++ b/nova/api/openstack/compute/plugins/v3/services.py @@ -16,6 +16,7 @@ from oslo.config import cfg import webob.exc from nova.api.openstack import extensions +from nova.api.openstack import wsgi from nova import compute from nova import exception from nova.openstack.common.gettextutils import _ @@ -28,7 +29,7 @@ CONF = cfg.CONF CONF.import_opt('service_down_time', 'nova.service') -class ServiceController(object): +class ServiceController(wsgi.Controller): def __init__(self): self.host_api = compute.HostAPI() @@ -60,6 +61,7 @@ class ServiceController(object): if svc['disabled']: active = 'disabled' service_detail = {'binary': svc['binary'], 'host': svc['host'], + 'id': svc['id'], 'zone': svc['availability_zone'], 'status': active, 'state': state, 'updated_at': svc['updated_at'], @@ -84,6 +86,19 @@ class ServiceController(object): return True + @wsgi.response(204) + @extensions.expected_errors((400, 404)) + def delete(self, req, id): + """Deletes the specified service.""" + context = req.environ['nova.context'] + authorize(context) + + try: + self.host_api.service_delete(context, id) + except exception.ServiceNotFound: + explanation = _("Service %s not found.") % id + raise webob.exc.HTTPNotFound(explanation=explanation) + @extensions.expected_errors(()) def index(self, req): """ diff --git a/nova/cells/manager.py b/nova/cells/manager.py index 7e7cd7fde389..b35be14c6647 100644 --- a/nova/cells/manager.py +++ b/nova/cells/manager.py @@ -71,7 +71,7 @@ class CellsManager(manager.Manager): Scheduling requests get passed to the scheduler class. """ - target = oslo_messaging.Target(version='1.25') + target = oslo_messaging.Target(version='1.26') def __init__(self, *args, **kwargs): LOG.warn(_('The cells feature of Nova is considered experimental ' @@ -303,6 +303,12 @@ class CellsManager(manager.Manager): cells_utils.add_cell_to_service(service, response.cell_name) return service + def service_delete(self, ctxt, cell_service_id): + """Deletes the specified service.""" + cell_name, service_id = cells_utils.split_cell_and_item( + cell_service_id) + self.msg_runner.service_delete(ctxt, cell_name, service_id) + def proxy_rpc_to_manager(self, ctxt, topic, rpc_message, call, timeout): """Proxy an RPC message as-is to a manager.""" compute_topic = CONF.compute_topic diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py index 4db07c465bfa..377cae6c5084 100644 --- a/nova/cells/messaging.py +++ b/nova/cells/messaging.py @@ -757,6 +757,10 @@ class _TargetedMessageMethods(_BaseMessageMethods): self.host_api.service_update(message.ctxt, host_name, binary, params_to_update)) + def service_delete(self, message, service_id): + """Deletes the specified service.""" + self.host_api.service_delete(message.ctxt, service_id) + def proxy_rpc_to_manager(self, message, host_name, rpc_message, topic, timeout): """Proxy RPC to the given compute topic.""" @@ -1535,6 +1539,15 @@ class MessageRunner(object): need_response=True) return message.process() + def service_delete(self, ctxt, cell_name, service_id): + """Deletes the specified service.""" + method_kwargs = {'service_id': service_id} + message = _TargetedMessage(self, ctxt, + 'service_delete', + method_kwargs, 'down', cell_name, + need_response=True) + message.process() + def proxy_rpc_to_manager(self, ctxt, cell_name, host_name, topic, rpc_message, call, timeout): method_kwargs = {'host_name': host_name, diff --git a/nova/cells/rpcapi.py b/nova/cells/rpcapi.py index 70ef10993c19..cfa3f3a98a86 100644 --- a/nova/cells/rpcapi.py +++ b/nova/cells/rpcapi.py @@ -88,6 +88,7 @@ class CellsAPI(object): handle the version_cap being set to 1.24. 1.25 - Adds rebuild_instance() + 1.26 - Adds service_delete() ''' VERSION_ALIASES = { @@ -258,6 +259,12 @@ class CellsAPI(object): binary=binary, params_to_update=params_to_update) + def service_delete(self, ctxt, cell_service_id): + """Deletes the specified service.""" + cctxt = self.client.prepare(version='1.26') + cctxt.call(ctxt, 'service_delete', + cell_service_id=cell_service_id) + def proxy_rpc_to_manager(self, ctxt, rpc_message, topic, call=False, timeout=None): """Proxy RPC to a compute manager. The host in the topic diff --git a/nova/compute/api.py b/nova/compute/api.py index 83d3db56bf44..b87b9ace8508 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -3152,6 +3152,10 @@ class HostAPI(base.Base): service.save() return service + def service_delete(self, context, service_id): + """Deletes the specified service.""" + service_obj.Service.get_by_id(context, service_id).destroy() + def instance_get_all_by_host(self, context, host_name): """Return all instances on the given host.""" return self.db.instance_get_all_by_host(context, host_name) diff --git a/nova/compute/cells_api.py b/nova/compute/cells_api.py index 0f31c2428688..afa3a5b54d82 100644 --- a/nova/compute/cells_api.py +++ b/nova/compute/cells_api.py @@ -558,6 +558,10 @@ class HostAPI(compute_api.HostAPI): service_obj.Service(), db_service) + def service_delete(self, context, service_id): + """Deletes the specified service.""" + self.cells_rpcapi.service_delete(context, service_id) + def instance_get_all_by_host(self, context, host_name): """Get all instances by host. Host might have a cell prepended to it, so we'll need to strip it out. We don't need to proxy diff --git a/nova/tests/api/openstack/compute/contrib/test_services.py b/nova/tests/api/openstack/compute/contrib/test_services.py index d9b877e34841..9d0006488335 100644 --- a/nova/tests/api/openstack/compute/contrib/test_services.py +++ b/nova/tests/api/openstack/compute/contrib/test_services.py @@ -15,6 +15,8 @@ import calendar import datetime + +import mock import webob.exc from nova.api.openstack.compute.contrib import services @@ -332,6 +334,44 @@ class ServicesTest(test.TestCase): 'disabled_reason': 'test2'}]} self.assertEqual(res_dict, response) + def test_services_detail_with_delete_extension(self): + self.ext_mgr.extensions['os-extended-services-delete'] = True + self.controller = services.ServiceController(self.ext_mgr) + with mock.patch.object(self.controller.host_api, 'service_get_all', + side_effect=fake_host_api_service_get_all): + req = FakeRequest() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'id': 1, + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, + {'binary': 'nova-compute', + 'host': 'host1', + 'id': 2, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}, + {'binary': 'nova-scheduler', + 'host': 'host2', + 'id': 3, + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)}, + {'binary': 'nova-compute', + 'host': 'host2', + 'id': 4, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} + self.assertEqual(res_dict, response) + def test_services_enable(self): def _service_update(context, service_id, values): self.assertIsNone(values['disabled_reason']) @@ -396,3 +436,35 @@ class ServicesTest(test.TestCase): self.assertFalse(self.controller._is_valid_as_reason(reason)) reason = 'it\'s a valid reason.' self.assertTrue(self.controller._is_valid_as_reason(reason)) + + def test_services_delete(self): + self.ext_mgr.extensions['os-extended-services-delete'] = True + self.controller = services.ServiceController(self.ext_mgr) + + request = fakes.HTTPRequest.blank('/v2/fakes/os-services/1', + use_admin_context=True) + request.method = 'DELETE' + + with mock.patch.object(self.controller.host_api, + 'service_delete') as service_delete: + self.controller.delete(request, '1') + service_delete.assert_called_once_with( + request.environ['nova.context'], '1') + self.assertEqual(self.controller.delete.wsgi_code, 204) + + def test_services_delete_not_found(self): + self.ext_mgr.extensions['os-extended-services-delete'] = True + self.controller = services.ServiceController(self.ext_mgr) + + request = fakes.HTTPRequest.blank('/v2/fakes/os-services/abc', + use_admin_context=True) + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, request, 'abc') + + def test_services_delete_not_enabled(self): + request = fakes.HTTPRequest.blank('/v2/fakes/os-services/300', + use_admin_context=True) + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPMethodNotAllowed, + self.controller.delete, request, '300') diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_services.py b/nova/tests/api/openstack/compute/plugins/v3/test_services.py index ce80e62b13d7..966a6e84b13b 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/test_services.py +++ b/nova/tests/api/openstack/compute/plugins/v3/test_services.py @@ -14,6 +14,8 @@ import calendar import datetime + +import mock import webob.exc from nova.api.openstack.compute.plugins.v3 import services @@ -150,6 +152,7 @@ class ServicesTest(test.TestCase): res_dict = self.controller.index(req) response = {'services': [ {'binary': 'nova-scheduler', + 'id': 1, 'host': 'host1', 'zone': 'internal', 'status': 'disabled', @@ -158,6 +161,7 @@ class ServicesTest(test.TestCase): 'disabled_reason': 'test1'}, {'binary': 'nova-compute', 'host': 'host1', + 'id': 2, 'zone': 'nova', 'status': 'disabled', 'state': 'up', @@ -165,6 +169,7 @@ class ServicesTest(test.TestCase): 'disabled_reason': 'test2'}, {'binary': 'nova-scheduler', 'host': 'host2', + 'id': 3, 'zone': 'internal', 'status': 'enabled', 'state': 'down', @@ -172,6 +177,7 @@ class ServicesTest(test.TestCase): 'disabled_reason': ''}, {'binary': 'nova-compute', 'host': 'host2', + 'id': 4, 'zone': 'nova', 'status': 'disabled', 'state': 'down', @@ -187,6 +193,7 @@ class ServicesTest(test.TestCase): response = {'services': [ {'binary': 'nova-scheduler', 'host': 'host1', + 'id': 1, 'zone': 'internal', 'status': 'disabled', 'state': 'up', @@ -194,6 +201,7 @@ class ServicesTest(test.TestCase): 'disabled_reason': 'test1'}, {'binary': 'nova-compute', 'host': 'host1', + 'id': 2, 'zone': 'nova', 'status': 'disabled', 'state': 'up', @@ -209,6 +217,7 @@ class ServicesTest(test.TestCase): response = {'services': [ {'binary': 'nova-compute', 'host': 'host1', + 'id': 2, 'zone': 'nova', 'status': 'disabled', 'state': 'up', @@ -216,6 +225,7 @@ class ServicesTest(test.TestCase): 'disabled_reason': 'test2'}, {'binary': 'nova-compute', 'host': 'host2', + 'id': 4, 'zone': 'nova', 'status': 'disabled', 'state': 'down', @@ -231,6 +241,7 @@ class ServicesTest(test.TestCase): response = {'services': [ {'binary': 'nova-compute', 'host': 'host1', + 'id': 2, 'zone': 'nova', 'status': 'disabled', 'state': 'up', @@ -299,3 +310,22 @@ class ServicesTest(test.TestCase): self.assertFalse(self.controller._is_valid_as_reason(reason)) reason = 'it\'s a valid reason.' self.assertTrue(self.controller._is_valid_as_reason(reason)) + + def test_services_delete(self): + request = fakes.HTTPRequestV3.blank('/v3/os-services/1', + use_admin_context=True) + request.method = 'DELETE' + + with mock.patch.object(self.controller.host_api, + 'service_delete') as service_delete: + response = self.controller.delete(request, '1') + service_delete.assert_called_once_with( + request.environ['nova.context'], '1') + self.assertEqual(self.controller.delete.wsgi_code, 204) + + def test_services_delete_not_found(self): + request = fakes.HTTPRequestV3.blank('/v3/os-services/abc', + use_admin_context=True) + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, request, 'abc') diff --git a/nova/tests/cells/test_cells_manager.py b/nova/tests/cells/test_cells_manager.py index adf804da1e00..46203e02c1df 100644 --- a/nova/tests/cells/test_cells_manager.py +++ b/nova/tests/cells/test_cells_manager.py @@ -18,6 +18,7 @@ Tests For CellsManager import copy import datetime +import mock from oslo.config import cfg from nova.cells import messaging @@ -341,6 +342,17 @@ class CellsManagerClassTestCase(test.NoDBTestCase): params_to_update=params_to_update) self.assertEqual(expected_response, response) + def test_service_delete(self): + fake_cell = 'fake-cell' + service_id = '1' + cell_service_id = cells_utils.cell_with_item(fake_cell, service_id) + + with mock.patch.object(self.msg_runner, + 'service_delete') as service_delete: + self.cells_manager.service_delete(self.ctxt, cell_service_id) + service_delete.assert_called_once_with( + self.ctxt, fake_cell, service_id) + def test_proxy_rpc_to_manager(self): self.mox.StubOutWithMock(self.msg_runner, 'proxy_rpc_to_manager') diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py index 28d7c14a0beb..b992ce6b6b3e 100644 --- a/nova/tests/cells/test_cells_messaging.py +++ b/nova/tests/cells/test_cells_messaging.py @@ -898,6 +898,18 @@ class CellsTargetedMethodsTestCase(test.TestCase): topic='compute') self.assertEqual(expected_result, result) + def test_service_delete(self): + fake_service = dict(id=42, host='fake_host', binary='nova-compute', + topic='compute') + + ctxt = self.ctxt.elevated() + db.service_create(ctxt, fake_service) + + self.src_msg_runner.service_delete( + ctxt, self.tgt_cell_name, fake_service['id']) + self.assertRaises(exception.ServiceNotFound, + db.service_get, ctxt, fake_service['id']) + def test_proxy_rpc_to_manager_call(self): fake_topic = 'fake-topic' fake_rpc_message = {'method': 'fake_rpc_method', 'args': {}} diff --git a/nova/tests/cells/test_cells_rpcapi.py b/nova/tests/cells/test_cells_rpcapi.py index a1768ea1267d..4c0727b91f4c 100644 --- a/nova/tests/cells/test_cells_rpcapi.py +++ b/nova/tests/cells/test_cells_rpcapi.py @@ -310,6 +310,16 @@ class CellsAPITestCase(test.NoDBTestCase): version='1.7') self.assertEqual(result, 'fake_response') + def test_service_delete(self): + call_info = self._stub_rpc_method('call', None) + cell_service_id = 'cell@id' + result = self.cells_rpcapi.service_delete( + self.fake_context, cell_service_id=cell_service_id) + expected_args = {'cell_service_id': cell_service_id} + self._check_result(call_info, 'service_delete', + expected_args, version='1.26') + self.assertIsNone(result) + def test_proxy_rpc_to_manager(self): call_info = self._stub_rpc_method('call', 'fake_response') result = self.cells_rpcapi.proxy_rpc_to_manager( diff --git a/nova/tests/compute/test_host_api.py b/nova/tests/compute/test_host_api.py index 8ed4eaf003c8..50bfe417cbbb 100644 --- a/nova/tests/compute/test_host_api.py +++ b/nova/tests/compute/test_host_api.py @@ -14,10 +14,15 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib + +import mock + from nova.cells import utils as cells_utils from nova import compute from nova import context from nova import exception +from nova.objects import service as service_obj from nova import test from nova.tests import fake_notifier from nova.tests.objects import test_objects @@ -305,6 +310,18 @@ class ComputeHostAPITestCase(test.TestCase): state='fake-state') self.assertEqual('fake-response', result) + def test_service_delete(self): + with contextlib.nested( + mock.patch.object(service_obj.Service, 'get_by_id', + return_value=service_obj.Service()), + mock.patch.object(service_obj.Service, 'destroy') + ) as ( + get_by_id, destroy + ): + self.host_api.service_delete(self.ctxt, 1) + get_by_id.assert_called_once_with(self.ctxt, 1) + destroy.assert_called_once_with() + class ComputeHostAPICellsTestCase(ComputeHostAPITestCase): def setUp(self): @@ -413,6 +430,14 @@ class ComputeHostAPICellsTestCase(ComputeHostAPITestCase): self.ctxt, host_name, binary, params_to_update) self._compare_obj(result, expected_result) + def test_service_delete(self): + cell_service_id = cells_utils.cell_with_item('cell1', 1) + with mock.patch.object(self.host_api.cells_rpcapi, + 'service_delete') as service_delete: + self.host_api.service_delete(self.ctxt, cell_service_id) + service_delete.assert_called_once_with( + self.ctxt, cell_service_id) + def test_instance_get_all_by_host(self): instances = [dict(id=1, cell_name='cell1', host='host1'), dict(id=2, cell_name='cell2', host='host1'), diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index c3ae0280047d..2c90e2524ce5 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -623,6 +623,14 @@ "name": "PreserveEphemeralOnRebuild", "namespace": "http://docs.openstack.org/compute/ext/preserve_ephemeral_rebuild/api/v2", "updated": "%(timestamp)s" + }, + { + "alias": "os-extended-services-delete", + "description": "%(text)s", + "links": [], + "name": "ExtendedServicesDelete", + "namespace": "http://docs.openstack.org/compute/ext/extended_services_delete/api/v2", + "updated": "%(timestamp)s" } ] } diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index e8715ee8f77d..95504f416315 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -233,4 +233,7 @@ %(text)s + + %(text)s + diff --git a/nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.json.tpl new file mode 100644 index 000000000000..93091e5a3696 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.json.tpl @@ -0,0 +1,40 @@ +{ + "services": [ + { + "id": 1, + "binary": "nova-scheduler", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "%(timestamp)s", + "zone": "internal" + }, + { + "id": 2, + "binary": "nova-compute", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "%(timestamp)s", + "zone": "nova" + }, + { + "id": 3, + "binary": "nova-scheduler", + "host": "host2", + "state": "down", + "status": "enabled", + "updated_at": "%(timestamp)s", + "zone": "internal" + }, + { + "id": 4, + "binary": "nova-compute", + "host": "host2", + "state": "down", + "status": "disabled", + "updated_at": "%(timestamp)s", + "zone": "nova" + } + ] +} diff --git a/nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.xml.tpl new file mode 100644 index 000000000000..1b1a3cf74c33 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-extended-services-delete/services-get-resp.xml.tpl @@ -0,0 +1,7 @@ + + + + + + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index fab6009346d8..4069484b5573 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -23,6 +23,7 @@ import urllib import uuid as uuid_lib from lxml import etree +import mock from oslo.config import cfg from nova.api.metadata import password @@ -1653,8 +1654,8 @@ class ServicesJsonTest(ApiSampleTestBaseV2): super(ServicesJsonTest, self).tearDown() timeutils.clear_time_override() - def fake_load(self, *args): - return True + def fake_load(self, service_name): + return service_name == 'os-extended-services' def test_services_list(self): """Return a list of all agent builds.""" @@ -1738,6 +1739,51 @@ class ExtendedServicesXmlTest(ExtendedServicesJsonTest): ctype = 'xml' +@mock.patch.object(db, 'service_get_all', + side_effect=test_services.fake_db_api_service_get_all) +@mock.patch.object(db, 'service_get_by_args', + side_effect=test_services.fake_service_get_by_host_binary) +class ExtendedServicesDeleteJsonTest(ApiSampleTestBaseV2): + extends_name = ("nova.api.openstack.compute.contrib.services.Services") + extension_name = ("nova.api.openstack.compute.contrib." + "extended_services_delete.Extended_services_delete") + + def setUp(self): + super(ExtendedServicesDeleteJsonTest, self).setUp() + timeutils.set_time_override(test_services.fake_utcnow()) + + def tearDown(self): + super(ExtendedServicesDeleteJsonTest, self).tearDown() + timeutils.clear_time_override() + + def test_service_detail(self, *mocks): + """ + Return a list of all running services with the disable reason + information if that exists. + """ + response = self._do_get('os-services') + self.assertEqual(response.status, 200) + subs = {'id': 1, + 'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up'} + subs.update(self._get_regexes()) + return self._verify_response('services-get-resp', + subs, response, 200) + + def test_service_delete(self, *mocks): + response = self._do_delete('os-services/1') + self.assertEqual(response.status, 204) + self.assertEqual(response.read(), "") + + +class ExtendedServicesDeleteXmlTest(ExtendedServicesDeleteJsonTest): + """This extension is tested in the ExtendedServicesDeleteJsonTest class.""" + ctype = 'xml' + + class SimpleTenantUsageSampleJsonTest(ServersSampleBase): extension_name = ("nova.api.openstack.compute.contrib.simple_tenant_usage." "Simple_tenant_usage") diff --git a/nova/tests/integrated/v3/api_samples/os-services/services-list-get-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-services/services-list-get-resp.json.tpl index 4f921736e498..ee7f4181e3f0 100644 --- a/nova/tests/integrated/v3/api_samples/os-services/services-list-get-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-services/services-list-get-resp.json.tpl @@ -4,6 +4,7 @@ "binary": "nova-scheduler", "disabled_reason": "test1", "host": "host1", + "id": 1, "state": "up", "status": "disabled", "updated_at": "%(timestamp)s", @@ -13,6 +14,7 @@ "binary": "nova-compute", "disabled_reason": "test2", "host": "host1", + "id": 2, "state": "up", "status": "disabled", "updated_at": "%(timestamp)s", @@ -22,6 +24,7 @@ "binary": "nova-scheduler", "disabled_reason": "", "host": "host2", + "id": 3, "state": "down", "status": "enabled", "updated_at": "%(timestamp)s", @@ -31,6 +34,7 @@ "binary": "nova-compute", "disabled_reason": "test4", "host": "host2", + "id": 4, "state": "down", "status": "disabled", "updated_at": "%(timestamp)s", diff --git a/nova/tests/integrated/v3/test_services.py b/nova/tests/integrated/v3/test_services.py index 13a3174e7d81..ec2bcc2f677d 100644 --- a/nova/tests/integrated/v3/test_services.py +++ b/nova/tests/integrated/v3/test_services.py @@ -78,3 +78,9 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV3): 'service-disable-log-put-req', subs) return self._verify_response('service-disable-log-put-resp', subs, response, 200) + + def test_service_delete(self): + """Delete an existing service.""" + response = self._do_delete('os-services/1') + self.assertEqual(response.status, 204) + self.assertEqual(response.read(), "")