Merge "Teach HostAPI about cells"

This commit is contained in:
Jenkins 2017-04-10 22:49:17 +00:00 committed by Gerrit Code Review
commit b87bd4b416
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

@ -4283,6 +4283,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."""
@ -4301,6 +4317,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)
@ -4315,6 +4332,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,
@ -4322,6 +4340,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)
@ -4337,6 +4356,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.
@ -4393,10 +4413,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)
@ -4408,6 +4469,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.
@ -4419,12 +4481,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)
@ -4442,15 +4506,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(