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
This commit is contained in:
Jason Dillaman
2013-07-31 13:51:17 -04:00
parent 6b29aca811
commit ac3972b0bc
26 changed files with 431 additions and 5 deletions

View File

@@ -623,6 +623,14 @@
"name": "Volumes", "name": "Volumes",
"namespace": "http://docs.openstack.org/compute/ext/volumes/api/v1.1", "namespace": "http://docs.openstack.org/compute/ext/volumes/api/v1.1",
"updated": "2011-03-25T00:00:00+00:00" "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"
} }
] ]
} }

View File

@@ -254,4 +254,7 @@
<extension alias="os-volumes" updated="2011-03-25T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes"> <extension alias="os-volumes" updated="2011-03-25T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes">
<description>Volumes support.</description> <description>Volumes support.</description>
</extension> </extension>
<extension alias="os-extended-services-delete" updated="2013-12-10T00:00:00" namespace="http://docs.openstack.org/compute/ext/extended_services_delete/api/v2" name="ExtendedServicesDelete">
<description>Services deletion support.</description>
</extension>
</extensions> </extensions>

View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,6 @@
<services>
<service status="disabled" binary="nova-scheduler" zone="internal" state="up" host="host1" updated_at="2012-10-29T13:42:02.000000" id="1"/>
<service status="disabled" binary="nova-compute" zone="nova" state="up" host="host1" updated_at="2012-10-29T13:42:05.000000" id="2"/>
<service status="enabled" binary="nova-scheduler" zone="internal" state="down" host="host2" updated_at="2012-09-19T06:55:34.000000" id="3"/>
<service status="disabled" binary="nova-compute" zone="nova" state="down" host="host2" updated_at="2012-09-18T08:03:38.000000" id="4"/>
</services>

View File

@@ -1,6 +1,7 @@
{ {
"services": [ "services": [
{ {
"id": 1,
"binary": "nova-scheduler", "binary": "nova-scheduler",
"disabled_reason": "test1", "disabled_reason": "test1",
"host": "host1", "host": "host1",
@@ -10,6 +11,7 @@
"zone": "internal" "zone": "internal"
}, },
{ {
"id": 2,
"binary": "nova-compute", "binary": "nova-compute",
"disabled_reason": "test2", "disabled_reason": "test2",
"host": "host1", "host": "host1",
@@ -19,6 +21,7 @@
"zone": "nova" "zone": "nova"
}, },
{ {
"id": 3,
"binary": "nova-scheduler", "binary": "nova-scheduler",
"disabled_reason": "", "disabled_reason": "",
"host": "host2", "host": "host2",
@@ -28,6 +31,7 @@
"zone": "internal" "zone": "internal"
}, },
{ {
"id": 4,
"binary": "nova-compute", "binary": "nova-compute",
"disabled_reason": "test4", "disabled_reason": "test4",
"host": "host2", "host": "host2",
@@ -37,4 +41,4 @@
"zone": "nova" "zone": "nova"
} }
] ]
} }

View File

@@ -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"

View File

@@ -33,6 +33,7 @@ class ServicesIndexTemplate(xmlutil.TemplateBuilder):
def construct(self): def construct(self):
root = xmlutil.TemplateElement('services') root = xmlutil.TemplateElement('services')
elem = xmlutil.SubTemplateElement(root, 'service', selector='services') elem = xmlutil.SubTemplateElement(root, 'service', selector='services')
elem.set('id')
elem.set('binary') elem.set('binary')
elem.set('host') elem.set('host')
elem.set('zone') elem.set('zone')
@@ -106,6 +107,8 @@ class ServiceController(object):
'zone': svc['availability_zone'], 'zone': svc['availability_zone'],
'status': active, 'state': state, 'status': active, 'state': state,
'updated_at': svc['updated_at']} 'updated_at': svc['updated_at']}
if self.ext_mgr.is_loaded('os-extended-services-delete'):
service_detail['id'] = svc['id']
if detailed: if detailed:
service_detail['disabled_reason'] = svc['disabled_reason'] service_detail['disabled_reason'] = svc['disabled_reason']
@@ -128,6 +131,21 @@ class ServiceController(object):
return True 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) @wsgi.serializers(xml=ServicesIndexTemplate)
def index(self, req): def index(self, req):
""" """

View File

@@ -16,6 +16,7 @@ from oslo.config import cfg
import webob.exc import webob.exc
from nova.api.openstack import extensions from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova import compute from nova import compute
from nova import exception from nova import exception
from nova.openstack.common.gettextutils import _ from nova.openstack.common.gettextutils import _
@@ -28,7 +29,7 @@ CONF = cfg.CONF
CONF.import_opt('service_down_time', 'nova.service') CONF.import_opt('service_down_time', 'nova.service')
class ServiceController(object): class ServiceController(wsgi.Controller):
def __init__(self): def __init__(self):
self.host_api = compute.HostAPI() self.host_api = compute.HostAPI()
@@ -60,6 +61,7 @@ class ServiceController(object):
if svc['disabled']: if svc['disabled']:
active = 'disabled' active = 'disabled'
service_detail = {'binary': svc['binary'], 'host': svc['host'], service_detail = {'binary': svc['binary'], 'host': svc['host'],
'id': svc['id'],
'zone': svc['availability_zone'], 'zone': svc['availability_zone'],
'status': active, 'state': state, 'status': active, 'state': state,
'updated_at': svc['updated_at'], 'updated_at': svc['updated_at'],
@@ -84,6 +86,19 @@ class ServiceController(object):
return True 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(()) @extensions.expected_errors(())
def index(self, req): def index(self, req):
""" """

View File

@@ -71,7 +71,7 @@ class CellsManager(manager.Manager):
Scheduling requests get passed to the scheduler class. 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): def __init__(self, *args, **kwargs):
LOG.warn(_('The cells feature of Nova is considered experimental ' 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) cells_utils.add_cell_to_service(service, response.cell_name)
return service 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): def proxy_rpc_to_manager(self, ctxt, topic, rpc_message, call, timeout):
"""Proxy an RPC message as-is to a manager.""" """Proxy an RPC message as-is to a manager."""
compute_topic = CONF.compute_topic compute_topic = CONF.compute_topic

View File

@@ -757,6 +757,10 @@ class _TargetedMessageMethods(_BaseMessageMethods):
self.host_api.service_update(message.ctxt, host_name, binary, self.host_api.service_update(message.ctxt, host_name, binary,
params_to_update)) 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, def proxy_rpc_to_manager(self, message, host_name, rpc_message,
topic, timeout): topic, timeout):
"""Proxy RPC to the given compute topic.""" """Proxy RPC to the given compute topic."""
@@ -1535,6 +1539,15 @@ class MessageRunner(object):
need_response=True) need_response=True)
return message.process() 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, def proxy_rpc_to_manager(self, ctxt, cell_name, host_name, topic,
rpc_message, call, timeout): rpc_message, call, timeout):
method_kwargs = {'host_name': host_name, method_kwargs = {'host_name': host_name,

View File

@@ -88,6 +88,7 @@ class CellsAPI(object):
handle the version_cap being set to 1.24. handle the version_cap being set to 1.24.
1.25 - Adds rebuild_instance() 1.25 - Adds rebuild_instance()
1.26 - Adds service_delete()
''' '''
VERSION_ALIASES = { VERSION_ALIASES = {
@@ -258,6 +259,12 @@ class CellsAPI(object):
binary=binary, binary=binary,
params_to_update=params_to_update) 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, def proxy_rpc_to_manager(self, ctxt, rpc_message, topic, call=False,
timeout=None): timeout=None):
"""Proxy RPC to a compute manager. The host in the topic """Proxy RPC to a compute manager. The host in the topic

View File

@@ -3152,6 +3152,10 @@ class HostAPI(base.Base):
service.save() service.save()
return service 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): def instance_get_all_by_host(self, context, host_name):
"""Return all instances on the given host.""" """Return all instances on the given host."""
return self.db.instance_get_all_by_host(context, host_name) return self.db.instance_get_all_by_host(context, host_name)

View File

@@ -558,6 +558,10 @@ class HostAPI(compute_api.HostAPI):
service_obj.Service(), service_obj.Service(),
db_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): def instance_get_all_by_host(self, context, host_name):
"""Get all instances by host. Host might have a cell prepended """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 to it, so we'll need to strip it out. We don't need to proxy

View File

@@ -15,6 +15,8 @@
import calendar import calendar
import datetime import datetime
import mock
import webob.exc import webob.exc
from nova.api.openstack.compute.contrib import services from nova.api.openstack.compute.contrib import services
@@ -332,6 +334,44 @@ class ServicesTest(test.TestCase):
'disabled_reason': 'test2'}]} 'disabled_reason': 'test2'}]}
self.assertEqual(res_dict, response) 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 test_services_enable(self):
def _service_update(context, service_id, values): def _service_update(context, service_id, values):
self.assertIsNone(values['disabled_reason']) self.assertIsNone(values['disabled_reason'])
@@ -396,3 +436,35 @@ class ServicesTest(test.TestCase):
self.assertFalse(self.controller._is_valid_as_reason(reason)) self.assertFalse(self.controller._is_valid_as_reason(reason))
reason = 'it\'s a valid reason.' reason = 'it\'s a valid reason.'
self.assertTrue(self.controller._is_valid_as_reason(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')

View File

@@ -14,6 +14,8 @@
import calendar import calendar
import datetime import datetime
import mock
import webob.exc import webob.exc
from nova.api.openstack.compute.plugins.v3 import services from nova.api.openstack.compute.plugins.v3 import services
@@ -150,6 +152,7 @@ class ServicesTest(test.TestCase):
res_dict = self.controller.index(req) res_dict = self.controller.index(req)
response = {'services': [ response = {'services': [
{'binary': 'nova-scheduler', {'binary': 'nova-scheduler',
'id': 1,
'host': 'host1', 'host': 'host1',
'zone': 'internal', 'zone': 'internal',
'status': 'disabled', 'status': 'disabled',
@@ -158,6 +161,7 @@ class ServicesTest(test.TestCase):
'disabled_reason': 'test1'}, 'disabled_reason': 'test1'},
{'binary': 'nova-compute', {'binary': 'nova-compute',
'host': 'host1', 'host': 'host1',
'id': 2,
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'status': 'disabled',
'state': 'up', 'state': 'up',
@@ -165,6 +169,7 @@ class ServicesTest(test.TestCase):
'disabled_reason': 'test2'}, 'disabled_reason': 'test2'},
{'binary': 'nova-scheduler', {'binary': 'nova-scheduler',
'host': 'host2', 'host': 'host2',
'id': 3,
'zone': 'internal', 'zone': 'internal',
'status': 'enabled', 'status': 'enabled',
'state': 'down', 'state': 'down',
@@ -172,6 +177,7 @@ class ServicesTest(test.TestCase):
'disabled_reason': ''}, 'disabled_reason': ''},
{'binary': 'nova-compute', {'binary': 'nova-compute',
'host': 'host2', 'host': 'host2',
'id': 4,
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'status': 'disabled',
'state': 'down', 'state': 'down',
@@ -187,6 +193,7 @@ class ServicesTest(test.TestCase):
response = {'services': [ response = {'services': [
{'binary': 'nova-scheduler', {'binary': 'nova-scheduler',
'host': 'host1', 'host': 'host1',
'id': 1,
'zone': 'internal', 'zone': 'internal',
'status': 'disabled', 'status': 'disabled',
'state': 'up', 'state': 'up',
@@ -194,6 +201,7 @@ class ServicesTest(test.TestCase):
'disabled_reason': 'test1'}, 'disabled_reason': 'test1'},
{'binary': 'nova-compute', {'binary': 'nova-compute',
'host': 'host1', 'host': 'host1',
'id': 2,
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'status': 'disabled',
'state': 'up', 'state': 'up',
@@ -209,6 +217,7 @@ class ServicesTest(test.TestCase):
response = {'services': [ response = {'services': [
{'binary': 'nova-compute', {'binary': 'nova-compute',
'host': 'host1', 'host': 'host1',
'id': 2,
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'status': 'disabled',
'state': 'up', 'state': 'up',
@@ -216,6 +225,7 @@ class ServicesTest(test.TestCase):
'disabled_reason': 'test2'}, 'disabled_reason': 'test2'},
{'binary': 'nova-compute', {'binary': 'nova-compute',
'host': 'host2', 'host': 'host2',
'id': 4,
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'status': 'disabled',
'state': 'down', 'state': 'down',
@@ -231,6 +241,7 @@ class ServicesTest(test.TestCase):
response = {'services': [ response = {'services': [
{'binary': 'nova-compute', {'binary': 'nova-compute',
'host': 'host1', 'host': 'host1',
'id': 2,
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'status': 'disabled',
'state': 'up', 'state': 'up',
@@ -299,3 +310,22 @@ class ServicesTest(test.TestCase):
self.assertFalse(self.controller._is_valid_as_reason(reason)) self.assertFalse(self.controller._is_valid_as_reason(reason))
reason = 'it\'s a valid reason.' reason = 'it\'s a valid reason.'
self.assertTrue(self.controller._is_valid_as_reason(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')

View File

@@ -18,6 +18,7 @@ Tests For CellsManager
import copy import copy
import datetime import datetime
import mock
from oslo.config import cfg from oslo.config import cfg
from nova.cells import messaging from nova.cells import messaging
@@ -341,6 +342,17 @@ class CellsManagerClassTestCase(test.NoDBTestCase):
params_to_update=params_to_update) params_to_update=params_to_update)
self.assertEqual(expected_response, response) 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): def test_proxy_rpc_to_manager(self):
self.mox.StubOutWithMock(self.msg_runner, self.mox.StubOutWithMock(self.msg_runner,
'proxy_rpc_to_manager') 'proxy_rpc_to_manager')

View File

@@ -898,6 +898,18 @@ class CellsTargetedMethodsTestCase(test.TestCase):
topic='compute') topic='compute')
self.assertEqual(expected_result, result) 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): def test_proxy_rpc_to_manager_call(self):
fake_topic = 'fake-topic' fake_topic = 'fake-topic'
fake_rpc_message = {'method': 'fake_rpc_method', 'args': {}} fake_rpc_message = {'method': 'fake_rpc_method', 'args': {}}

View File

@@ -310,6 +310,16 @@ class CellsAPITestCase(test.NoDBTestCase):
version='1.7') version='1.7')
self.assertEqual(result, 'fake_response') 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): def test_proxy_rpc_to_manager(self):
call_info = self._stub_rpc_method('call', 'fake_response') call_info = self._stub_rpc_method('call', 'fake_response')
result = self.cells_rpcapi.proxy_rpc_to_manager( result = self.cells_rpcapi.proxy_rpc_to_manager(

View File

@@ -14,10 +14,15 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import contextlib
import mock
from nova.cells import utils as cells_utils from nova.cells import utils as cells_utils
from nova import compute from nova import compute
from nova import context from nova import context
from nova import exception from nova import exception
from nova.objects import service as service_obj
from nova import test from nova import test
from nova.tests import fake_notifier from nova.tests import fake_notifier
from nova.tests.objects import test_objects from nova.tests.objects import test_objects
@@ -305,6 +310,18 @@ class ComputeHostAPITestCase(test.TestCase):
state='fake-state') state='fake-state')
self.assertEqual('fake-response', result) 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): class ComputeHostAPICellsTestCase(ComputeHostAPITestCase):
def setUp(self): def setUp(self):
@@ -413,6 +430,14 @@ class ComputeHostAPICellsTestCase(ComputeHostAPITestCase):
self.ctxt, host_name, binary, params_to_update) self.ctxt, host_name, binary, params_to_update)
self._compare_obj(result, expected_result) 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): def test_instance_get_all_by_host(self):
instances = [dict(id=1, cell_name='cell1', host='host1'), instances = [dict(id=1, cell_name='cell1', host='host1'),
dict(id=2, cell_name='cell2', host='host1'), dict(id=2, cell_name='cell2', host='host1'),

View File

@@ -623,6 +623,14 @@
"name": "PreserveEphemeralOnRebuild", "name": "PreserveEphemeralOnRebuild",
"namespace": "http://docs.openstack.org/compute/ext/preserve_ephemeral_rebuild/api/v2", "namespace": "http://docs.openstack.org/compute/ext/preserve_ephemeral_rebuild/api/v2",
"updated": "%(timestamp)s" "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"
} }
] ]
} }

View File

@@ -233,4 +233,7 @@
<extension alias="os-preserve-ephemeral-rebuild" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/preserve_ephemeral_rebuild/api/v2" name="PreserveEphemeralOnRebuild"> <extension alias="os-preserve-ephemeral-rebuild" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/preserve_ephemeral_rebuild/api/v2" name="PreserveEphemeralOnRebuild">
<description>%(text)s</description> <description>%(text)s</description>
</extension> </extension>
<extension alias="os-extended-services-delete" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/extended_services_delete/api/v2" name="ExtendedServicesDelete">
<description>%(text)s</description>
</extension>
</extensions> </extensions>

View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<services>
<service status="disabled" binary="nova-scheduler" zone="internal" state="up" updated_at="%(timestamp)s" host="host1" id="1"/>
<service status="disabled" binary="nova-compute" zone="nova" state="up" updated_at="%(timestamp)s" host="host1" id="2"/>
<service status="enabled" binary="nova-scheduler" zone="internal" state="down" updated_at="%(timestamp)s" host="host2" id="3"/>
<service status="disabled" binary="nova-compute" zone="nova" state="down" updated_at="%(timestamp)s" host="host2" id="4"/>
</services>

View File

@@ -23,6 +23,7 @@ import urllib
import uuid as uuid_lib import uuid as uuid_lib
from lxml import etree from lxml import etree
import mock
from oslo.config import cfg from oslo.config import cfg
from nova.api.metadata import password from nova.api.metadata import password
@@ -1653,8 +1654,8 @@ class ServicesJsonTest(ApiSampleTestBaseV2):
super(ServicesJsonTest, self).tearDown() super(ServicesJsonTest, self).tearDown()
timeutils.clear_time_override() timeutils.clear_time_override()
def fake_load(self, *args): def fake_load(self, service_name):
return True return service_name == 'os-extended-services'
def test_services_list(self): def test_services_list(self):
"""Return a list of all agent builds.""" """Return a list of all agent builds."""
@@ -1738,6 +1739,51 @@ class ExtendedServicesXmlTest(ExtendedServicesJsonTest):
ctype = 'xml' 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): class SimpleTenantUsageSampleJsonTest(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib.simple_tenant_usage." extension_name = ("nova.api.openstack.compute.contrib.simple_tenant_usage."
"Simple_tenant_usage") "Simple_tenant_usage")

View File

@@ -4,6 +4,7 @@
"binary": "nova-scheduler", "binary": "nova-scheduler",
"disabled_reason": "test1", "disabled_reason": "test1",
"host": "host1", "host": "host1",
"id": 1,
"state": "up", "state": "up",
"status": "disabled", "status": "disabled",
"updated_at": "%(timestamp)s", "updated_at": "%(timestamp)s",
@@ -13,6 +14,7 @@
"binary": "nova-compute", "binary": "nova-compute",
"disabled_reason": "test2", "disabled_reason": "test2",
"host": "host1", "host": "host1",
"id": 2,
"state": "up", "state": "up",
"status": "disabled", "status": "disabled",
"updated_at": "%(timestamp)s", "updated_at": "%(timestamp)s",
@@ -22,6 +24,7 @@
"binary": "nova-scheduler", "binary": "nova-scheduler",
"disabled_reason": "", "disabled_reason": "",
"host": "host2", "host": "host2",
"id": 3,
"state": "down", "state": "down",
"status": "enabled", "status": "enabled",
"updated_at": "%(timestamp)s", "updated_at": "%(timestamp)s",
@@ -31,6 +34,7 @@
"binary": "nova-compute", "binary": "nova-compute",
"disabled_reason": "test4", "disabled_reason": "test4",
"host": "host2", "host": "host2",
"id": 4,
"state": "down", "state": "down",
"status": "disabled", "status": "disabled",
"updated_at": "%(timestamp)s", "updated_at": "%(timestamp)s",

View File

@@ -78,3 +78,9 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV3):
'service-disable-log-put-req', subs) 'service-disable-log-put-req', subs)
return self._verify_response('service-disable-log-put-resp', return self._verify_response('service-disable-log-put-resp',
subs, response, 200) 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(), "")