Cleanup RP and HM records while deleting a compute service.

Currently when deleting a nova-compute service via the API, we will
(soft) delete the service and compute_node records in the DB, but the
placement resource provider and host mapping records will be orphaned.
This patch deletes the resource provider and host_mapping records
before deleting the service/compute node.

Change-Id: I7b8622b178d5043ed1556d7bdceaf60f47e5ac80
Closes-Bug: #1756179
(cherry picked from commit 589c495c1a)
(cherry picked from commit dede2de2bd)
(cherry picked from commit 9fe847bdca)
This commit is contained in:
Surya Seetharaman 2018-03-21 14:16:24 +01:00 committed by huanhongda
parent fe0c1033d9
commit 94848ca40f
4 changed files with 54 additions and 3 deletions

View File

@ -23,7 +23,9 @@ from nova.api import validation
from nova import compute from nova import compute
from nova import exception from nova import exception
from nova.i18n import _ from nova.i18n import _
from nova import objects
from nova.policies import services as services_policies from nova.policies import services as services_policies
from nova.scheduler.client import report
from nova import servicegroup from nova import servicegroup
from nova import utils from nova import utils
@ -39,6 +41,7 @@ class ServiceController(wsgi.Controller):
self.actions = {"enable": self._enable, self.actions = {"enable": self._enable,
"disable": self._disable, "disable": self._disable,
"disable-log-reason": self._disable_log_reason} "disable-log-reason": self._disable_log_reason}
self.placementclient = report.SchedulerReportClient()
def _get_services(self, req): def _get_services(self, req):
api_services = ('nova-osapi_compute', 'nova-ec2', 'nova-metadata') api_services = ('nova-osapi_compute', 'nova-ec2', 'nova-metadata')
@ -189,6 +192,16 @@ class ServiceController(wsgi.Controller):
self.aggregate_api.remove_host_from_aggregate(context, self.aggregate_api.remove_host_from_aggregate(context,
ag.id, ag.id,
service.host) service.host)
# remove the corresponding resource provider record from
# placement for this compute node
self.placementclient.delete_resource_provider(
context, service.compute_node, cascade=True)
# remove the host_mapping of this host.
try:
hm = objects.HostMapping.get_by_host(context, service.host)
hm.destroy()
except exception.HostMappingNotFound:
pass
self.host_api.service_delete(context, id) self.host_api.service_delete(context, id)
except exception.ServiceNotFound: except exception.ServiceNotFound:

View File

@ -57,7 +57,17 @@ class APIResponse(object):
self.status = response.status_code self.status = response.status_code
self.content = response.content self.content = response.content
if self.content: if self.content:
self.body = jsonutils.loads(self.content) # The Compute API and Placement API handle error responses a bit
# differently so we need to check the content-type header to
# figure out what to do.
content_type = response.headers.get('content-type')
if 'application/json' in content_type:
self.body = response.json()
elif 'text/html' in content_type:
self.body = response.text
else:
raise ValueError('Unexpected response content-type: %s' %
content_type)
self.headers = response.headers self.headers = response.headers
def __str__(self): def __str__(self):

View File

@ -31,6 +31,7 @@ from nova import compute
from nova import context from nova import context
from nova import exception from nova import exception
from nova import objects from nova import objects
from nova.scheduler.client import report as scheduler_report
from nova.servicegroup.drivers import db as db_driver from nova.servicegroup.drivers import db as db_driver
from nova import test from nova import test
from nova.tests.unit.api.openstack import fakes from nova.tests.unit.api.openstack import fakes
@ -554,7 +555,9 @@ class ServicesTestV21(test.TestCase):
self.controller.update, self.req, "disable-log-reason", self.controller.update, self.req, "disable-log-reason",
body=body) body=body)
def test_services_delete(self): @mock.patch.object(scheduler_report.SchedulerReportClient,
'delete_resource_provider')
def test_services_delete(self, mock_delete_rp):
self.ext_mgr.extensions['os-extended-services-delete'] = True self.ext_mgr.extensions['os-extended-services-delete'] = True
compute = self.host_api.db.service_create(self.ctxt, compute = self.host_api.db.service_create(self.ctxt,
@ -563,12 +566,29 @@ class ServicesTestV21(test.TestCase):
'topic': 'compute', 'topic': 'compute',
'report_count': 0}) 'report_count': 0})
self.host_api.db.compute_node_create(self.ctxt,
{
'uuid': 'f1891d93-37a1-4230-aa10-433f63fb7d67',
'host': 'fake-compute-host',
'vcpus': 64,
'memory_mb': 1024,
'local_gb': 500,
'vcpus_used': 0,
'memory_mb_used': 128,
'local_gb_used': 20,
'hypervisor_type': 'QEMU',
'hypervisor_version': 2009000,
'cpu_info': '{"vendor": "Intel", "model": "Broadwell", '
'"arch": "x86_64"}'
})
with mock.patch.object(self.controller.host_api, with mock.patch.object(self.controller.host_api,
'service_delete') as service_delete: 'service_delete') as service_delete:
self.controller.delete(self.req, compute.id) self.controller.delete(self.req, compute.id)
service_delete.assert_called_once_with( service_delete.assert_called_once_with(
self.req.environ['nova.context'], compute.id) self.req.environ['nova.context'], compute.id)
self.assertEqual(self.controller.delete.wsgi_code, 204) self.assertEqual(self.controller.delete.wsgi_code, 204)
mock_delete_rp.assert_called_once()
def test_services_delete_not_found(self): def test_services_delete_not_found(self):
self.ext_mgr.extensions['os-extended-services-delete'] = True self.ext_mgr.extensions['os-extended-services-delete'] = True

View File

@ -31,6 +31,7 @@ from nova.tests.unit.api.openstack import fakes
from nova.tests.unit import fake_notifier from nova.tests.unit import fake_notifier
from nova.tests.unit.objects import test_objects from nova.tests.unit.objects import test_objects
from nova.tests.unit.objects import test_service from nova.tests.unit.objects import test_service
from nova.tests import uuidsentinel as uuids
import testtools import testtools
@ -312,12 +313,18 @@ class ComputeHostAPITestCase(test.TestCase):
get_by_id.assert_called_once_with(self.ctxt, 1) get_by_id.assert_called_once_with(self.ctxt, 1)
destroy.assert_called_once_with() destroy.assert_called_once_with()
def test_service_delete_compute_in_aggregate(self): @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
@mock.patch.object(objects.HostMapping, 'get_by_host')
def test_service_delete_compute_in_aggregate(self, mock_hm, mock_get_cn):
compute = self.host_api.db.service_create(self.ctxt, compute = self.host_api.db.service_create(self.ctxt,
{'host': 'fake-compute-host', {'host': 'fake-compute-host',
'binary': 'nova-compute', 'binary': 'nova-compute',
'topic': 'compute', 'topic': 'compute',
'report_count': 0}) 'report_count': 0})
# This is needed because of lazy-loading service.compute_node
cn = objects.ComputeNode(uuid=uuids.cn, host="fake-compute-host",
hypervisor_hostname="fake-compute-host")
mock_get_cn.return_value = [cn]
aggregate = self.aggregate_api.create_aggregate(self.ctxt, aggregate = self.aggregate_api.create_aggregate(self.ctxt,
'aggregate', 'aggregate',
None) None)
@ -328,6 +335,7 @@ class ComputeHostAPITestCase(test.TestCase):
result = self.aggregate_api.get_aggregate(self.ctxt, result = self.aggregate_api.get_aggregate(self.ctxt,
aggregate.id).hosts aggregate.id).hosts
self.assertEqual([], result) self.assertEqual([], result)
mock_hm.return_value.destroy.assert_called_once_with()
class ComputeHostAPICellsTestCase(ComputeHostAPITestCase): class ComputeHostAPICellsTestCase(ComputeHostAPITestCase):