diff --git a/bin/nova-manage b/bin/nova-manage index c783c304b8c2..4f3d889eaecf 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1056,11 +1056,11 @@ class CellCommands(object): ctxt = context.get_admin_context() db.cell_create(ctxt, values) - @args('--cell_id', dest='cell_id', metavar='', - help='ID of the cell to delete') - def delete(self, cell_id): + @args('--cell_name', dest='cell_name', metavar='', + help='Name of the cell to delete') + def delete(self, cell_name): ctxt = context.get_admin_context() - db.cell_delete(ctxt, cell_id) + db.cell_delete(ctxt, cell_name) def list(self): ctxt = context.get_admin_context() diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index 25d077f276a2..bd002c080d89 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -88,6 +88,14 @@ "namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1", "updated": "2012-08-09T00:00:00+00:00" }, + { + "alias": "os-cells", + "description": "Enables cells-related functionality such as adding neighbor cells,\n listing neighbor cells, and getting the capabilities of the local cell.\n ", + "links": [], + "name": "Cells", + "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", + "updated": "2011-09-21T00:00:00+00:00" + }, { "alias": "os-certificates", "description": "Certificates support.", diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index b66c3dbe78a6..ebb1c4302225 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -37,6 +37,12 @@ Add availability_zone to the Create Server v1.1 API. + + Enables cells-related functionality such as adding child cells, + listing child cells, getting the capabilities of the local cell, + and returning build plans to parent cells' schedulers + + Certificates support. diff --git a/doc/api_samples/os-cells/cells-get-resp.json b/doc/api_samples/os-cells/cells-get-resp.json new file mode 100644 index 000000000000..62eb8ec31d20 --- /dev/null +++ b/doc/api_samples/os-cells/cells-get-resp.json @@ -0,0 +1,9 @@ +{ + "cell": { + "name": "cell3", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username3" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-get-resp.xml b/doc/api_samples/os-cells/cells-get-resp.xml new file mode 100644 index 000000000000..12256a5bdcef --- /dev/null +++ b/doc/api_samples/os-cells/cells-get-resp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-empty-resp.json b/doc/api_samples/os-cells/cells-list-empty-resp.json new file mode 100644 index 000000000000..5325a4e855e2 --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-empty-resp.json @@ -0,0 +1,3 @@ +{ + "cells": [] +} \ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-empty-resp.xml b/doc/api_samples/os-cells/cells-list-empty-resp.xml new file mode 100644 index 000000000000..6ac77b4bd8c5 --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-empty-resp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-resp.json b/doc/api_samples/os-cells/cells-list-resp.json new file mode 100644 index 000000000000..97ea4c6dd32d --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-resp.json @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "name": "cell1", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username1" + }, + { + "name": "cell3", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username3" + }, + { + "name": "cell5", + "rpc_host": null, + "rpc_port": null, + "type": "child", + "username": "username5" + }, + { + "name": "cell2", + "rpc_host": null, + "rpc_port": null, + "type": "parent", + "username": "username2" + }, + { + "name": "cell4", + "rpc_host": null, + "rpc_port": null, + "type": "parent", + "username": "username4" + } + ] +} \ No newline at end of file diff --git a/doc/api_samples/os-cells/cells-list-resp.xml b/doc/api_samples/os-cells/cells-list-resp.xml new file mode 100644 index 000000000000..7d697bb91868 --- /dev/null +++ b/doc/api_samples/os-cells/cells-list-resp.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/doc/api_samples/os-hosts/hosts-list-resp.json b/doc/api_samples/os-hosts/hosts-list-resp.json index 5a963c602442..0c4126a7e0b2 100644 --- a/doc/api_samples/os-hosts/hosts-list-resp.json +++ b/doc/api_samples/os-hosts/hosts-list-resp.json @@ -24,6 +24,11 @@ "host_name": "6e48bfe1a3304b7b86154326328750ae", "service": "conductor", "zone": "internal" + }, + { + "host_name": "39f55087a1024d1380755951c945ca69", + "service": "cells", + "zone": "internal" } ] } diff --git a/doc/api_samples/os-hosts/hosts-list-resp.xml b/doc/api_samples/os-hosts/hosts-list-resp.xml index 8266a5d49147..9a99c577a0dc 100644 --- a/doc/api_samples/os-hosts/hosts-list-resp.xml +++ b/doc/api_samples/os-hosts/hosts-list-resp.xml @@ -5,4 +5,5 @@ - \ No newline at end of file + + diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 04766371eec4..fd1f9c2e05a2 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -29,6 +29,7 @@ "compute_extension:admin_actions:migrate": "rule:admin_api", "compute_extension:aggregates": "rule:admin_api", "compute_extension:agents": "rule:admin_api", + "compute_extension:cells": "rule:admin_api", "compute_extension:certificates": "", "compute_extension:cloudpipe": "rule:admin_api", "compute_extension:cloudpipe_update": "rule:admin_api", diff --git a/nova/api/openstack/compute/contrib/cells.py b/nova/api/openstack/compute/contrib/cells.py new file mode 100644 index 000000000000..03e2e4ca27d9 --- /dev/null +++ b/nova/api/openstack/compute/contrib/cells.py @@ -0,0 +1,303 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011-2012 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +"""The cells extension.""" +from xml.dom import minidom +from xml.parsers import expat + +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.cells import rpcapi as cells_rpcapi +from nova.compute import api as compute +from nova import db +from nova import exception +from nova.openstack.common import cfg +from nova.openstack.common import log as logging +from nova.openstack.common import timeutils + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF +CONF.import_opt('name', 'nova.cells.opts', group='cells') +CONF.import_opt('capabilities', 'nova.cells.opts', group='cells') + +authorize = extensions.extension_authorizer('compute', 'cells') + + +def make_cell(elem): + elem.set('name') + elem.set('username') + elem.set('type') + elem.set('rpc_host') + elem.set('rpc_port') + + caps = xmlutil.SubTemplateElement(elem, 'capabilities', + selector='capabilities') + cap = xmlutil.SubTemplateElement(caps, xmlutil.Selector(0), + selector=xmlutil.get_items) + cap.text = 1 + + +cell_nsmap = {None: wsgi.XMLNS_V10} + + +class CellTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('cell', selector='cell') + make_cell(root) + return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap) + + +class CellsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('cells') + elem = xmlutil.SubTemplateElement(root, 'cell', selector='cells') + make_cell(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap) + + +class CellDeserializer(wsgi.XMLDeserializer): + """Deserializer to handle xml-formatted cell create requests.""" + + def _extract_capabilities(self, cap_node): + caps = {} + for cap in cap_node.childNodes: + cap_name = cap.tagName + caps[cap_name] = self.extract_text(cap) + return caps + + def _extract_cell(self, node): + cell = {} + cell_node = self.find_first_child_named(node, 'cell') + + extract_fns = {'capabilities': self._extract_capabilities} + + for child in cell_node.childNodes: + name = child.tagName + extract_fn = extract_fns.get(name, self.extract_text) + cell[name] = extract_fn(child) + return cell + + def default(self, string): + """Deserialize an xml-formatted cell create request.""" + try: + node = minidom.parseString(string) + except expat.ExpatError: + msg = _("cannot understand XML") + raise exception.MalformedRequestBody(reason=msg) + + return {'body': {'cell': self._extract_cell(node)}} + + +def _filter_keys(item, keys): + """ + Filters all model attributes except for keys + item is a dict + + """ + return dict((k, v) for k, v in item.iteritems() if k in keys) + + +def _scrub_cell(cell, detail=False): + keys = ['name', 'username', 'rpc_host', 'rpc_port'] + if detail: + keys.append('capabilities') + + cell_info = _filter_keys(cell, keys) + cell_info['type'] = 'parent' if cell['is_parent'] else 'child' + return cell_info + + +class Controller(object): + """Controller for Cell resources.""" + + def __init__(self): + self.compute_api = compute.API() + self.cells_rpcapi = cells_rpcapi.CellsAPI() + + def _get_cells(self, ctxt, req, detail=False): + """Return all cells.""" + # Ask the CellsManager for the most recent data + items = self.cells_rpcapi.get_cell_info_for_neighbors(ctxt) + items = common.limited(items, req) + items = [_scrub_cell(item, detail=detail) for item in items] + return dict(cells=items) + + @wsgi.serializers(xml=CellsTemplate) + def index(self, req): + """Return all cells in brief.""" + ctxt = req.environ['nova.context'] + authorize(ctxt) + return self._get_cells(ctxt, req) + + @wsgi.serializers(xml=CellsTemplate) + def detail(self, req): + """Return all cells in detail.""" + ctxt = req.environ['nova.context'] + authorize(ctxt) + return self._get_cells(ctxt, req, detail=True) + + @wsgi.serializers(xml=CellTemplate) + def info(self, req): + """Return name and capabilities for this cell.""" + context = req.environ['nova.context'] + authorize(context) + cell_capabs = {} + my_caps = CONF.cells.capabilities + for cap in my_caps: + key, value = cap.split('=') + cell_capabs[key] = value + cell = {'name': CONF.cells.name, + 'type': 'self', + 'rpc_host': None, + 'rpc_port': 0, + 'username': None, + 'capabilities': cell_capabs} + return dict(cell=cell) + + @wsgi.serializers(xml=CellTemplate) + def show(self, req, id): + """Return data about the given cell name. 'id' is a cell name.""" + context = req.environ['nova.context'] + authorize(context) + try: + cell = db.cell_get(context, id) + except exception.CellNotFound: + raise exc.HTTPNotFound() + return dict(cell=_scrub_cell(cell)) + + def delete(self, req, id): + """Delete a child or parent cell entry. 'id' is a cell name.""" + context = req.environ['nova.context'] + authorize(context) + num_deleted = db.cell_delete(context, id) + if num_deleted == 0: + raise exc.HTTPNotFound() + return {} + + def _validate_cell_name(self, cell_name): + """Validate cell name is not empty and doesn't contain '!' or '.'.""" + if not cell_name: + msg = _("Cell name cannot be empty") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + if '!' in cell_name or '.' in cell_name: + msg = _("Cell name cannot contain '!' or '.'") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + + def _validate_cell_type(self, cell_type): + """Validate cell_type is 'parent' or 'child'.""" + if cell_type not in ['parent', 'child']: + msg = _("Cell type must be 'parent' or 'child'") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + + def _convert_cell_type(self, cell): + """Convert cell['type'] to is_parent boolean.""" + if 'type' in cell: + self._validate_cell_type(cell['type']) + cell['is_parent'] = cell['type'] == 'parent' + del cell['type'] + else: + cell['is_parent'] = False + + @wsgi.serializers(xml=CellTemplate) + @wsgi.deserializers(xml=CellDeserializer) + def create(self, req, body): + """Create a child cell entry.""" + context = req.environ['nova.context'] + authorize(context) + if 'cell' not in body: + msg = _("No cell information in request") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + cell = body['cell'] + if 'name' not in cell: + msg = _("No cell name in request") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + self._validate_cell_name(cell['name']) + self._convert_cell_type(cell) + cell = db.cell_create(context, cell) + return dict(cell=_scrub_cell(cell)) + + @wsgi.serializers(xml=CellTemplate) + @wsgi.deserializers(xml=CellDeserializer) + def update(self, req, id, body): + """Update a child cell entry. 'id' is the cell name to update.""" + context = req.environ['nova.context'] + authorize(context) + if 'cell' not in body: + msg = _("No cell information in request") + LOG.error(msg) + raise exc.HTTPBadRequest(explanation=msg) + cell = body['cell'] + cell.pop('id', None) + if 'name' in cell: + self._validate_cell_name(cell['name']) + self._convert_cell_type(cell) + try: + cell = db.cell_update(context, id, cell) + except exception.CellNotFound: + raise exc.HTTPNotFound() + return dict(cell=_scrub_cell(cell)) + + def sync_instances(self, req, body): + """Tell all cells to sync instance info.""" + context = req.environ['nova.context'] + authorize(context) + project_id = body.pop('project_id', None) + deleted = body.pop('deleted', False) + updated_since = body.pop('updated_since', None) + if body: + msg = _("Only 'updated_since' and 'project_id' are understood.") + raise exc.HTTPBadRequest(explanation=msg) + if updated_since: + try: + timeutils.parse_isotime(updated_since) + except ValueError: + msg = _('Invalid changes-since value') + raise exc.HTTPBadRequest(explanation=msg) + self.cells_rpcapi.sync_instances(context, project_id=project_id, + updated_since=updated_since, deleted=deleted) + + +class Cells(extensions.ExtensionDescriptor): + """Enables cells-related functionality such as adding neighbor cells, + listing neighbor cells, and getting the capabilities of the local cell. + """ + + name = "Cells" + alias = "os-cells" + namespace = "http://docs.openstack.org/compute/ext/cells/api/v1.1" + updated = "2011-09-21T00:00:00+00:00" + + def get_resources(self): + coll_actions = { + 'detail': 'GET', + 'info': 'GET', + 'sync_instances': 'POST', + } + + res = extensions.ResourceExtension('os-cells', + Controller(), collection_actions=coll_actions) + return [res] diff --git a/nova/cells/manager.py b/nova/cells/manager.py index 0942bae28fa8..1339467948ba 100644 --- a/nova/cells/manager.py +++ b/nova/cells/manager.py @@ -65,7 +65,7 @@ class CellsManager(manager.Manager): Scheduling requests get passed to the scheduler class. """ - RPC_API_VERSION = '1.0' + RPC_API_VERSION = '1.1' def __init__(self, *args, **kwargs): # Mostly for tests. @@ -186,6 +186,10 @@ class CellsManager(manager.Manager): self.msg_runner.schedule_run_instance(ctxt, our_cell, host_sched_kwargs) + def get_cell_info_for_neighbors(self, _ctxt): + """Return cell information for our neighbor cells.""" + return self.state_manager.get_cell_info_for_neighbors() + def run_compute_api_method(self, ctxt, cell_name, method_info, call): """Call a compute API method in a specific cell.""" response = self.msg_runner.run_compute_api_method(ctxt, @@ -218,3 +222,10 @@ class CellsManager(manager.Manager): def bw_usage_update_at_top(self, ctxt, bw_update_info): """Update bandwidth usage at top level cell.""" self.msg_runner.bw_usage_update_at_top(ctxt, bw_update_info) + + def sync_instances(self, ctxt, project_id, updated_since, deleted): + """Force a sync of all instances, potentially by project_id, + and potentially since a certain date/time. + """ + self.msg_runner.sync_instances(ctxt, project_id, updated_since, + deleted) diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py index 56d5218922da..34ca748552b8 100644 --- a/nova/cells/messaging.py +++ b/nova/cells/messaging.py @@ -27,6 +27,7 @@ import sys from eventlet import queue from nova.cells import state as cells_state +from nova.cells import utils as cells_utils from nova import compute from nova import context from nova.db import base @@ -37,6 +38,7 @@ from nova.openstack.common import importutils from nova.openstack.common import jsonutils from nova.openstack.common import log as logging from nova.openstack.common.rpc import common as rpc_common +from nova.openstack.common import timeutils from nova.openstack.common import uuidutils from nova import utils @@ -778,6 +780,26 @@ class _BroadcastMessageMethods(_BaseMessageMethods): return self.db.bw_usage_update(message.ctxt, **bw_update_info) + def _sync_instance(self, ctxt, instance): + if instance['deleted']: + self.msg_runner.instance_destroy_at_top(ctxt, instance) + else: + self.msg_runner.instance_update_at_top(ctxt, instance) + + def sync_instances(self, message, project_id, updated_since, deleted, + **kwargs): + projid_str = project_id is None and "" or project_id + since_str = updated_since is None and "" or updated_since + LOG.info(_("Forcing a sync of instances, project_id=" + "%(projid_str)s, updated_since=%(since_str)s"), locals()) + if updated_since is not None: + updated_since = timeutils.parse_isotime(updated_since) + instances = cells_utils.get_instances_to_sync(message.ctxt, + updated_since=updated_since, project_id=project_id, + deleted=deleted) + for instance in instances: + self._sync_instance(message.ctxt, instance) + _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS = {'targeted': _TargetedMessage, 'broadcast': _BroadcastMessage, @@ -1004,6 +1026,18 @@ class MessageRunner(object): 'up', run_locally=False) message.process() + def sync_instances(self, ctxt, project_id, updated_since, deleted): + """Force a sync of all instances, potentially by project_id, + and potentially since a certain date/time. + """ + method_kwargs = dict(project_id=project_id, + updated_since=updated_since, + deleted=deleted) + message = _BroadcastMessage(self, ctxt, 'sync_instances', + method_kwargs, 'down', + run_locally=False) + message.process() + @staticmethod def get_message_types(): return _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS.keys() diff --git a/nova/cells/rpcapi.py b/nova/cells/rpcapi.py index 8ce2988295b6..0ab4fc352a74 100644 --- a/nova/cells/rpcapi.py +++ b/nova/cells/rpcapi.py @@ -40,6 +40,7 @@ class CellsAPI(rpc_proxy.RpcProxy): API version history: 1.0 - Initial version. + 1.1 - Adds get_cell_info_for_neighbors() and sync_instances() ''' BASE_RPC_API_VERSION = '1.0' @@ -136,3 +137,21 @@ class CellsAPI(rpc_proxy.RpcProxy): 'info_cache': iicache} self.cast(ctxt, self.make_msg('instance_update_at_top', instance=instance)) + + def get_cell_info_for_neighbors(self, ctxt): + """Get information about our neighbor cells from the manager.""" + if not CONF.cells.enable: + return [] + return self.call(ctxt, self.make_msg('get_cell_info_for_neighbors'), + version='1.1') + + def sync_instances(self, ctxt, project_id=None, updated_since=None, + deleted=False): + """Ask all cells to sync instance data.""" + if not CONF.cells.enable: + return + return self.cast(ctxt, self.make_msg('sync_instances', + project_id=project_id, + updated_since=updated_since, + deleted=deleted), + version='1.1') diff --git a/nova/cells/state.py b/nova/cells/state.py index 345c44ca906f..e3886bedb178 100644 --- a/nova/cells/state.py +++ b/nova/cells/state.py @@ -75,8 +75,8 @@ class CellState(object): def get_cell_info(self): """Return subset of cell information for OS API use.""" - db_fields_to_return = ['id', 'is_parent', 'weight_scale', - 'weight_offset', 'username', 'rpc_host', 'rpc_port'] + db_fields_to_return = ['is_parent', 'weight_scale', 'weight_offset', + 'username', 'rpc_host', 'rpc_port'] cell_info = dict(name=self.name, capabilities=self.capabilities) if self.db_info: for field in db_fields_to_return: @@ -266,6 +266,15 @@ class CellStateManager(base.Base): self._refresh_cells_from_db(ctxt) self._update_our_capacity(ctxt) + @sync_from_db + def get_cell_info_for_neighbors(self): + """Return cell information for all neighbor cells.""" + cell_list = [cell.get_cell_info() + for cell in self.child_cells.itervalues()] + cell_list.extend([cell.get_cell_info() + for cell in self.parent_cells.itervalues()]) + return cell_list + @sync_from_db def get_my_state(self): """Return information for my (this) cell.""" diff --git a/nova/db/api.py b/nova/db/api.py index d7d9bd0d2590..ecfcfab15836 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1360,19 +1360,19 @@ def cell_create(context, values): return IMPL.cell_create(context, values) -def cell_update(context, cell_id, values): +def cell_update(context, cell_name, values): """Update a child Cell entry.""" - return IMPL.cell_update(context, cell_id, values) + return IMPL.cell_update(context, cell_name, values) -def cell_delete(context, cell_id): +def cell_delete(context, cell_name): """Delete a child Cell.""" - return IMPL.cell_delete(context, cell_id) + return IMPL.cell_delete(context, cell_name) -def cell_get(context, cell_id): +def cell_get(context, cell_name): """Get a specific child Cell.""" - return IMPL.cell_get(context, cell_id) + return IMPL.cell_get(context, cell_name) def cell_get_all(context): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 3fdfd53c8368..038a47ca163a 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3719,34 +3719,30 @@ def cell_create(context, values): return cell -def _cell_get_by_id_query(context, cell_id, session=None): - return model_query(context, models.Cell, session=session).\ - filter_by(id=cell_id) +def _cell_get_by_name_query(context, cell_name, session=None): + return model_query(context, models.Cell, + session=session).filter_by(name=cell_name) @require_admin_context -def cell_update(context, cell_id, values): - cell = cell_get(context, cell_id) - cell.update(values) - cell.save() +def cell_update(context, cell_name, values): + session = get_session() + with session.begin(): + cell = _cell_get_by_name_query(context, cell_name, session=session) + cell.update(values) return cell @require_admin_context -def cell_delete(context, cell_id): - session = get_session() - with session.begin(): - return _cell_get_by_id_query(context, cell_id, session=session).\ - delete() +def cell_delete(context, cell_name): + return _cell_get_by_name_query(context, cell_name).soft_delete() @require_admin_context -def cell_get(context, cell_id): - result = _cell_get_by_id_query(context, cell_id).first() - +def cell_get(context, cell_name): + result = _cell_get_by_name_query(context, cell_name).first() if not result: - raise exception.CellNotFound(cell_id=cell_id) - + raise exception.CellNotFound(cell_name=cell_name) return result diff --git a/nova/exception.py b/nova/exception.py index f96b1eaf3cd0..c1005f866264 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -768,7 +768,7 @@ class FlavorAccessNotFound(NotFound): class CellNotFound(NotFound): - message = _("Cell %(cell_id)s could not be found.") + message = _("Cell %(cell_name)s doesn't exist.") class CellRoutingInconsistency(NovaException): diff --git a/nova/tests/api/openstack/compute/contrib/test_cells.py b/nova/tests/api/openstack/compute/contrib/test_cells.py new file mode 100644 index 000000000000..82d4695247b6 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_cells.py @@ -0,0 +1,396 @@ +# Copyright 2011-2012 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +import copy + +from lxml import etree +from webob import exc + +from nova.api.openstack.compute.contrib import cells as cells_ext +from nova.api.openstack import xmlutil +from nova.cells import rpcapi as cells_rpcapi +from nova import context +from nova import db +from nova import exception +from nova.openstack.common import timeutils +from nova import test +from nova.tests.api.openstack import fakes + + +FAKE_CELLS = [ + dict(id=1, name='cell1', username='bob', is_parent=True, + weight_scale=1.0, weight_offset=0.0, + rpc_host='r1.example.org', password='xxxx'), + dict(id=2, name='cell2', username='alice', is_parent=False, + weight_scale=1.0, weight_offset=0.0, + rpc_host='r2.example.org', password='qwerty')] + + +FAKE_CAPABILITIES = [ + {'cap1': '0,1', 'cap2': '2,3'}, + {'cap3': '4,5', 'cap4': '5,6'}] + + +def fake_db_cell_get(context, cell_name): + for cell in FAKE_CELLS: + if cell_name == cell['name']: + return cell + else: + raise exception.CellNotFound(cell_name=cell_name) + + +def fake_db_cell_create(context, values): + cell = dict(id=1) + cell.update(values) + return cell + + +def fake_db_cell_update(context, cell_id, values): + cell = fake_db_cell_get(context, cell_id) + cell.update(values) + return cell + + +def fake_cells_api_get_all_cell_info(*args): + cells = copy.deepcopy(FAKE_CELLS) + del cells[0]['password'] + del cells[1]['password'] + for i, cell in enumerate(cells): + cell['capabilities'] = FAKE_CAPABILITIES[i] + return cells + + +def fake_db_cell_get_all(context): + return FAKE_CELLS + + +class CellsTest(test.TestCase): + def setUp(self): + super(CellsTest, self).setUp() + self.stubs.Set(db, 'cell_get', fake_db_cell_get) + self.stubs.Set(db, 'cell_get_all', fake_db_cell_get_all) + self.stubs.Set(db, 'cell_update', fake_db_cell_update) + self.stubs.Set(db, 'cell_create', fake_db_cell_create) + self.stubs.Set(cells_rpcapi.CellsAPI, 'get_cell_info_for_neighbors', + fake_cells_api_get_all_cell_info) + + self.controller = cells_ext.Controller() + self.context = context.get_admin_context() + + def _get_request(self, resource): + return fakes.HTTPRequest.blank('/v2/fake/' + resource) + + def test_index(self): + req = self._get_request("cells") + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['cells']), 2) + for i, cell in enumerate(res_dict['cells']): + self.assertEqual(cell['name'], FAKE_CELLS[i]['name']) + self.assertNotIn('capabilitiles', cell) + self.assertNotIn('password', cell) + + def test_detail(self): + req = self._get_request("cells/detail") + res_dict = self.controller.detail(req) + + self.assertEqual(len(res_dict['cells']), 2) + for i, cell in enumerate(res_dict['cells']): + self.assertEqual(cell['name'], FAKE_CELLS[i]['name']) + self.assertEqual(cell['capabilities'], FAKE_CAPABILITIES[i]) + self.assertNotIn('password', cell) + + def test_show_bogus_cell_raises(self): + req = self._get_request("cells/bogus") + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, 'bogus') + + def test_get_cell_by_name(self): + req = self._get_request("cells/cell1") + res_dict = self.controller.show(req, 'cell1') + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'cell1') + self.assertEqual(cell['rpc_host'], 'r1.example.org') + self.assertNotIn('password', cell) + + def test_cell_delete(self): + call_info = {'delete_called': 0} + + def fake_db_cell_delete(context, cell_name): + self.assertEqual(cell_name, 'cell999') + call_info['delete_called'] += 1 + + self.stubs.Set(db, 'cell_delete', fake_db_cell_delete) + + req = self._get_request("cells/cell999") + self.controller.delete(req, 'cell999') + self.assertEqual(call_info['delete_called'], 1) + + def test_delete_bogus_cell_raises(self): + req = self._get_request("cells/cell999") + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPNotFound, self.controller.delete, req, + 'cell999') + + def test_cell_create_parent(self): + body = {'cell': {'name': 'meow', + 'username': 'fred', + 'password': 'fubar', + 'rpc_host': 'r3.example.org', + 'type': 'parent', + # Also test this is ignored/stripped + 'is_parent': False}} + + req = self._get_request("cells") + res_dict = self.controller.create(req, body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'meow') + self.assertEqual(cell['username'], 'fred') + self.assertEqual(cell['rpc_host'], 'r3.example.org') + self.assertEqual(cell['type'], 'parent') + self.assertNotIn('password', cell) + self.assertNotIn('is_parent', cell) + + def test_cell_create_child(self): + body = {'cell': {'name': 'meow', + 'username': 'fred', + 'password': 'fubar', + 'rpc_host': 'r3.example.org', + 'type': 'child'}} + + req = self._get_request("cells") + res_dict = self.controller.create(req, body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'meow') + self.assertEqual(cell['username'], 'fred') + self.assertEqual(cell['rpc_host'], 'r3.example.org') + self.assertEqual(cell['type'], 'child') + self.assertNotIn('password', cell) + self.assertNotIn('is_parent', cell) + + def test_cell_create_no_name_raises(self): + body = {'cell': {'username': 'moocow', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_empty_string_raises(self): + body = {'cell': {'name': '', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_with_bang_raises(self): + body = {'cell': {'name': 'moo!cow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_with_dot_raises(self): + body = {'cell': {'name': 'moo.cow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_create_name_with_invalid_type_raises(self): + body = {'cell': {'name': 'moocow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'invalid'}} + + req = self._get_request("cells") + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_cell_update(self): + body = {'cell': {'username': 'zeb', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + res_dict = self.controller.update(req, 'cell1', body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'cell1') + self.assertEqual(cell['rpc_host'], FAKE_CELLS[0]['rpc_host']) + self.assertEqual(cell['username'], 'zeb') + self.assertNotIn('password', cell) + + def test_cell_update_empty_name_raises(self): + body = {'cell': {'name': '', + 'username': 'zeb', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + self.assertRaises(exc.HTTPBadRequest, + self.controller.update, req, 'cell1', body) + + def test_cell_update_invalid_type_raises(self): + body = {'cell': {'username': 'zeb', + 'type': 'invalid', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + self.assertRaises(exc.HTTPBadRequest, + self.controller.update, req, 'cell1', body) + + def test_cell_info(self): + caps = ['cap1=a;b', 'cap2=c;d'] + self.flags(name='darksecret', capabilities=caps, group='cells') + + req = self._get_request("cells/info") + res_dict = self.controller.info(req) + cell = res_dict['cell'] + cell_caps = cell['capabilities'] + + self.assertEqual(cell['name'], 'darksecret') + self.assertEqual(cell_caps['cap1'], 'a;b') + self.assertEqual(cell_caps['cap2'], 'c;d') + + def test_sync_instances(self): + call_info = {} + + def sync_instances(self, context, **kwargs): + call_info['project_id'] = kwargs.get('project_id') + call_info['updated_since'] = kwargs.get('updated_since') + + self.stubs.Set(cells_rpcapi.CellsAPI, 'sync_instances', sync_instances) + + req = self._get_request("cells/sync_instances") + body = {} + self.controller.sync_instances(req, body=body) + self.assertEqual(call_info['project_id'], None) + self.assertEqual(call_info['updated_since'], None) + + body = {'project_id': 'test-project'} + self.controller.sync_instances(req, body=body) + self.assertEqual(call_info['project_id'], 'test-project') + self.assertEqual(call_info['updated_since'], None) + + expected = timeutils.utcnow().isoformat() + if not expected.endswith("+00:00"): + expected += "+00:00" + + body = {'updated_since': expected} + self.controller.sync_instances(req, body=body) + self.assertEqual(call_info['project_id'], None) + self.assertEqual(call_info['updated_since'], expected) + + body = {'updated_since': 'skjdfkjsdkf'} + self.assertRaises(exc.HTTPBadRequest, + self.controller.sync_instances, req, body=body) + + body = {'foo': 'meow'} + self.assertRaises(exc.HTTPBadRequest, + self.controller.sync_instances, req, body=body) + + +class TestCellsXMLSerializer(test.TestCase): + def test_multiple_cells(self): + fixture = {'cells': fake_cells_api_get_all_cell_info()} + + serializer = cells_ext.CellsTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cells' % xmlutil.XMLNS_V10) + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree[1].tag, '{%s}cell' % xmlutil.XMLNS_V10) + + def test_single_cell_with_caps(self): + cell = {'id': 1, + 'name': 'darksecret', + 'username': 'meow', + 'capabilities': {'cap1': 'a;b', + 'cap2': 'c;d'}} + fixture = {'cell': cell} + + serializer = cells_ext.CellTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('name'), 'darksecret') + self.assertEqual(res_tree.get('username'), 'meow') + self.assertEqual(res_tree.get('password'), None) + self.assertEqual(len(res_tree), 1) + + child = res_tree[0] + self.assertEqual(child.tag, + '{%s}capabilities' % xmlutil.XMLNS_V10) + for elem in child: + self.assertIn(elem.tag, ('{%s}cap1' % xmlutil.XMLNS_V10, + '{%s}cap2' % xmlutil.XMLNS_V10)) + if elem.tag == '{%s}cap1' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'a;b') + elif elem.tag == '{%s}cap2' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'c;d') + + def test_single_cell_without_caps(self): + cell = {'id': 1, + 'username': 'woof', + 'name': 'darksecret'} + fixture = {'cell': cell} + + serializer = cells_ext.CellTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('name'), 'darksecret') + self.assertEqual(res_tree.get('username'), 'woof') + self.assertEqual(res_tree.get('password'), None) + self.assertEqual(len(res_tree), 0) + + +class TestCellsXMLDeserializer(test.TestCase): + def test_cell_deserializer(self): + caps_dict = {'cap1': 'a;b', + 'cap2': 'c;d'} + caps_xml = ("a;b" + "c;d") + expected = {'cell': {'name': 'testcell1', + 'type': 'child', + 'rpc_host': 'localhost', + 'capabilities': caps_dict}} + intext = ("\n" + "testcell1child" + "localhost" + "%s") % caps_xml + deserializer = cells_ext.CellDeserializer() + result = deserializer.deserialize(intext) + self.assertEqual(dict(body=expected), result) diff --git a/nova/tests/cells/test_cells_manager.py b/nova/tests/cells/test_cells_manager.py index 72ef3f1f096c..ef165f4edab4 100644 --- a/nova/tests/cells/test_cells_manager.py +++ b/nova/tests/cells/test_cells_manager.py @@ -38,6 +38,21 @@ class CellsManagerClassTestCase(test.TestCase): self.driver = self.cells_manager.driver self.ctxt = 'fake_context' + def _get_fake_responses(self): + responses = [] + expected_responses = [] + for x in xrange(1, 4): + responses.append(messaging.Response('cell%s' % x, x, False)) + expected_responses.append(('cell%s' % x, x)) + return expected_responses, responses + + def test_get_cell_info_for_neighbors(self): + self.mox.StubOutWithMock(self.cells_manager.state_manager, + 'get_cell_info_for_neighbors') + self.cells_manager.state_manager.get_cell_info_for_neighbors() + self.mox.ReplayAll() + self.cells_manager.get_cell_info_for_neighbors(self.ctxt) + def test_post_start_hook_child_cell(self): self.mox.StubOutWithMock(self.driver, 'start_consumers') self.mox.StubOutWithMock(context, 'get_admin_context') @@ -211,3 +226,14 @@ class CellsManagerClassTestCase(test.TestCase): # Now the last 1 and the first 1 self.assertEqual(call_info['sync_instances'], [instances[-1], instances[0]]) + + def test_sync_instances(self): + self.mox.StubOutWithMock(self.msg_runner, + 'sync_instances') + self.msg_runner.sync_instances(self.ctxt, 'fake-project', + 'fake-time', 'fake-deleted') + self.mox.ReplayAll() + self.cells_manager.sync_instances(self.ctxt, + project_id='fake-project', + updated_since='fake-time', + deleted='fake-deleted') diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py index 9973716f67e7..da45721ed613 100644 --- a/nova/tests/cells/test_cells_messaging.py +++ b/nova/tests/cells/test_cells_messaging.py @@ -14,11 +14,14 @@ """ Tests For Cells Messaging module """ +import mox from nova.cells import messaging +from nova.cells import utils as cells_utils from nova import context from nova import exception from nova.openstack.common import cfg +from nova.openstack.common import timeutils from nova import test from nova.tests.cells import fakes @@ -912,3 +915,46 @@ class CellsBroadcastMethodsTestCase(test.TestCase): self.src_msg_runner.bw_usage_update_at_top(self.ctxt, fake_bw_update_info) + + def test_sync_instances(self): + # Reset this, as this is a broadcast down. + self._setup_attrs(up=False) + project_id = 'fake_project_id' + updated_since_raw = 'fake_updated_since_raw' + updated_since_parsed = 'fake_updated_since_parsed' + deleted = 'fake_deleted' + + instance1 = dict(uuid='fake_uuid1', deleted=False) + instance2 = dict(uuid='fake_uuid2', deleted=True) + fake_instances = [instance1, instance2] + + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'instance_update_at_top') + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'instance_destroy_at_top') + + self.mox.StubOutWithMock(timeutils, 'parse_isotime') + self.mox.StubOutWithMock(cells_utils, 'get_instances_to_sync') + + # Middle cell. + timeutils.parse_isotime(updated_since_raw).AndReturn( + updated_since_parsed) + cells_utils.get_instances_to_sync(self.ctxt, + updated_since=updated_since_parsed, + project_id=project_id, + deleted=deleted).AndReturn([]) + + # Bottom/Target cell + timeutils.parse_isotime(updated_since_raw).AndReturn( + updated_since_parsed) + cells_utils.get_instances_to_sync(self.ctxt, + updated_since=updated_since_parsed, + project_id=project_id, + deleted=deleted).AndReturn(fake_instances) + self.tgt_msg_runner.instance_update_at_top(self.ctxt, instance1) + self.tgt_msg_runner.instance_destroy_at_top(self.ctxt, instance2) + + self.mox.ReplayAll() + + self.src_msg_runner.sync_instances(self.ctxt, + project_id, updated_since_raw, deleted) diff --git a/nova/tests/cells/test_cells_rpcapi.py b/nova/tests/cells/test_cells_rpcapi.py index b51bfa0c1bad..5e045aca9f93 100644 --- a/nova/tests/cells/test_cells_rpcapi.py +++ b/nova/tests/cells/test_cells_rpcapi.py @@ -204,3 +204,23 @@ class CellsAPITestCase(test.TestCase): expected_args = {'bw_update_info': bw_update_info} self._check_result(call_info, 'bw_usage_update_at_top', expected_args) + + def test_get_cell_info_for_neighbors(self): + call_info = self._stub_rpc_method('call', 'fake_response') + result = self.cells_rpcapi.get_cell_info_for_neighbors( + self.fake_context) + self._check_result(call_info, 'get_cell_info_for_neighbors', {}, + version='1.1') + self.assertEqual(result, 'fake_response') + + def test_sync_instances(self): + call_info = self._stub_rpc_method('cast', None) + self.cells_rpcapi.sync_instances(self.fake_context, + project_id='fake_project', updated_since='fake_time', + deleted=True) + + expected_args = {'project_id': 'fake_project', + 'updated_since': 'fake_time', + 'deleted': True} + self._check_result(call_info, 'sync_instances', expected_args, + version='1.1') diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 51f3a3f85cc1..15890cdcd50d 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -104,6 +104,7 @@ policy_data = """ "compute_extension:admin_actions:migrate": "", "compute_extension:aggregates": "", "compute_extension:agents": "", + "compute_extension:cells": "", "compute_extension:certificates": "", "compute_extension:cloudpipe": "", "compute_extension:cloudpipe_update": "", 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 3d69fad45e9b..fe0613646ef0 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 @@ -88,6 +88,14 @@ "namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1", "updated": "%(timestamp)s" }, + { + "alias": "os-cells", + "description": "%(text)s", + "links": [], + "name": "Cells", + "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", + "updated": "%(timestamp)s" + }, { "alias": "os-certificates", "description": "%(text)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 5953ba7040b5..2051d891ac1b 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 @@ -33,6 +33,9 @@ %(text)s + + %(text)s + %(text)s diff --git a/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl new file mode 100644 index 000000000000..2993b1df881f --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl @@ -0,0 +1,9 @@ +{ + "cell": { + "name": "cell3", + "username": "username3", + "rpc_host": null, + "rpc_port": null, + "type": "child" + } +} diff --git a/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl new file mode 100644 index 000000000000..d31a674a2fec --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl @@ -0,0 +1,2 @@ + + diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl new file mode 100644 index 000000000000..b16e12cd6940 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl @@ -0,0 +1,4 @@ +{ + "cells": [] +} + diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl new file mode 100644 index 000000000000..32fef4f04848 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl @@ -0,0 +1,2 @@ + + diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl new file mode 100644 index 000000000000..3d7a6c207c33 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "name": "cell1", + "username": "username1", + "rpc_host": null, + "rpc_port": null, + "type": "child" + }, + { + "name": "cell2", + "username": "username2", + "rpc_host": null, + "rpc_port": null, + "type": "parent" + }, + { + "name": "cell3", + "username": "username3", + "rpc_host": null, + "rpc_port": null, + "type": "child" + }, + { + "name": "cell4", + "username": "username4", + "rpc_host": null, + "rpc_port": null, + "type": "parent" + }, + { + "name": "cell5", + "username": "username5", + "rpc_host": null, + "rpc_port": null, + "type": "child" + } + ] +} diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl new file mode 100644 index 000000000000..58312201f6f1 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl index eeb191597bae..504f66f595c6 100644 --- a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl +++ b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl @@ -21,9 +21,14 @@ "zone": "internal" }, { - "host_name": "%(host_name)s", - "service": "conductor", - "zone": "internal" + "host_name": "%(host_name)s", + "service": "conductor", + "zone": "internal" + }, + { + "host_name": "%(host_name)s", + "service": "cells", + "zone": "internal" } ] } diff --git a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl index 25ef5a299656..4e9d3195d53e 100644 --- a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl @@ -5,4 +5,5 @@ + diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index e20d6881bfa9..f17dc025f8db 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -27,7 +27,7 @@ import nova.image.glance from nova.openstack.common import cfg from nova.openstack.common.log import logging from nova import service -from nova import test # For the flags +from nova import test from nova.tests import fake_crypto import nova.tests.image.fake from nova.tests.integrated.api import client @@ -35,6 +35,8 @@ from nova.tests.integrated.api import client CONF = cfg.CONF LOG = logging.getLogger(__name__) +CONF = cfg.CONF +CONF.import_opt('manager', 'nova.cells.opts', group='cells') def generate_random_alphanumeric(length): @@ -81,6 +83,7 @@ class _IntegratedTestBase(test.TestCase): self.scheduler = self.start_service('cert') self.network = self.start_service('network') self.scheduler = self.start_service('scheduler') + self.cells = self.start_service('cells', manager=CONF.cells.manager) self._start_api_service() diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 98ac6a230940..7c3157872bdd 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -54,6 +54,8 @@ CONF.import_opt('osapi_compute_extension', CONF.import_opt('vpn_image_id', 'nova.cloudpipe.pipelib') CONF.import_opt('osapi_compute_link_prefix', 'nova.api.openstack.common') CONF.import_opt('osapi_glance_link_prefix', 'nova.api.openstack.common') +CONF.import_opt('enable', 'nova.cells.opts', group='cells') +CONF.import_opt('db_check_interval', 'nova.cells.state', group='cells') LOG = logging.getLogger(__name__) @@ -2501,3 +2503,63 @@ class QuotaClassesSampleJsonTests(ApiSampleTestBase): class QuotaClassesSampleXmlTests(QuotaClassesSampleJsonTests): ctype = "xml" + + +class CellsSampleJsonTest(ApiSampleTestBase): + extension_name = "nova.api.openstack.compute.contrib.cells.Cells" + + def setUp(self): + # db_check_interval < 0 makes cells manager always hit the DB + self.flags(enable=True, db_check_interval=-1, group='cells') + super(CellsSampleJsonTest, self).setUp() + self._stub_cells() + + def _stub_cells(self, num_cells=5): + self.cells = [] + self.cells_next_id = 1 + + def _fake_cell_get_all(context): + return self.cells + + def _fake_cell_get(context, cell_name): + for cell in self.cells: + if cell['name'] == cell_name: + return cell + raise exception.CellNotFound(cell_name=cell_name) + + for x in xrange(num_cells): + cell = models.Cell() + our_id = self.cells_next_id + self.cells_next_id += 1 + cell.update({'id': our_id, + 'name': 'cell%s' % our_id, + 'username': 'username%s' % our_id, + 'is_parent': our_id % 2 == 0}) + self.cells.append(cell) + + self.stubs.Set(db, 'cell_get_all', _fake_cell_get_all) + self.stubs.Set(db, 'cell_get', _fake_cell_get) + + def test_cells_empty_list(self): + # Override this + self._stub_cells(num_cells=0) + response = self._do_get('os-cells') + self.assertEqual(response.status, 200) + subs = self._get_regexes() + return self._verify_response('cells-list-empty-resp', subs, response) + + def test_cells_list(self): + response = self._do_get('os-cells') + self.assertEqual(response.status, 200) + subs = self._get_regexes() + return self._verify_response('cells-list-resp', subs, response) + + def test_cells_get(self): + response = self._do_get('os-cells/cell3') + self.assertEqual(response.status, 200) + subs = self._get_regexes() + return self._verify_response('cells-get-resp', subs, response) + + +class CellsSampleXmlTest(CellsSampleJsonTest): + ctype = 'xml'