Teach HostAPI about cells

This makes the HostAPI module know enough about cells to support
the os-hypervisors, os-services, and several other API modules that
deal with hosts and services.

Note that this introduces a conflict where duplicate-id resources could
be returned from the API because they are in different DBs with different
key namespaces. For now, we return an error in the delete case where an
ambiguous result could cause us to delete the wrong service. This is
a fundamental problem across several of our APIs and is being worked as
a separate improvement.

Related to blueprint cells-aware-api

Change-Id: If1e03c9343b8cc9c34bd51c2b4d25acdb21131ff
This commit is contained in:
Dan Smith 2017-03-06 07:25:44 -08:00
parent 791cf06434
commit 74c5bfea6f
12 changed files with 262 additions and 37 deletions

View File

@ -108,7 +108,8 @@ class EvacuateController(wsgi.Controller):
if host is not None:
try:
self.host_api.service_get_by_compute_host(context, host)
except exception.ComputeHostNotFound:
except (exception.ComputeHostNotFound,
exception.HostMappingNotFound):
msg = _("Compute host %s not found.") % host
raise exc.HTTPNotFound(explanation=msg)

View File

@ -25,6 +25,7 @@ from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api import validation
from nova import compute
from nova import context as nova_context
from nova import exception
from nova import objects
from nova.policies import hosts as hosts_policies
@ -85,7 +86,7 @@ class HostController(wsgi.Controller):
if zone:
filters['availability_zone'] = zone
services = self.api.service_get_all(context, filters=filters,
set_zones=True)
set_zones=True, all_cells=True)
hosts = []
api_services = ('nova-osapi_compute', 'nova-ec2', 'nova-metadata')
for service in services:
@ -143,7 +144,7 @@ class HostController(wsgi.Controller):
result = self.api.set_host_maintenance(context, host_name, mode)
except NotImplementedError:
common.raise_feature_not_supported()
except exception.HostNotFound as e:
except (exception.HostNotFound, exception.HostMappingNotFound) as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
except exception.ComputeServiceUnavailable as e:
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
@ -161,11 +162,10 @@ class HostController(wsgi.Controller):
else:
LOG.info("Disabling host %s.", host_name)
try:
result = self.api.set_host_enabled(context, host_name=host_name,
enabled=enabled)
result = self.api.set_host_enabled(context, host_name, enabled)
except NotImplementedError:
common.raise_feature_not_supported()
except exception.HostNotFound as e:
except (exception.HostNotFound, exception.HostMappingNotFound) as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
except exception.ComputeServiceUnavailable as e:
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
@ -178,11 +178,10 @@ class HostController(wsgi.Controller):
context = req.environ['nova.context']
context.can(hosts_policies.BASE_POLICY_NAME)
try:
result = self.api.host_power_action(context, host_name=host_name,
action=action)
result = self.api.host_power_action(context, host_name, action)
except NotImplementedError:
common.raise_feature_not_supported()
except exception.HostNotFound as e:
except (exception.HostNotFound, exception.HostMappingNotFound) as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
except exception.ComputeServiceUnavailable as e:
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
@ -265,12 +264,15 @@ class HostController(wsgi.Controller):
context.can(hosts_policies.BASE_POLICY_NAME)
host_name = id
try:
mapping = objects.HostMapping.get_by_host(context, host_name)
nova_context.set_target_cell(context, mapping.cell_mapping)
compute_node = (
objects.ComputeNode.get_first_node_by_host_for_old_compat(
context, host_name))
except exception.ComputeHostNotFound as e:
instances = self.api.instance_get_all_by_host(context, host_name)
except (exception.ComputeHostNotFound,
exception.HostMappingNotFound) as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
instances = self.api.instance_get_all_by_host(context, host_name)
resources = [self._get_total_resources(host_name, compute_node)]
resources.append(self._get_used_now_resources(host_name,
compute_node))

View File

@ -211,7 +211,8 @@ class HypervisorsController(wsgi.Controller):
uptime = self.host_api.get_host_uptime(context, host)
except NotImplementedError:
common.raise_feature_not_supported()
except exception.ComputeServiceUnavailable as e:
except (exception.ComputeServiceUnavailable,
exception.HostMappingNotFound) as e:
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
service = self.host_api.service_get_by_compute_host(context, host)
@ -246,10 +247,13 @@ class HypervisorsController(wsgi.Controller):
raise webob.exc.HTTPNotFound(explanation=msg)
hypervisors = []
for compute_node in compute_nodes:
instances = self.host_api.instance_get_all_by_host(context,
try:
instances = self.host_api.instance_get_all_by_host(context,
compute_node.host)
service = self.host_api.service_get_by_compute_host(
context, compute_node.host)
service = self.host_api.service_get_by_compute_host(
context, compute_node.host)
except exception.HostMappingNotFound as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
hyp = self._view_hypervisor(compute_node, service, False, req,
instances)
hypervisors.append(hyp)

View File

@ -47,7 +47,8 @@ class ServiceController(wsgi.Controller):
_services = [
s
for s in self.host_api.service_get_all(context, set_zones=True)
for s in self.host_api.service_get_all(context, set_zones=True,
all_cells=True)
if s['binary'] not in api_services
]
@ -150,7 +151,8 @@ class ServiceController(wsgi.Controller):
"""Do the actual PUT/update"""
try:
self.host_api.service_update(context, host, binary, payload)
except exception.HostBinaryNotFound as exc:
except (exception.HostBinaryNotFound,
exception.HostMappingNotFound) as exc:
raise webob.exc.HTTPNotFound(explanation=exc.format_message())
def _perform_action(self, req, id, body, actions):
@ -193,6 +195,9 @@ class ServiceController(wsgi.Controller):
except exception.ServiceNotFound:
explanation = _("Service %s not found.") % id
raise webob.exc.HTTPNotFound(explanation=explanation)
except exception.ServiceNotUnique:
explanation = _("Service id %s refers to multiple services.") % id
raise webob.exc.HTTPBadRequest(explanation=explanation)
@extensions.expected_errors(())
def index(self, req):

View File

@ -4281,6 +4281,22 @@ class API(base.Base):
return host_statuses
def target_host_cell(fn):
"""Target a host-based function to a cell.
Expects to wrap a function of signature:
func(self, context, host, ...)
"""
@functools.wraps(fn)
def targeted(self, context, host, *args, **kwargs):
mapping = objects.HostMapping.get_by_host(context, host)
nova_context.set_target_cell(context, mapping.cell_mapping)
return fn(self, context, host, *args, **kwargs)
return targeted
class HostAPI(base.Base):
"""Sub-set of the Compute Manager API for managing host operations."""
@ -4299,6 +4315,7 @@ class HostAPI(base.Base):
return service['host']
@wrap_exception()
@target_host_cell
def set_host_enabled(self, context, host_name, enabled):
"""Sets the specified host's ability to accept new instances."""
host_name = self._assert_host_exists(context, host_name)
@ -4313,6 +4330,7 @@ class HostAPI(base.Base):
payload)
return result
@target_host_cell
def get_host_uptime(self, context, host_name):
"""Returns the result of calling "uptime" on the target host."""
host_name = self._assert_host_exists(context, host_name,
@ -4320,6 +4338,7 @@ class HostAPI(base.Base):
return self.rpcapi.get_host_uptime(context, host=host_name)
@wrap_exception()
@target_host_cell
def host_power_action(self, context, host_name, action):
"""Reboots, shuts down or powers up the host."""
host_name = self._assert_host_exists(context, host_name)
@ -4335,6 +4354,7 @@ class HostAPI(base.Base):
return result
@wrap_exception()
@target_host_cell
def set_host_maintenance(self, context, host_name, mode):
"""Start/Stop host maintenance window. On start, it triggers
guest VMs evacuation.
@ -4391,10 +4411,51 @@ class HostAPI(base.Base):
ret_services.append(service)
return ret_services
def _find_service(self, context, service_id):
"""Find a service by id by searching all cells.
If one matching service is found, return it. If none or multiple
are found, raise an exception.
:param context: A context.RequestContext
:param service_id: The DB ID of the service to find
:returns: An objects.Service
:raises: ServiceNotUnique if multiple matches are found
:raises: ServiceNotFound if no matches are found
"""
load_cells()
# NOTE(danms): Unfortunately this API exposes database identifiers
# which means we really can't do something efficient here
service = None
found_in_cell = None
for cell in CELLS:
# NOTE(danms): Services can be in cell0, so don't skip it here
try:
with nova_context.target_cell(context, cell):
cell_service = objects.Service.get_by_id(context,
service_id)
except exception.ServiceNotFound:
# NOTE(danms): Keep looking in other cells
continue
if service and cell_service:
raise exception.ServiceNotUnique()
service = cell_service
found_in_cell = cell
if service:
# NOTE(danms): Set the cell on the context so it remains
# when we return to our caller
nova_context.set_target_cell(context, found_in_cell)
return service
else:
raise exception.ServiceNotFound(service_id=service_id)
def service_get_by_id(self, context, service_id):
"""Get service entry for the given service id."""
return objects.Service.get_by_id(context, service_id)
return self._find_service(context, service_id)
@target_host_cell
def service_get_by_compute_host(self, context, host_name):
"""Get service entry for the given compute hostname."""
return objects.Service.get_by_compute_host(context, host_name)
@ -4406,6 +4467,7 @@ class HostAPI(base.Base):
service.save()
return service
@target_host_cell
def service_update(self, context, host_name, binary, params_to_update):
"""Enable / Disable a service.
@ -4417,12 +4479,14 @@ class HostAPI(base.Base):
def _service_delete(self, context, service_id):
"""Performs the actual Service deletion operation."""
objects.Service.get_by_id(context, service_id).destroy()
service = self._find_service(context, service_id)
service.destroy()
def service_delete(self, context, service_id):
"""Deletes the specified service."""
self._service_delete(context, service_id)
@target_host_cell
def instance_get_all_by_host(self, context, host_name):
"""Return all instances on the given host."""
return objects.InstanceList.get_by_host(context, host_name)
@ -4440,15 +4504,65 @@ class HostAPI(base.Base):
def compute_node_get(self, context, compute_id):
"""Return compute node entry for particular integer ID."""
return objects.ComputeNode.get_by_id(context, int(compute_id))
load_cells()
# NOTE(danms): Unfortunately this API exposes database identifiers
# which means we really can't do something efficient here
for cell in CELLS:
if cell.uuid == objects.CellMapping.CELL0_UUID:
continue
with nova_context.target_cell(context, cell):
try:
return objects.ComputeNode.get_by_id(context,
int(compute_id))
except exception.ComputeHostNotFound:
# NOTE(danms): Keep looking in other cells
continue
raise exception.ComputeHostNotFound(host=compute_id)
def compute_node_get_all(self, context, limit=None, marker=None):
return objects.ComputeNodeList.get_by_pagination(
context, limit=limit, marker=marker)
load_cells()
computes = []
for cell in CELLS:
if cell.uuid == objects.CellMapping.CELL0_UUID:
continue
with nova_context.target_cell(context, cell):
try:
cell_computes = objects.ComputeNodeList.get_by_pagination(
context, limit=limit, marker=marker)
except exception.MarkerNotFound:
# NOTE(danms): Keep looking through cells
continue
computes.extend(cell_computes)
# NOTE(danms): We must have found the marker, so continue on
# without one
marker = None
if limit:
limit -= len(cell_computes)
if limit <= 0:
break
if marker is not None and len(computes) == 0:
# NOTE(danms): If we did not find the marker in any cell,
# mimic the db_api behavior here.
raise exception.MarkerNotFound(marker=marker)
return objects.ComputeNodeList(objects=computes)
def compute_node_search_by_hypervisor(self, context, hypervisor_match):
return objects.ComputeNodeList.get_by_hypervisor(context,
hypervisor_match)
load_cells()
computes = []
for cell in CELLS:
if cell.uuid == objects.CellMapping.CELL0_UUID:
continue
with nova_context.target_cell(context, cell):
cell_computes = objects.ComputeNodeList.get_by_hypervisor(
context, hypervisor_match)
computes.extend(cell_computes)
return objects.ComputeNodeList(objects=computes)
def compute_node_statistics(self, context):
return self.db.compute_node_statistics(context)

View File

@ -431,6 +431,10 @@ class ServiceUnavailable(Invalid):
msg_fmt = _("Service is unavailable at this time.")
class ServiceNotUnique(Invalid):
msg_fmt = _("More than one possible service found.")
class ComputeResourcesUnavailable(ServiceUnavailable):
msg_fmt = _("Insufficient compute resources: %(reason)s.")

View File

@ -14,6 +14,7 @@
from oslo_utils import fixture as utils_fixture
from nova.tests import fixtures
from nova.tests.functional.notification_sample_tests \
import notification_sample_base
from nova.tests.unit.api.openstack.compute import test_services
@ -29,6 +30,7 @@ class TestServiceUpdateNotificationSample(
self.stub_out("nova.db.service_update",
test_services.fake_service_update)
self.useFixture(utils_fixture.TimeFixture(test_services.fake_utcnow()))
self.useFixture(fixtures.SingleCellSimple())
def test_service_enable(self):
body = {'host': 'host1',

View File

@ -47,6 +47,8 @@ def fake_compute_api_get(self, context, instance_id, **kwargs):
def fake_service_get_by_compute_host(self, context, host):
if host == 'bad-host':
raise exception.ComputeHostNotFound(host=host)
elif host == 'unmapped-host':
raise exception.HostMappingNotFound(name=host)
else:
return {
'host_name': host,
@ -154,6 +156,12 @@ class EvacuateTestV21(test.NoDBTestCase):
'onSharedStorage': 'False',
'adminPass': 'MyNewPass'})
def test_evacuate_instance_with_unmapped_target(self):
self._check_evacuate_failure(webob.exc.HTTPNotFound,
{'host': 'unmapped-host',
'onSharedStorage': 'False',
'adminPass': 'MyNewPass'})
def test_evacuate_instance_with_target(self):
admin_pass = 'MyNewPass'
res = self._get_evacuate_response({'host': 'my-host',

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
import testtools
import webob.exc
@ -23,6 +24,7 @@ from nova import context as context_maker
from nova import db
from nova import exception
from nova import test
from nova.tests import fixtures
from nova.tests.unit.api.openstack import fakes
from nova.tests.unit import fake_hosts
from nova.tests import uuidsentinel
@ -158,6 +160,7 @@ class HostTestCaseV21(test.TestCase):
self.controller = self.Controller()
self.hosts_api = self.controller.api
self.req = fakes.HTTPRequest.blank('', use_admin_context=True)
self.useFixture(fixtures.SingleCellSimple())
self._setup_stubs()
@ -369,6 +372,14 @@ class HostTestCaseV21(test.TestCase):
db.instance_destroy(ctxt, i_ref1['uuid'])
db.instance_destroy(ctxt, i_ref2['uuid'])
def test_show_late_host_mapping_gone(self):
s_ref = self._create_compute_service()
with mock.patch.object(self.controller.api,
'instance_get_all_by_host') as m:
m.side_effect = exception.HostMappingNotFound
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show, self.req, s_ref['host'])
def test_list_hosts_with_zone(self):
result = self.controller.index(FakeRequestWithNovaZone())
self.assertIn('hosts', result)

View File

@ -447,6 +447,17 @@ class HypervisorsTestV21(test.NoDBTestCase):
mock_get_uptime.assert_called_once_with(
mock.ANY, self.TEST_HYPERS_OBJ[0].host)
def test_uptime_hypervisor_not_mapped(self):
with mock.patch.object(self.controller.host_api, 'get_host_uptime',
side_effect=exception.HostMappingNotFound(name='dummy')
) as mock_get_uptime:
req = self._get_request(True)
self.assertRaises(exc.HTTPBadRequest,
self.controller.uptime, req,
self.TEST_HYPERS_OBJ[0].id)
mock_get_uptime.assert_called_once_with(
mock.ANY, self.TEST_HYPERS_OBJ[0].host)
def test_search(self):
req = self._get_request(True)
result = self.controller.search(req, 'hyper')
@ -488,6 +499,14 @@ class HypervisorsTestV21(test.NoDBTestCase):
del server['name']
self.assertEqual(dict(hypervisors=expected_dict), result)
def test_servers_not_mapped(self):
req = self._get_request(True)
with mock.patch.object(self.controller.host_api,
'instance_get_all_by_host') as m:
m.side_effect = exception.HostMappingNotFound
self.assertRaises(exc.HTTPNotFound,
self.controller.servers, req, 'hyper')
def test_servers_non_id(self):
with mock.patch.object(self.controller.host_api,
'compute_node_search_by_hypervisor',

View File

@ -33,6 +33,7 @@ from nova import exception
from nova import objects
from nova.servicegroup.drivers import db as db_driver
from nova import test
from nova.tests import fixtures
from nova.tests.unit.api.openstack import fakes
from nova.tests.unit.objects import test_service
@ -130,7 +131,8 @@ class FakeRequestWithHostService(FakeRequest):
def fake_service_get_all(services):
def service_get_all(context, filters=None, set_zones=False):
def service_get_all(context, filters=None, set_zones=False,
all_cells=False):
if set_zones or 'availability_zone' in filters:
return availability_zones.set_availability_zones(context,
services)
@ -210,6 +212,7 @@ class ServicesTestV21(test.TestCase):
fake_db_service_update(fake_services_list))
self.req = fakes.HTTPRequest.blank('')
self.useFixture(fixtures.SingleCellSimple())
def _process_output(self, services, has_disabled=False, has_id=False):
return services
@ -487,6 +490,17 @@ class ServicesTestV21(test.TestCase):
"enable",
body=body)
def test_services_enable_with_unmapped_host(self):
body = {'host': 'invalid', 'binary': 'nova-compute'}
with mock.patch.object(self.controller.host_api,
'service_update') as m:
m.side_effect = exception.HostMappingNotFound
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.update,
self.req,
"enable",
body=body)
def test_services_enable_with_invalid_binary(self):
body = {'host': 'host1', 'binary': 'invalid'}
self.assertRaises(webob.exc.HTTPNotFound,
@ -573,12 +587,19 @@ class ServicesTestV21(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, self.req, 1234)
def test_services_delete_bad_request(self):
def test_services_delete_invalid_id(self):
self.ext_mgr.extensions['os-extended-services-delete'] = True
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete, self.req, 'abc')
def test_services_delete_duplicate_service(self):
with mock.patch.object(self.controller, 'host_api') as host_api:
host_api.service_delete.side_effect = exception.ServiceNotUnique()
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete, self.req, 1234)
self.assertTrue(host_api.service_delete.called)
# This test is just to verify that the servicegroup API gets used when
# calling the API
@mock.patch.object(db_driver.DbDriver, 'is_up', side_effect=KeyError)

View File

@ -313,17 +313,47 @@ class ComputeHostAPITestCase(test.TestCase):
_do_test()
def test_service_delete(self):
with test.nested(
mock.patch.object(objects.Service, 'get_by_id',
return_value=objects.Service()),
mock.patch.object(objects.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()
@mock.patch('nova.context.set_target_cell')
@mock.patch('nova.compute.api.load_cells')
@mock.patch('nova.objects.Service.get_by_id')
def test_service_delete(self, get_by_id, load_cells, set_target):
compute_api.CELLS = [
objects.CellMapping(),
objects.CellMapping(),
objects.CellMapping(),
]
service = mock.MagicMock()
get_by_id.side_effect = [exception.ServiceNotFound(service_id=1),
service,
exception.ServiceNotFound(service_id=1)]
self.host_api.service_delete(self.ctxt, 1)
get_by_id.assert_has_calls([mock.call(self.ctxt, 1),
mock.call(self.ctxt, 1),
mock.call(self.ctxt, 1)])
service.destroy.assert_called_once_with()
set_target.assert_called_once_with(self.ctxt, compute_api.CELLS[1])
@mock.patch('nova.context.set_target_cell')
@mock.patch('nova.compute.api.load_cells')
@mock.patch('nova.objects.Service.get_by_id')
def test_service_delete_ambiguous(self, get_by_id, load_cells, set_target):
compute_api.CELLS = [
objects.CellMapping(),
objects.CellMapping(),
objects.CellMapping(),
]
service1 = mock.MagicMock()
service2 = mock.MagicMock()
get_by_id.side_effect = [exception.ServiceNotFound(service_id=1),
service1,
service2]
self.assertRaises(exception.ServiceNotUnique,
self.host_api.service_delete, self.ctxt, 1)
self.assertFalse(service1.destroy.called)
self.assertFalse(service2.destroy.called)
self.assertFalse(set_target.called)
def test_service_delete_compute_in_aggregate(self):
compute = self.host_api.db.service_create(self.ctxt,
@ -353,6 +383,10 @@ class ComputeHostAPICellsTestCase(ComputeHostAPITestCase):
def test_service_get_all_cells(self):
pass
@testtools.skip('cellsv1 does not use this')
def test_service_delete_ambiguous(self):
pass
def test_service_get_all_no_zones(self):
services = [
cells_utils.ServiceProxy(